Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(useMedia): enhance SSR support by setting state during component mount #2774

Merged
merged 1 commit into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,25 +60,82 @@ Numeric values will be handled as an `em` unit.

### `useMedia` hook usage

```js
```tsx
import { useMedia } from '@dnb/eufemia/shared'

function Component() {
const { isSmall, isMedium, isLarge, isSSR } = useMedia()

return isSmall ? 'true' : 'false'
return isSmall && <IsVisibleWhenSmall />
}
```

To lower the possibility of CLS (Cumulative Layout Shift) on larger screens – you can make use of the `isSSR` property. Try to use it in combination with `isLarge`, because the negative CLS experience is most recognizable on larger screens:

```js
```tsx
import { useMedia } from '@dnb/eufemia/shared'

function Component() {
const { isSmall, isMedium, isLarge, isSSR } = useMedia()

return isLarge || isSSR ? 'true' : 'false'
return (isLarge || isSSR) && <IsVisibleDuringSsrAndWhenLarge />
}
```

During SSR, when no `window` object is available, all results are negative. But you can provide a `initialValue`:

```tsx
import { useMedia } from '@dnb/eufemia/shared'

function Component() {
const { isSmall } = useMedia({
initialValue: {
isSmall: true,
},
})

return isSmall && <IsVisibleDuringSSR />
}
```

Here are all the options:

```tsx
import { useMedia } from '@dnb/eufemia/shared'

function Component() {
const { isSmall } = useMedia({
/**
* Give a initial value, that is used during SSR as well.
* Default: null
*/
initialValue?: Partial<UseMediaResult>

/**
* If set to true, no MediaQuery will be used.
* Default: false
*/
disabled?: boolean

/**
* Provide a custom breakpoint
* Default: defaultBreakpoints
*/
breakpoints?: MediaQueryBreakpoints

/**
* Provide a custom query
* Default: defaultQueries
*/
queries?: Record<string, MediaQueryCondition>

/**
* For debugging
*/
log?: boolean
})

return isSmall
}
```

Expand Down
69 changes: 68 additions & 1 deletion packages/dnb-eufemia/src/shared/__tests__/useMedia.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ describe('useMedia', () => {
})
)

expect(count).toBe(24)
expect(count).toBe(28)
})

it('will return correct key based on size', async () => {
Expand Down Expand Up @@ -880,6 +880,73 @@ describe('useMedia', () => {
)
})
})

describe('ssr', () => {
beforeAll(() => {
global.window['__SSR_TEST__'] = true
})

afterAll(() => {
delete global.window['__SSR_TEST__']
})

it('will by default return false on all sizes', () => {
const { result } = renderHook(useMedia, { wrapper })

expect(result.current).toEqual(
expect.objectContaining({
isSSR: true,
isSmall: false,
isMedium: false,
isLarge: false,
key: null,
})
)
})

it('will return positive isSmall when in initialValue', () => {
const { result } = renderHook(useMedia, {
wrapper,
initialProps: {
initialValue: {
isSmall: true,
},
},
})

expect(result.current).toEqual(
expect.objectContaining({
isSSR: true,
isSmall: true,
isMedium: false,
isLarge: false,
key: 'small',
})
)
})

it('will return both positive isSmall and isLarge', () => {
const { result } = renderHook(useMedia, {
wrapper,
initialProps: {
initialValue: {
isSmall: true,
isLarge: true,
},
},
})

expect(result.current).toEqual(
expect.objectContaining({
isSSR: true,
isSmall: true,
isMedium: false,
isLarge: true,
key: 'large',
})
)
})
})
})

describe('useMedia without window.matchMedia', () => {
Expand Down
51 changes: 39 additions & 12 deletions packages/dnb-eufemia/src/shared/useMedia.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,22 @@ import type {
} from './MediaQueryUtils'
import { toPascalCase } from './component-helper'

const makeLayoutEffect = () => {
// SSR warning fix: https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
return typeof window === 'undefined'
? React.useEffect
: window['__SSR_TEST__'] // To be able to test this hook like we are in SSR land
? () => null
: React.useLayoutEffect
}

export type UseMediaProps = {
/**
* Give a initial value, that is used during SSR as well.
* Default: null
*/
initialValue?: Partial<UseMediaResult>

/**
* If set to true, no MediaQuery will be used.
* Default: false
Expand Down Expand Up @@ -78,32 +93,40 @@ type UseMediaQueryProps = {
export default function useMedia(
props: UseMediaProps = {}
): UseMediaResult {
const { disabled, breakpoints, queries = defaultQueries, log } = props
const {
initialValue = null,
disabled,
breakpoints,
queries = defaultQueries,
log,
} = props

const context = React.useContext(Context)

const refs = React.useRef({})
const defaults = React.useRef({})
const disabledRef = React.useRef(disabled)
const isMounted = React.useRef(false)
const isDisabled = React.useRef(disabled)
const [result, updateRerender] =
React.useState<UseMediaResult>(makeResult)

React.useEffect(() => {
// In StrictMode, the keys got empty,
// so we make the result again
if (Object.keys(refs.current).length) {
makeResult()
const useLayoutEffect = React.useMemo(makeLayoutEffect, [])

useLayoutEffect(() => {
if (!isMounted.current) {
isMounted.current = true
updateRerender(makeResult())
}

return removeListeners
}, []) // eslint-disable-line react-hooks/exhaustive-deps

React.useEffect(() => {
useLayoutEffect(() => {
// If it was disabled before
if (disabledRef.current && !disabled) {
if (isDisabled.current && !disabled) {
updateRerender(makeResult())
}
disabledRef.current = disabled
isDisabled.current = disabled
}, [disabled]) // eslint-disable-line react-hooks/exhaustive-deps

return result
Expand Down Expand Up @@ -136,7 +159,11 @@ export default function useMedia(
key,
})

const hasMatch = item?.mediaQueryList?.matches || false
const hasMatch = !isMounted.current
? typeof initialValue?.[name] !== 'undefined'
? initialValue[name]
: false
: item?.mediaQueryList?.matches || false
acc[name] = hasMatch
if (hasMatch) {
acc.key = key
Expand Down Expand Up @@ -170,7 +197,7 @@ export default function useMedia(
)

const event = createMediaQueryListener(mediaQueryList, (match) => {
if (!disabledRef.current && match) {
if (!isDisabled.current && match) {
const state = {
...defaults.current,
key,
Expand Down
Loading