If you're here because of the catchy title, that's great! I'm all for prioritizing HTML-first development in web apps. This article is about how I like building web applications.
I think making web applications with server-side rendering is usually the right approach. But for apps like Figma or those needing a lot of interactivity, it's better to render them on the client side.
The main focus of this article is on the React UI library. However, the concepts discussed here can also be applied to other libraries or frameworks like Solid.js, Vue.js, and more.
As all of us know React is an UI library and that means that components in our application should be only in charge of rendering content not fetching and doing server actions ( react is not a data library). we should use a ssr framework ( remix, next.js) or writing our own framework :)
but what about existing csr applications out there? should we rewrite them?
I think “NO”.
There are two solutions to consider. The first solution involves rewriting the application from scratch while reusing as much code from the client-side rendering (CSR) project as possible. Personally, I strongly disagree with this approach, and my reasons are straightforward. Firstly, there might be limitations in terms of available resources and developers to undertake a complete re-creation of the application. Secondly, rewriting essentially means not leveraging the existing codebase, which strikes me as counterintuitive.
Now, when is starting from scratch a viable solution? It becomes a reasonable option when there's virtually nothing that can be repurposed or reused from the previous project.
The second solution involves partially updating and refactoring the code to align with our objectives.
Should people that satisfied with the CSR follow this article yet? the answer is YES.
most probably in the CSR applications out there you use useEffect
to fetch your data or you are using a library that doest that for you, so you shouldn't do that any more. by following this article you will find the solution.
Many React applications use the react-router-dom
library. The creators of this library have made changes and provided steps to prioritize an HTML-first approach. This makes it easier to switch to a server-side rendering (SSR) framework if you plan to, or it ensures you're using React correctly even if you're not migrating.
What are these steps? To start, ensure you have react-router-dom
version 6.4
.
In this version, each route features a property named loader
. This property allows us to attach our fetch function specific to that route.
example:
<Route
path="/"
loader={async ({ request }) => {
// loaders can be async functions
const res = await fetch("/api/user.json", {
signal: request.signal,
});
const user = await res.json();
return user;
}}
element={<Root />}
>
<Route
path=":teamId"
// loaders understand Fetch Responses and will automatically
// unwrap the res.json(), so you can simply return a fetch
loader={({ params }) => {
return fetch(`/api/teams/${params.teamId}`);
}}
element={<Team />}
>
<Route
path=":gameId"
loader={({ params }) => {
// of course you can use any data store
return fakeSdk.getTeam(params.gameId);
}}
element={<Game />}
/>
</Route>
Now, let's address the process of fetching data for use within a component. The router team has introduced a hook known as useLoaderData
. As shown in the example below, you can utilize this hook to fetch data specific to the path.
function Root() {
const user = useLoaderData();
// data from <Route path="/">
}
function Team() {
const team = useLoaderData();
// data from <Route path=":teamId">
}
function Game() {
const game = useLoaderData();
// data from <Route path=":gameId">
}
Picture this: you're preparing to render a page featuring comments, history, and issue information. The key objective is to guarantee that the issue-related data is present when the page is displayed, eliminating the need for loading spinners or placeholders. To achieve this, we should await the data fetch. However, for comments and history, it's preferred to wait until the data is loaded before showing a spinner (or placeholder). The code example below provides a example that how it's going to be done:
<Route
path="issue/:issueId"
element={<Issue />}
loader={async ({ params }) => {
// these are promises, but *not* awaited
const comments = fake.getIssueComments(params.issueId);
const history = fake.getIssueHistory(params.issueId);
// the issue, however, *is* awaited
const issue = await fake.getIssue(params.issueId);
// defer enables suspense for the un-awaited promises
return defer({ issue, comments, history });
}}
/>;
function Issue() {
const { issue, history, comments } = useLoaderData();
return (
<div>
<IssueDescription issue={issue} />
{/* Suspense provides the placeholder fallback */}
<Suspense fallback={<IssueHistorySkeleton />}>
{/* Await manages the deferred data (promise) */}
<Await resolve={history}>
{/* this calls back when the data is resolved */}
{(resolvedHistory) => (
<IssueHistory history={resolvedHistory} />
)}
</Await>
</Suspense>
<Suspense fallback={<IssueCommentsSkeleton />}>
<Await resolve={comments}>
{/* ... or you can use hooks to access the data */}
<IssueComments />
</Await>
</Suspense>
</div>
);
}
function IssueComments() {
const comments = useAsyncValue();
return <div>{/* ... */}</div>;
}
Fetching data on the page is the initial and vital step towards moving to server-side rendering. Even if you're not sure about switching to server rendering, this step is essential. Remember, React is for UI, not data fetching in components.
Next comes handling data mutations, a task separate from components. React is mainly for UI, not data.
Lastly, the examples in this article are from the react-router-dom documentation. For more details, explore the documentation. The main point here: React isn't meant for data handling.
in case you had question or you wanted to discuss furtur feel free to text me on X(Twitter): https://twitter.com/AmirArsalan_Az
Top comments (1)
Amazing Work