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

Make hooks client-side rendering and server-side rendering compatible #339

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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
Loading