skip to Main Content

So I have a technical misunderstanding of react function component re-renders or some sort, when they happen exactly. In the provided basic app, whenever I click on the theme icon on the far left of the navbar(Header.js component), a click event handler changes the state, provided by a custom hook, which is specified in the useTheme.js file. The state updates successfully, Header component re-renders, updates the theme accordingly. However, this state change is not reflected in the Box.js file, where the label of the boxes should be "This a box dark" and vice versa. What am I missing?
Thanks for your response!

https://codesandbox.io/p/sandbox/patient-water-cn35lh?file=%2Fsrc%2Fcomponents%2Fhooks%2FuseTheme.js

Regards

Tried to add a useEffect hook to the affected component to monitor state changes, but they are not reflected.

2

Answers


  1. TL;DR: the state has to be shared over all your components. Use Context to provide the state over a subtree of components.


    With every initialization of useState a new state is generated. It dose not mater if its called within a component or in your case a custom hook. Every of your components has its own custom theme-state and they share nothing.

    React has context to provide a state to child-components without passing theme through via props and without the need of re rendering all child-components.

    A very short example:

    ThemeContext.jsx

    
    import React, {createContext, useContext, useState} from 'react';
    
    // First create a new context with its default-value
    const ThemeContext = createContext(['light', () => {}]);
    
    // Second create a Provider, that holds the State
    // This should wrap the Part, that should share the State
    // In Theming Context this should be placed around your App
    export const ThemeProvider = ({ children }) => {
      const themeState = useState('light');
    
      return (
        <ThemeContext.Provider value={themeState}>
          {children}
        </ThemeContext.Provider>
      )
    }
    
    // Third create a Hook, that provides the State
    export const useTheme = () => {
      return useContext(ThemeContext);
    }
    
    

    A Example that fits more to your useTheme Hook

    ThemeContext.jsx

    
    import React, {createContext, useContext, useState} from 'react';
    
    const ThemeContext = createContext({ 
      isDarkTheme: false,
      toggleTheme: () => {},
    });
    
    export const ThemeProvider = ({ children }) => {
      // usually you only used isDarkTheme - so store this value in the state
      const [isDarkTheme, setIsDarkTheme] = useState(false);
    
      // Put all your Logic in here, that is sate-related and not component related.
      // If needed, this is the place to update the html-element-attribute
      useEffect(() => {
        document.documentElement.setAttribute(
          "data-theme",
          isDarkTheme  ? 'dark' : 'light',
        );
      }, [theme]);
    
      // Map your response to the desired format
      // Use useMemo & useCallback to prevent unneeded re renders
      const themeState = useMemo(() => {
        return {
          isDarkTheme,
          // Implement a true toggle
          toggleTheme: setIsDarkTheme((prevIsDarkTheme) => !prevIsDarkTheme),
        }
      }, [isDarkTheme, setIsDarkTheme])
    
      return (
        <ThemeContext.Provider value={themeState}>
          {children}
        </ThemeContext.Provider>
      )
    }
    
    export const useTheme = () => {
      // Keep the Hook as slim as possible.
      // Only add Logic that is Component related, for this will be calculated within every component.
      return useContext(ThemeContext);
    }
    
    

    Integrate the second Example within your app/example

    App.jsx

    function App() {
      // Add the Provider to your App
      return (
        <ThemeProvider>
          <BrowserRouter>
            <Routes>
              <Route path="/" element={ <Header />} >
                <Route path="/body" element={ <Body />} />
                <Route path="/boxholder" element={ <BoxHolder /> } />
              </Route>
            </Routes>
          </BrowserRouter>
        </ThemeProvider>
      )
    }
    

    Box.jsx

    function Box() {
        // Use the Hook instead of your hook
        const { isDarkTheme, toggleTheme } = useTheme();
    
        return (
            <div className="box">
                <h3>This a Box {isDarkTheme ? "dark" : "light"}</h3>
                <button onClick={toggleTheme}>TOGGLE</button>
            </div>
        )
    };
    
    export default Box;
    
    Login or Signup to reply.
  2. State is not shared between hook instances. Everywhere you call a hook it will create a new state. So you can have useTheme.isDarkTheme === true in one place and useTheme.isDarkTheme === false in another.

    If you want to achieve this with the given structure, you can use useContext. In that case all you need to do is update useTheme and wrap your app in ThemeWrapper.

    // create the context with default values, they are used when
    // `useTheme` is called outside of the context provider (`ThemeWrapper`)
    const ThemeContext = createContext({
      isDarkTheme: false,
      onToggleTheme: () => { throw new Error("context not initiated") }
    })
    
    const useTheme = () => useContext(ThemeContext)
    
    // context provider, this component must be a shared parent
    // to all components that should share the same theme 
    const ThemeWrapper = ({ children }) => {
      const [theme, setTheme] = useState("light");
    
      useEffect(() => {
        document.documentElement.setAttribute("data-theme", theme);
      }, [theme]);
    
      const onToggleTheme = () => {
        let arg = theme === "light" ? "dark" : "light";
    
        setTheme(arg);
      }
    
      const isDarkTheme = (theme === "dark");
    
      return (
        // pass the values so that all consumers use the same values
        <ThemeContext.Provider value={{ isDarkTheme, onToggleTheme }} >
          {children}
        </ThemeContext.Provider>
      )
    }
    

    Now every component that is a child to ThemeWrapper will receive the current values as defined inside of ThemeWrapper when calling useTheme.

    An alternative approach would be to have one instance of your current useTheme hook and pass the values as properties to all components that need it.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search