Good day, I believe your aim could be succeded using react-router v6:
loader
Also, good article from official docs about Deferred Data Guide
The explanation contains from two parts:
- First part:
The core idea is that the loader function could be async and if you await your fetch data inside the component chunk do not trigger and will wait for this data before calling the element in router config.
So this allows you "keep the current page rendered until the new page is loaded"
- Second part:
But if you remove await and return a new promise as a response your lazy component chunk starts loading simultaneously with data fetching.
So this allows you to show UI (not the blank screen) and loader while data retrieving in the process
Conclusion:
So my idea is that for an initial load, we use the "second part", so load chunk with fetch in parallel, and for the following enters to this route load in loader needed chunks + fetch data in parallel and await both in loader before going to component
Let's deep into the example:
- We have a router with an async loader that for the initial load creates a fetch promise inside and passes through yourself, so fetch and chunk load simultaneously. And for subsequence loads await fetch and chunks in parallel (Promise.all, Promise.allSettled)
import { defer } from 'react-router-dom';
// store if it is initial load
let isInitialLoad = true;
const loaderDetails = async ({ request, params }) => {
const { yourParam } = params;
if (!yourParam) {
throw new Error('Error');
}
const responsePromise = fetch(
`url/${yourParam}`,
{
signal: request.signal,
},
).then((res) => res.json());
if (isInitialLoad) {
isInitialLoad = false;
return defer({ responsePromise });
}
// For subsequences calls we load all chunks + fetch data and await for result before further transfer
const [response] = await Promise.all([
responsePromise,
// ... other chunks
]);
return {
responsePromise: response,
};
};
- Set this loader to the router config:
const ROUTES = createBrowserRoutes({
{
path: '*',
element: () => import('YourContainerComponent'),
/**
* FYI: loader could be sync or async
* so we can apply here dynamic import
* This solves the problem when we have tons of route configurations
* and we load loader logic only for a particular route
* BUT this causes another HUGE problem:
* we must wait for dynamic loader import and only then start
* loading lazy component from the element prop.
*
* So we should make a call for all needed chunks inside the loader.
**/
loader: async (props) => {
Promise.all(import('loaderDetails'), ...otherChunks);
const loader = await import('loaderDetails');
return loader.default(props);
},
/* Sync approach doesn't have this problem
* BUT have another:
* Let's imagine we have 25 pages and load only one
* for this case we load all loader logic
* let's assume loader size is 20kb so it is 0.5 mb size total
* This is huge, we don't need 480kb for displaying our page
* My conclusion: we should almost always strive to first approach
*/
// loader: loaderDetails,
});
and initialize in the App:
import { Suspense } from 'react';
import { RouterProvider } from 'react-router-dom';
/**
* Suspense need if we assign async requests in ROUTES config
**/
const App = () => (
<Suspense>
<RouterProvider router={ROUTES} />
</Suspense>
);
Retrieve this data in a particular component from react-router:
const YourContainerComponent = () => {
const loaderResponse = useLoaderData(); // return { responsePromise }
return (
<Suspense>
<Await
key={'UNIQUE_KEY_PROP_IDENTIFIER'}
resolve={loaderResponse.responsePromise}
errorElement={'some error'}
>
<YourComponent />
</Await>
</SuspenseDefault>
)
}
const YourPresentationComponent = () => {
// get data direct here
// component wait for retrieved data
// while he waiting on UI shows a fallback property from <Suspense fallback={undefined}>
const loaderData = useAsyncValue();
return (
<>
Show data: {JSON.stringify(loaderData)}
</>
);
}
export default YourContainerComponent;
In addition to showing loader I propose creating a global loader for it, we should play around with it:
- For showing the global loader we can create a component and get the state of loading and show the loader if the state === 'loading'
const GlobalLoader = () => {
const { state } = useNavigation();
const isEnabled = state === 'loading';
if (isEnabled) {
return state;
}
return null;
};
Hope it helps you and pushes for great solutions