Search code examples
reactjsreact-routerreact-router-dom

React Router 6 useLoaderData returning cached data


  • I am using a simple vite react.js (v18) app client app with react router v6.
  • In react router dom version 6.22.3, I am using useFetcher's fetcher.Form component and calling fetcher.submit on change inside the form.
  • This successfully changed the request params and the loader function of the parent route for the component is called again.
  • However the useLoaderData in that route still returns the old data to child components.

Edit: Just to clarify the loader function itself runs correctly in the index route and returns the correct userData. However the data returned inside the component via useLoaderData is incorrect after the fetcher.submit calls.

As requested below is a link to a codeSandbox link that demonstrates the issue: CodeSandBox Link

Below is the create router:

const router = createBrowserRouter([
    {
        path: '/',
        element: <Root />,
        errorElement: <ErrorPage />,
        children: [
            { index: true, element: <Index />, loader: indexLoader },
        ],
    }
]);

const rootElement = document.getElementById('root');

if (rootElement) {
    ReactDOM.createRoot(rootElement).render(
        <React.StrictMode>
            <RouterProvider router={router} />
        </React.StrictMode>
    );
} else {
    console.error("Element with ID 'root' not found in the document");
}

then the index route:

import RangePicker from '../components/rangePicker';


export async function loader({ request }) {
    
    const url = new URL(request.url);
    const startDate = url.searchParams.get('startDate');
    const endDate = url.searchParams.get('endDate');
    // This gets called correctly on every fetcher.submit();
    const userData = await getUserData(startDate, endDate);

    return { userData };
}

export default function Index() {
    // userData if logged gets called once with new data, but
    // after that gets called again with old data
    const { userData } = useLoaderData();
    
    
    return (
        <>
            <div>
                <RangePicker />
            </div>
            <div>
                <User data={userData} />
            </div>
        </>
    );
}

Below is the RangePicker component:

import { addDays } from 'date-fns';
import { useState } from 'react';
import { DateRangePicker } from 'react-date-range';
import { useFetcher } from 'react-router-dom';

export default function RangePicker() {
    const [state, setState] = useState([
        {
            startDate: new Date(),
            endDate: addDays(new Date(), 7),
            key: 'selection',
        },
    ]);

    const fetcher = useFetcher();

    const handleDateRangeChange = (item) => {
        setState([item.selection]);
        const startDate = item.selection.startDate;
        const endDate = item.selection.endDate;
        //successfully triggers the loader for parent, but useLoaderData
        // still returns previous data.
        fetcher.submit(
            { startDate, endDate }
        );
    };

    return (
        <fetcher.Form>
            <DateRangePicker
                onChange={handleDateRangeChange}
                moveRangeOnFirstSelection={false}
                ranges={state}
            />
        </fetcher.Form>
    );
}


Solution

  • As I posted this as an issue on RR-github page as well. I got a more comprehensive answer there

    But the answer is that GET requests do not trigger validation in react-router v6; only POST or mutations do.

    • The solution is to use method='post' in the form.
    • do not call preventDefault() in the changeHandler as that will not send request method.
    • And then call useFetcher in the parent index route and consolidate the data returned from useLoaderData.
    • Below code is not tested but shows the resolution
    export default function Index() {
        // userData if logged gets called once with new data, but
        // after that gets called again with old data
        const { userData } = useLoaderData();
        let fetcher = useFetcher();
        let mostRecentData = fetcher.data || userData;
        
        return (
            <>
                <div>
                    <RangePicker />
                </div>
                <div>
                    <User data={mostRecentData} />
                </div>
            </>
        );
    }