Search code examples
reactjsnext.jsodometer

How can I integrate React Odometerjs in Next js


It is very sad that after 3 days trying I still fail to integrate Odometer js in Next js. I can't understand where I wrong in my code. Here is my code in CodeSandBox - https://codesandbox.io/s/long-moon-z9zqu

this is code-

export default function Home() {
  const [odometerValue, setOdometerValue] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      setOdometerValue(300);
    }, 1000);
  }, []);

  return (
    <Odometer
      value={odometerValue}
      format="(,ddd)"
      theme="default"
    />
  );
}

The npm pacjage I use - https://www.npmjs.com/package/react-odometerjs

Please see the code and solve the problem who can. It will be very help full for me.


Solution

  • I think the problem is that it's loading the react-odometerjs library dynamically, so the first time your Home component gets rendered, the library isn't loaded yet. So it renders the loading component of the dynamic options which just shows a 0. Because it's the first render your useEffect is run and that sets odometerValue to 300.

    A little bit later, the react-odometerjs library is loaded which causes Home to rerender. Home renders the Odometer component, but now this renders the real Odometer component instead of the loading component so it will render like an odometer, but the value is set to 300 for the first load, so it just sits at 300.

    If you add in a little delay before setting it to 300, then you can see it working. Here's an example:

    export default function Home() {
      const [odometerValue, setOdometerValue] = useState(0);
    
      useEffect(() => {
        setTimeout(() => {
          setOdometerValue(300);
        }, 1000);
      }, []);
    
      return (
        <Odometer
          value={odometerValue}
          format="(,ddd)"
          theme="default"
        />
      );
    }
    

    The problem with using a timer for this is that it can take different amounts of time for the library to actually load. The reason you need to load this library using dynamic is because you're using nextjs and it's doing Server Side Rendering (SSR) and when it does SSR it's rendering in nodejs which doesn't have the document or window globals defined and odometer.js uses those.

    So you have some choices:

    1. Use the setTimeout thing and get some slightly annoying behavior if the library loads slowly.
    2. Use another library to get the odometer effect.
    3. Fork the odometer library and remove it's dependency on document and window and then you can just use import {Odometer} from 'react-odometerjs' instead of using dynamic.
    4. Implement something to let you know when the library has loaded on the client side, so that you can set the initial value after the library has loaded. I've put a hacky version of this below.

    Hacky version to wait for library to load before setting odometerValue:

    let loadedCallback = null;
    let loaded = false;
    
    const Odometer = dynamic(async () => {
      const mod = await import("react-odometerjs");
      loaded = true;
      if (loadedCallback != null) {
        loadedCallback();
      }
      return mod;
    }, {
      ssr: false,
      loading: () => 0
    });
    
    export default function Home() {
      const [odometerLoaded, setOdometerLoaded] = useState(loaded);
      const [odometerValue, setOdometerValue] = useState(0);
        
      loadedCallback = () => {
        setOdometerLoaded(true);
      };
    
      useEffect(() => {
        if (odometerLoaded) {
          setOdometerValue(1);
        }
      }, [odometerLoaded]);
    
      useEffect(() => {
        setOdometerValue(300);
      }, [odometerValue]);
    
      return (
        <Odometer
          value={odometerValue}
          format="(,ddd)"
          theme="default"
        />
      );
    }