reactjsreact-routersetstateremix.run

Wrapping setState in NavLink callback gives bad setState warning


I have a Tabs component that has bunch of NavLinks. I'm trying to show a loading spinner based on the NavLink's isPending property on the parent component. So I'm showing spinner when there is a navigation in the NavLink of Tabs component. I'm only triggering the showing spinner from the NavLink component. I'm handling turning the spinner off from the parent using navigation.

The parent component is something like:

import { useNavigation} from "@remix-run/react";

const [isTabNavigating, setIsTabNavigating] = useState(false);
const navigation = useNavigation();

useEffect(() => {
  if (navigation.state === "idle") setIsTabNavigating(false);
  }, [navigation.state]
);

return (
  <>
    <Tabs setIsTabNavigating={setIsTabNavigating} />
    { isTabNavigating ? <Spinner /> : <Outlet /> }
  </>
);

And the tabs component:

<>
  {tabItems.map((tab) => (
    <div>
       <NavLink to={tab.link}>
       {
         ({ isPending }) => {
           if (isPending) setIsTabNavigating(true);
           return <span>{tab.title}</span>
         }
       }
       </NavLink>
    </div>
  }
</>

I'm getting this warning: Warning: Cannot update a component while rendering a different component (NavLink). To locate the bad setState() call inside NavLink, follow the stack trace as described in https://reactjs.org/link/setstate-in-render


Solution

  • The code is enqueueing a state update during the component render, as an unintentional side-effect.

    You can abstract the children render prop into a React component such that the isTabNavigating state update can be properly enqueued in a lifecycle method, e.g. via a useEffect hook.

    Example:

    const TabItem = ({
      isPending,
      isTabNavigating,
      setIsTabNavigating,
      tab
    }) => {
      useEffect(() => {
        // If not currently tab navigating and isPending is true,
        // then update to start tab navigating.
        if (!isTabNavigating && isPending) {
          setIsTabNavigating(true);
        }
      }, [isPending, isTabNavigating, setIsTabNavigating]);
    
      return <span>{tab.title}</span>;
    };
    
    tabItems.map((tab) => (
      <div key={tab.link}>
        <NavLink to={tab.link}>
          {({ isPending }) => (
            <TabItem
              isPending={isPending}
              isTabNavigating={isTabNavigating}
              setIsTabNavigating={setIsTabNavigating}
              tab={tab}
            />
          )}
        </NavLink>
      </div>
    }
    

    Pass both isTabNavigating state and setIsTabNavigating callback down as props so they are accessible in the sub-ReactTree.

    const [isTabNavigating, setIsTabNavigating] = useState(false);
    const navigation = useNavigation();
    
    useEffect(() => {
      if (navigation.state === "idle"){
        setIsTabNavigating(false);
      }
    }, [navigation.state]);
    
    return (
      <>
        <Tabs
          isTabNavigating={isTabNavigating}
          setIsTabNavigating={setIsTabNavigating}
        />
        {isTabNavigating ? <Spinner /> : <Outlet />}
      </>
    );