In a regular React App I'd use Redux to manage the state, where I'd dispatch the initial data before matching any route in App
, however, Redux is not advised in Remix, so I'm using useContext
instead.
Is there a way to call loaders to fetch initial data (e.g. session, objects, etc.) before/without having to match any route and to then store that data in the context
global store and then can be accessed by any component whithin the store? That way, the API will only be called during app initialization.
I'm at this moment calling the initial data in the loader of root.tsx
, getting it with useLoaderData
and then passing it as a prop to StoreProvider
to dispatch it in the global state, however, I don't think this should be done like that way.
export let loader: LoaderFunction = async ({ request }) => {
let user = await getUser(request);
const products = await db.product.findMany();
return { user: user?.username, products };
};
function App() {
const data = useLoaderData<LoaderData>();
return (
<html lang="en">
...
<StoreProvider initData={data}>
<body>
...
<Outlet />
<ScrollRestoration />
<Scripts />
{process.env.NODE_ENV === "development" && <LiveReload />}
</body>
</StoreProvider>
</html>
);
}
export default App;
I think doing the data loading on the root route loader is the best way.
If you don't like that approach you could also fetch on entry.server and entry.client.
For example in entry.client you probably have something like this:
import { hydrate } from "react-dom";
import { RemixBrowser } from "remix";
hydrate(<RemixBrowser />, document);
So you can change it to do the fetch before calling hydrate.
import { hydrate } from "react-dom";
import { RemixBrowser } from "remix";
fetch(YOUR_API_ENDPOINT)
.then(response => response.json())
.then(data => {
hydrate(
<YourContextProvider value={data}>
<RemixBrowser />
</YourContextProvider>,
document
)
});
And in entry.server you can change the handleRequest function to something like this:
import { renderToString } from "react-dom/server";
import { RemixServer } from "remix";
import type { EntryContext } from "remix";
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
let response = await fetch(YOUR_API_ENDPOINT)
let data = await response.json()
let markup = renderToString(
<YourContextProvider value={data}>
<RemixServer context={remixContext} url={request.url} />
</YourContextProvider>
);
responseHeaders.set("Content-Type", "text/html");
return new Response("<!DOCTYPE html>" + markup, {
status: responseStatusCode,
headers: responseHeaders
});
}
By doing it on entry.client and entry.server the fetch will only happen once and it will never be triggered again.
I still recommend you to do it inside the loader of the root so after an action it can be fetched again to keep the data updated.