Search code examples
javascriptreactjsreact-hookshtml5-canvas

How to re-render a React functional component within useEffect?


I have a top-level component that uses a library to determine whether a user's browser is in light or dark mode. It's then being used to set the theme for the app, which include HTML Canvas components (this is important because those do not act like normal React components). Currently, the canvas components do not show the correct theme without a re-render (either a page reload or a hot reload in dev mode).

How do I get the App component to re-render without a reload? For example, in local dev mode, just the component re-renders when I hit save, and this is the exact behavior I'd like to replicate in the app.

According to other answers on SO, useReducer is the correct way to force a re-render in a functional component, but the following does not work for me:

function App(props) {
  // this is not working 
  const [, forceUpdate] = useReducer((x: number) => x + 1, 0);

  useEffect(() => {
    setDarkMode(getColorTheme() === "dark");
    // this is not working 
    forceUpdate();
  }, [getColorTheme()]);
}
  return (
    <div
      className={(darkMode ? "dark-mode-theme" : "")}
    >
      ...
    </div>
  )
}

Solution

  • A functional component re-renders when:

    1. any props change referential identity
    2. useState's setState / useReducer's dispatch is called
    3. useContext value changes referential identity

    Note: setState will abort the rerender if the next state is the same (referentially speaking) as the previous state.

    dispatch also has this behavior for when the resultant of the reducer returns the same state as previous.

    Looking at your code, the useEffect is not evaluating when theme changes because App is not rerendering. With React DevTools, you can set the option to make components flash when they rerender. Does the App component flash when theme is changed? This would explain why your code is not working.

    I suggest the following hook to solve our specific problem.

    /**
     * subscribes to colorTheme changes on mount.
     * unsubscribes to colorTheme changes on unmount.
     * @returns colorTheme
     **/
    function useColorTheme(): ColorTheme {
      const [colorTheme, setColorTheme] = useState<ColorTheme>(getColorTheme);
      useEffect(() => {
        return onColorThemeChanged(({ newColorTheme }) => setColorTheme(newColorTheme);
      }, []);
      return colorTheme;
    }
    
    function App() {
      const colorTheme = useColorTheme(); // rerenders on  colorTheme change.
    }