remix-run / react-router

Declarative routing for React

Home Page:https://reactrouter.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Bug]: React Router 6 useLoaderData returning cached data after fresh data is returned by the loader function

Nosherwan opened this issue · comments

What version of React Router are you using?

6.22.3

Steps to Reproduce

  • 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>
	);
}

Expected Behavior

New data should be passed to child components by useLoaderData

Actual Behavior

Stale data fetched first time is returned by useLoaderData.

I am also experiencing this issue

Loader data ia only revalidated after mutations (POST's) - but in these examples you're not performing a POST submission. In the code above, you don't have a method on <fetcher.Form> or fetcher.submit - and the default HTML behavior for form submissions is GET.

So, the fetcher is submitting a GET submission to the index route and executing the loader, and returning the new data on fetcher.data - but it's never causing a revalidation of the route loader data because no submissions were performed to mutate data.

In the codesandbox you did correctly have <fetcher.Form method="post">, but then you have a preventDefault call and a manual fetcher.submit() that was again lacking method so it was defaulting again to a GET.

The fix is to use <fetcher.Form method="post"> for declarative submissions or fetcher.submit(data, { method: 'post' }) for imperative. Once you are correctly posting, you need an action defined on your index route that handles the mutation.

I this is just a searchbox type UI and there's nothing to mutate, then your best bet is to use the route loader data initially and prefer the updated fetcher data once available.

let data = useLoaderData();
let fetcher = useFetcher();
let mostRecentData = fetcher.data || data;

Thanks @brophdawg11 this is new territory for me as it seems like we are trying to simulate the default behaviors for html forms, so by that logic this makes sense.
My thought process was that if loader is being called and data being fetched then the only thing left to do is re-validation.

Another simpler option I forgot to mention is that for a searchbox UI you can also just use navigations and skip the fetcher. <Form><input name="query" /></Form> will by default submit as a GET navigation and serialize the form data into the URL search params, and the changed params will trigger your loaders to revalidate. Then you don't even need a fetcher :)

@brophdawg11 I think that is what I was looking for, but somehow I started looking at useFetcher as it could serve CRUD altogether. I would suggest that RR6 docs need to be a bit more fleshed out for an easier description.