Skip to content

Commit

Permalink
Make hooks client-side rendering and server-side rendering compatible
Browse files Browse the repository at this point in the history
  • Loading branch information
leroykorterink committed May 3, 2024
1 parent aba651b commit a1b62eb
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 20 deletions.
5 changes: 4 additions & 1 deletion src/hooks/useContentRect/useContentRect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import { useResizeObserver } from '../useResizeObserver/useResizeObserver.js';
*/
export function useContentRect(
target: Unreffable<Element | null>,
serverSideRendering = false,
): RefObject<DOMRectReadOnly | null> {
const contentRectRef = useRef<DOMRectReadOnly | null>(null);
const contentRectRef = useRef<DOMRectReadOnly | null>(
serverSideRendering ? null : unref(target)?.getBoundingClientRect() ?? null,
);

const onResize = useCallback<ResizeObserverCallback>((entries): void => {
contentRectRef.current = entries.at(0)?.contentRect ?? null;
Expand Down
9 changes: 7 additions & 2 deletions src/hooks/useContentRectState/useContentRectState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ import { useResizeObserver } from '../useResizeObserver/useResizeObserver.js';
* A hook that returns the content rectangle of the target element.
* The content rectangle is updated whenever the target element is resized.
*/
export function useContentRectState(target: Unreffable<Element | null>): DOMRectReadOnly | null {
const [contentRect, setContentRect] = useState<DOMRectReadOnly | null>(null);
export function useContentRectState(
target: Unreffable<Element | null>,
serverSideRendering = false,
): DOMRectReadOnly | null {
const [contentRect, setContentRect] = useState<DOMRectReadOnly | null>(
serverSideRendering ? null : unref(target)?.getBoundingClientRect() ?? null,
);
const rafRef = useRef(0);

const onResize = useCallback<ResizeObserverCallback>((entries) => {
Expand Down
4 changes: 3 additions & 1 deletion src/hooks/useMediaDuration/useMediaDuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { type MutableRefObject, useCallback, useEffect, useState } from 'react';
export function useMediaDuration(
mediaElementRef: MutableRefObject<HTMLMediaElement | null>,
): number {
const [mediaDuration, setMediaDuration] = useState<number>(Number.NaN);
const [mediaDuration, setMediaDuration] = useState<number>(
mediaElementRef.current?.duration ?? Number.NaN,
);

const updateDuration = useCallback(() => {
setMediaDuration(mediaElementRef.current?.duration ?? Number.NaN);
Expand Down
37 changes: 31 additions & 6 deletions src/hooks/useMediaQuery/useMediaQuery.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,48 @@ Hook that returns true when the media query matches.
function useMediaQuery(mediaQueryOrVariableName: string, defaultValue = false): boolean;
```

## Usage
## Using a media query string

Define the media query in your CSS variables.
Use the hook in your component using a media query string.

```tsx
function DemoComponent() {
const isMinWidth480px = useMediaQuery('(min-width: 480px)');

return <div>{isMinWidth480px ? 'large' : 'small'}</div>;
}
```

## Using a CSS variable

To use the hook in your component using the CSS variable, define the media query in your CSS
variables.

```css
:root {
--min-width-480: (min-width: 480px);
}
```

Use the hook in your component using the CSS variable or a custom media query.
Use the media query variable as the first argument.

```tsx
function DemoComponent() {
const isMinWidth480px = useMediaQuery('--min-width-480');

return <div>{isMinWidth480px ? 'large' : 'small'}</div>;
}
```

## Server-side rendering

The hook returns the default value when the `matchMedia` API is not available (for example in
server-side rendering). Set a default value if you get hydration mismatch errors.

```tsx
function DemoComponent() {
const isMinWidth480pxUsingVar = useMediaQuery('--min-width-480');
const isMinWidth480pxUsingQuery = useMediaQuery('(min-width: 480px)');
const isMinWidth480px = useMediaQuery('(min-width: 480px)', true);

return <div>{isMinWidth480pxUsingVar && isMinWidth480pxUsingQuery ? 'large' : 'small'}</div>;
return <div>{isMinWidth480px ? 'large' : 'small'}</div>;
}
```
23 changes: 17 additions & 6 deletions src/hooks/useMediaQuery/useMediaQuery.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ const css = `
}
`;

function DemoComponent(): ReactElement {
const isMinWidth420px = useMediaQuery('--min-width-420');
type DemoComponentProps = { defaultValue?: boolean };
function DemoComponent({ defaultValue }: DemoComponentProps): ReactElement {
const value = useMediaQuery('--min-width-420', defaultValue);

return (
<>
Expand All @@ -31,8 +32,18 @@ function DemoComponent(): ReactElement {
<p className="mb-0">Resize the viewport to see the useMediaQuery hook in action.</p>
</div>

<code
style={{
whiteSpace: 'pre',
}}
>{`:root {
--min-width-420: (min-width: 420px);
}\n\n`}</code>

<div>
{isMinWidth420px ? 'Viewport is wider than 420px' : 'Viewport is narrower than 420px'}
Default value: {String(defaultValue)}
<br />
Matches: <span data-testid="value">{String(value)}</span>
</div>
</>
);
Expand All @@ -58,7 +69,7 @@ export const Mobile: Story = {
},
async play({ canvasElement }) {
const canvas = within(canvasElement);
canvas.getByText('Viewport is narrower than 420px');
expect(canvas.getByTestId('value').textContent).toBe('false');
},
};

Expand All @@ -83,7 +94,7 @@ export const Desktop: Story = {
async play({ canvasElement }) {
const canvas = within(canvasElement);

// Using findByText because initial state is different to support SSR
expect(await canvas.findByText('Viewport is wider than 420px')).toBeVisible();
// Using getByText because initial state is different to support SSR
expect(canvas.getByTestId('value').textContent).toBe('true');
},
};
10 changes: 6 additions & 4 deletions src/hooks/useMediaQuery/useMediaQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,18 @@ export function getMediaQueryList(
* Hook that returns a boolean indicating whether the media query matches.
*
* @param mediaQueryOrVariableName - The name of the CSS variable that describes the media query.
* @param defaultValue - The default value to return if the matchMedia API is not available.
* @param defaultValue - The default value to return if the matchMedia API is not available (set a value to make this hook work in SSR mode).
*/
export function useMediaQuery(
mediaQueryOrVariableName: MediaQueryValues,
defaultValue = false,
): boolean {
defaultValue?: boolean,
): boolean | undefined {
const [mediaQueryList, setMediaQueryList] = useState<MediaQueryList | undefined>(() =>
getMediaQueryList(mediaQueryOrVariableName),
);
const [matches, setMatches] = useState<boolean | undefined>(defaultValue);
const [matches, setMatches] = useState<boolean | undefined>(
defaultValue === undefined ? mediaQueryList?.matches : defaultValue,
);

useEffect(() => {
const newMediaQueryList = getMediaQueryList(mediaQueryOrVariableName);
Expand Down

0 comments on commit a1b62eb

Please sign in to comment.