Search code examples
reactjstypescriptreact-hooksjsxreact-state

React Router caught the following error during render Error: Rendered fewer hooks than expected


I'm trying to get the width of an element but I get an error when I invoke a function from the container component Sidebar: onClick={() => setActiveComponent(sidebarViews[1])}. Why is this error occurring?

Sidebar.tsx

function Sidebar() {
  const [activeComponent, setActiveComponent] = useState(PropertyList);
  const sidebarViews = [<Filters/>, <PropertyList/>]; // sidebarViews : JSX.Element[]

  return (
    <div className="sidebar">
      <div className="sidebar-top">
        <Navigation
          sidebarViews={sidebarViews}
          setActiveComponent={setActiveComponent}
        />
      </div>
      <div className="sidebar-content">
        {activeComponent}
      </div>
    </div>        
  );
}

function Navigation({
  sidebarViews,
  setActiveComponent
}: {
  sidebarViews: JSX.Element[],
  setActiveComponent: React.Dispatch<React.SetStateAction<JSX.Element>>
}) {
  return (
    <div style={{backgroundColor: "blue", color: "white"}}>                
      <div className="sidebar-navigation-bar">
        <NavigationButton
          onClick={() => setActiveComponent(sidebarViews[0])}
          icon="tune"
          key="0"
        ></NavigationButton>
        <NavigationButton
          onClick={() => setActiveComponent(sidebarViews[1])}
          icon="pin_drop"
          key="1"
        ></NavigationButton>
      </div>
    </div>
  )
}

function NavigationButton(
  { onClick, icon }: { onClick: any, icon: string }
) {
  return (
    <span onClick={onClick} className="material-icons">
      {icon}
    </span>
  )
}

PropertyList

import Property from './property.tsx';
import { useCallback, useEffect, useRef, useState } from 'react';

function PropertyList() {
  const ref = useRef<HTMLHeadingElement>(null);

  useEffect(() => {
    console.log('width', ref.current ? ref.current.offsetWidth : 0);
  }, [ref.current]);

  return (
    <div className="property-list">
      <Property key="1"></Property>
      <Property key="2"></Property>
    </div>
  );
}

export default PropertyList

I get this error:

Error handled by React Router default ErrorBoundary: Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.

DefaultErrorComponent@http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=91faa1fe:3960:15
RenderErrorBoundary@http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=91faa1fe:3992:5
DataRoutes@http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=91faa1fe:5154:7
Router@http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=91faa1fe:4421:7
RouterProvider@http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=91faa1fe:4969:7
localhost:5173:13851:25
    overrideMethod (index):13851
    overrideMethod (index):13898
    DefaultErrorComponent hooks.tsx:536
    React 14
    onClick sidebar.tsx:34
    React 23
    <anonymous> main.tsx:25 ```

I'm trying to set activeComponent to the PropertyList component to render it and get the width.


Solution

  • Issue

    The issue here is that you are passing functions to the useState hook and state updater functions which are being interpreted as lazy initialization functions (in the case of useState(PropertyList);) and functional state updates (in the case of setActiveComponent(sidebarViews[0])).

    When you pass functions in either of the above cases React will invoke the function and pass the returned result value for the state. This is a problem because in React we never manually/directly call our React functions. The issue is that doing this is calling any internal React hooks outside the React component lifecycle.

    On the initial render the PropertyList component is rendered which calls a useEffect hook. When the state is updated to render the Filters component, it appears no new hooks are called and there is now one less React hook called, thus producing the error.

    Solution

    You could update the logic to use functions that return the React component you wish to store in the state (versus the result of calling the React function). We basically replace the state type from JSX.Element to React.ComponentType, and use functional updates.

    Example:

    Sidebar

    function Sidebar() {
      // Initializer function that returns the React function
      const [ActiveComponent, setActiveComponent] = useState<React.ComponentType>(
        () => PropertyList
      );
    
      // Array of function component references
      const sidebarViews = [Filters, PropertyList]; // inferred React.ComponentType[]
    
      return (
        <div className="sidebar">
          <div className="sidebar-top">
            <Navigation
              sidebarViews={sidebarViews}
              setActiveComponent={setActiveComponent}
            />
          </div>
          <div className="sidebar-content">
            {/* Render component as JSX 🙂 */}
            <ActiveComponent />
          </div>
        </div>
      );
    }
    

    Navigation

    function Navigation({
      sidebarViews,
      setActiveComponent,
    }: {
      sidebarViews: React.ComponentType[];
      setActiveComponent: React.Dispatch<React.SetStateAction<React.ComponentType>>;
    }) {
      return (
        <div style={{ backgroundColor: "blue", color: "white" }}>
          <div className="sidebar-navigation-bar">
            <NavigationButton
              // Functional state update to return the React function
              onClick={() => setActiveComponent(() => sidebarViews[0])}
              icon="tune"
              key="0"
            />
            <NavigationButton
              // Functional state update to return the React function
              onClick={() => setActiveComponent(() => sidebarViews[1])}
              icon="pin_drop"
              key="1"
            />
          </div>
        </div>
      );
    }
    

    An alternative solution would be to store the key that represents the component you wish to render at runtime and compute the component.

    const componentsMap = {
      Filters,
      PropertyList,
    };
    

    Sidebar

    function Sidebar() {
      const [activeComponent, setActiveComponent] =
        useState<keyof typeof componentsMap>("PropertyList");
      const sidebarViews: (keyof typeof componentsMap)[] = [
        "Filters",
        "PropertyList",
      ];
    
      // Compute the component to render
      const ActiveComponent = componentsMap[activeComponent];
    
      return (
        <div className="sidebar">
          <div className="sidebar-top">
            <Navigation
              sidebarViews={sidebarViews}
              setActiveComponent={setActiveComponent}
            />
          </div>
          <div className="sidebar-content">
            {/* Render component as JSX 🙂 */}
            <ActiveComponent />
          </div>
        </div>
      );
    }
    

    Navbar

    function Navigation({
      sidebarViews,
      setActiveComponent,
    }: {
      sidebarViews: (keyof typeof componentsMap)[];
      setActiveComponent: React.Dispatch<
        React.SetStateAction<keyof typeof componentsMap>
      >;
    }) {
      return (
        <div style={{ backgroundColor: "blue", color: "white" }}>
          <div className="sidebar-navigation-bar">
            <NavigationButton
              onClick={() => setActiveComponent("Filters")}
              icon="tune"
              key="0"
            />
            <NavigationButton
              onClick={() => setActiveComponent("PropertyList")}
              icon="pin_drop"
              key="1"
            />
          </div>
        </div>
      );
    }