nico.fyi
    Published on

    useSyncExternalStore is awesome

    Authors
    • avatar
      Name
      Nico Prananta
      Twitter
      @2co_p

    Recently, someone asked on Twitter how to render the user's browser time in Next.js, which uses React Server Components, without encountering hydration errors. While Dan Abramov mentioned it's possible to suppress hydration mismatch errors using the suppressHydrationWarning prop, displaying the browser's time without this workaround is indeed feasible.

    My initial approach was to check for the window object on the server:

    if (typeof window === 'undefined') {
      return null
    }
    
    return <>{new Date().toISOString()}</>
    

    However, it was correctly noted that this still leads to hydration mismatches. This led me to utilize useState and useEffect:

    'use client';
    
    import { useEffect, useState } from 'react';
    
    const TimeComponent = () => {
      const [date, setDate] = useState<string | null>(null);
    
      useEffect(() => {
        if (typeof window === 'undefined') {
          return;
        }
        setDate(new Date().toISOString());
      }, []);
    
      return <p>{date}</p>;
    };
    
    export default TimeComponent;
    

    I was then reminded that achieving the same result with useSyncExternalStore is also possible. Although I seldom use this API, experimentation confirmed that it works:

    'use client';
    
    import { useSyncExternalStore } from 'react';
    
    const TimeComponentWithExternalStore = () => {
      const date = useSyncExternalStore(
        () => () => {},
        () => new Date().toISOString(),
        () => ''
      );
    
      return <p>{date}</p>;
    };
    
    export default TimeComponentWithExternalStore;
    

    We can go a step further by creating a component that renders only in the browser using useSyncExternalStore:

    'use client';
    import { ReactNode, useSyncExternalStore } from 'react';
    
    const ClientOnly = ({ children }: { children: ReactNode }) => {
      const isClient = useSyncExternalStore(
        () => () => {},
        () => true,
        () => false
      );
    
      if (isClient) {
        return <>{children}</>;
      }
    
      return null;
    };
    
    export default ClientOnly;
    

    This component then can be used within a server component:

    // /app/something/page.tsx
    import ClientOnly from './client-only';
    
    const ExperimentPage = async () => {
      return (
        <div className="flex flex-col space-y-2">
          <ClientOnly>
            <p>{new Date().toISOString()}</p>
          </ClientOnly>
        </div>
      );
    };
    
    export default ExperimentPage;
    

    Pretty neat, right? Have you incorporated useSyncExternalStore in your projects? I'd love to hear about it!


    Are you working in a team environment and your pull request process slows your team down? Then you have to grab a copy of my book, Pull Request Best Practices!