Search code examples
reactjsreact-routeruse-effect

State from previous outlet with react-router


I have simple setup with outlet and dynamic routes and on route change I want to fetch new data.

import React, { useEffect, useState } from 'react';

import { Link, Outlet, useParams } from 'react-router-dom';

export default function OutletContainer() {
  const [apiResponse, setApiResponse] = useState(null);

  let { outletComponentId } = useParams();

  useEffect(() => {
    // keeps api response state from previous route.
    console.log(' ue inv id', apiResponse, outletComponentId);

    fetch(
      `https://anapioficeandfire.com/api/characters/${outletComponentId}`
    ).then((r) => setApiResponse(r.url));

    return () => {
      console.log('runcleanup');
      setApiResponse(null);
    };
  }, [outletComponentId]);

  return (
    <div style={{ display: 'flex' }}>
      <nav style={{ borderRight: 'solid 1px', padding: '1rem' }}>
        {console.log('api', apiResponse, outletComponentId)}
        <Link key={13} to={`/outletContainer/${13}`}>
          13
        </Link>{' '}
        <Link key={12} to={`/outletContainer/${12}`}>
          12
        </Link>{' '}
        <Link key={2} to={`/outletContainer/${2}`}>
          2
        </Link>
      </nav>
      <Outlet />
    </div>
  );
}

So, the problem: When i swtich from /outletContainer/${12} to /outletContainer/${13} console.log in useEffect keeps the apiResponse value from route 12 in route 13, even thou cleanup method in useEffect set apiResponse state null

  return () => {
      console.log('runcleanup');
      setApiResponse(null);
    };

My questions are:

1.Why is this happening? I was reading Dans blog post about useEffect but I got even more confused. Initial TLDR gave me hope, but down the road I got lost. enter image description here

2.How to set state of a apiResponse to null each time i switch route.

You can check whole code on a stackblitz


Solution

  • I think this is just an issue of misunderstanding Javascript closures. apiResponse is missing from the useEffect hook's dependency (for good reason since including it would create a render loop), so the code appears to be logging a stale state value.

    useEffect(() => {
      // keeps api response state from previous route.
      console.log(' ue inv id', apiResponse, outletComponentId);
    
      fetch(
        `https://anapioficeandfire.com/api/characters/${outletComponentId}`
      ).then((r) => setApiResponse(r.url));
    
      return () => {
        console.log('runcleanup');
        setApiResponse(null);
      };
    }, [outletComponentId]);
    

    You are also console logging in the render return as an unintentional side-effect. Other than these I don't see any real issue here with the state updates.

    I suggest the following for accurate value logging. Move the extraneous logs into a separate useEffect hook to log when that value updates.

    export default function OutletContainer() {
      const [apiResponse, setApiResponse] = useState(null);
    
      const { outletComponentId } = useParams();
    
      useEffect(() => {
        console.log('useEffect', { outletComponentId }); // <-- log when outletComponentId changes
    
        if (outletComponentId) { // <-- only fetch, and update apiResponse, if outletComponentId
          fetch(
            `https://anapioficeandfire.com/api/characters/${outletComponentId}`
          ).then((r) => {
            console.log("update apiResponse", r.url); // <-- enqueue state update
            setApiResponse(r.url);
          });
        }
    
        return () => {
          console.log('run cleanup');
          setApiResponse(null);
        };
      }, [outletComponentId]); // <-- outletComponentId is dependency
    
      useEffect(() => {
        console.log('api updated', apiResponse); // <-- log when apiResponse changes
      }, [apiResponse]); // <-- apiResponse is dependency
    
      useEffect(() => {
        console.log("Rendered"); // <-- every render cycle
      });
    
      return (
        <div style={{ display: 'flex' }}>
          <nav style={{ borderRight: 'solid 1px', padding: '1rem' }}>
            <Link key={13} to={`/outletContainer/${13}`}>
              13
            </Link>{' '}
            <Link key={12} to={`/outletContainer/${12}`}>
              12
            </Link>{' '}
            <Link key={2} to={`/outletContainer/${2}`}>
              2
            </Link>
          </nav>
          <Outlet />
        </div>
      );
    }
    

    Produced Logs:

    enter image description here

    When outlet 13 is clicked the cleanup function runs and enqueues an apiResponse state update to null. Since the existing apiResponse state value is already null the update is ignored. The useEffect hook runs and updates the apiResponse state and there's the log from the second useEffect hook.

    When outlet 12 is clicked the cleanup function runs and this time we see the update go through and log that the apiResponse was set back to null. We also see the fetch made and update the apiResponse to the new value.