🤔 Do we need state management in React❓

Alon Valadji
Israeli Tech Radar
Published in
5 min readMar 5, 2024

--

“Life is simpler when UI components are unaware of the network, business logic, or app state. Given same props, always render same data.” — Eric Eliott

What’s the best way to manage our state in React?

A local state controls the interaction of a single component, while a global state controls the entire application logic.

As opposed to adding modal dialogs, autocomplete for searches, and validating forms, our backend already has the whole state.

We always fetch our backend state and add it to our application's global state. In addition to causing a delay, we also duplicate all our logic in the client.

What could we do better?

Making use of Web Primitives

Server Side Rendering (SSR) is a great option for rendering our page with the data, and Forms can be used to change data at the server and re-render it, so we can remove all local states.

Until recently, this was a good solution, but we still run the app on the client for hydration, create a lot of client code to make our forms and server requests look like a single-page app, and deal with loading state.

Data encapsulation (Server Components)

Wouldn’t it be nice if we could render our components without hydration?

Introducing Server Components!

With Next.js 13, we can because they’ve implemented the Server Components RFC (https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md).

This lets us fetch data at the backend and ship a React component without hydration or JavaScript.

export async function Posts() {
const posts = await fetch(`${process.env.API_URL}/posts`).then((res) =>
res.json()
);

return (
<ul className="flex flex-col gap-4">
{posts.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

That’ll bring the posts data and render the component on the client. This will be optimized and rendered as static content by Next.js.

What can we do to stop Next.js from statically rendering our server component?

We can use noStore in our component:

import { unstable_noStore as noStore } from 'next/cache';

export async function Posts() {
noStore();

const posts = await fetch(`${process.env.API_URL}/posts`).then((res) =>
res.json()
);

return (
<ul className="flex flex-col gap-4">
{posts.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

Data slow? Streaming server components

When the server returns data slowly, what do we do? Do we need to handle loading states at the client again?

Luckily, Next.js allows us to stream this to the client with a fallback component just by wrapping our component with Suspense or by adding loading.tsx file to the page directory.

import { Posts } from '@blog/components/server';
import { Suspense } from 'react';

export default async function Index() {
return (
<Suspense fallback={'loading posts...'}>
<Posts />
</Suspense>
);
}

Wow, that’s awesome!

Data mutation (Server Actions)

Want to mutate the data on the server and refresh the current view?

Nothing to worry about! We can use Server Actions.

Let’s create our AddPost component:

import { revalidatePath } from 'next/cache';

export async function AddPost({ path }: { path: string }) {
const onSubmit = async (data: FormData) => {
'use server';

const title = data.get('title');
const body = data.get('body');

await fetch(`${process.env.API_URL}/posts`, {
method: 'POST',
body: JSON.stringify({ title, body }),
});

revalidatePath(path);
};

return (
<form action={onSubmit} className="flex flex-col gap-5">
<h2 className="font-bold text-xl">Add New Post</h2>
<input
name="title"
className="border border-gray-800 rounded px-4 py-2"
/>
<textarea
name="body"
className="border border-gray-800 rounded px-4 py-2 min-h-[100px]"
/>

<button
type="submit"
className="self-start bg-gray-800 px-4 py-2 rounded text-white hover:bg-gray-600"
>
Add Post
</button>
</form>
);
}

The secret sauce is the onSubmit function with the use server annotation on top. That notifies Next.js to run the function code at the server, passing the form data from the client.

const onSubmit = async (data: FormData) => {
'use server';

const title = data.get('title');
const body = data.get('body');

await fetch(`${process.env.API_URL}/posts`, {
method: 'POST',
body: JSON.stringify({ title, body }),
});

revalidatePath(path);
};

We need to revalidatePath so Next.js knows to refresh the current page with all server components’ new data.

Form transition state

It’s important to show our users that the form is still submitting. The submit button needs to be extracted to a client component and then we can use useFormStatus hook.

Here’s our final AddPost server component:

'use client';

import { ReactNode } from 'react';
import { useFormStatus } from 'react-dom';

export function FormSubmit({ children }: { children: ReactNode }) {
const { pending } = useFormStatus();

return (
<button
type="submit"
className="self-start bg-gray-800 px-4 py-2 rounded text-white hover:bg-gray-600 disabled:bg-gray-400 disabled:cursor-not-allowed disabled:text-gray-600"
disabled={pending}
>
{children}
</button>
);
}

And our final AddPost server component looks like this:

import { revalidatePath } from 'next/cache';
import { FormSubmit } from './form-submit';

export async function AddPost({ path }: { path: string }) {
const onSubmit = async (data: FormData) => {
'use server';

const title = data.get('title');
const body = data.get('body');

await fetch(`${process.env.API_URL}/posts`, {
method: 'POST',
body: JSON.stringify({ title, body }),
});

revalidatePath(path);
};

return (
<form action={onSubmit} className="flex flex-col gap-5">
<h2 className="font-bold text-xl">Add New Post</h2>
<input
name="title"
className="border border-gray-800 rounded px-4 py-2"
/>
<textarea
name="body"
className="border border-gray-800 rounded px-4 py-2 min-h-[100px]"
/>

<FormSubmit>Add Post</FormSubmit>
</form>
);
}

Lifting Up The State

Think about setting up a list of blog posts and filtering your view of posts. We can use useState to manage the local state or we can lift it up to the url, not just the parent component.

The URL Search Params (Query String) lets us store our current state for filtering, pagination, and querying (Search).

Let's create our Filter component:

'use client';

import { useRouter } from 'next/navigation';

export function FilterPosts({
users = [],
userId = '',
}: {
users: Array<{ id: string; name: string }>;
userId: string;
}) {
const router = useRouter();

return (
<label className="flex gap-4 items-center">
Filter by user:
<select
className="border border-gray-800 rounded px-4 py-2"
onChange={(e) =>
router.push(e.target.value ? `/?userId=${e.target.value}` : `/`)
}
value={userId}
>
<option value="">All</option>
{users.map((user: { id: string; name: string }) => (
<option key={user.id} value={user.id}>
{user.name}
</option>
))}
</select>
</label>
);
}

Now our posts page will look like this:

import { Suspense } from 'react';
import { AddPost, Posts } from '@blog/components/server';
import { FilterPosts } from '@blog/components';

export default async function Index({
searchParams,
}: {
searchParams: { userId: string };
}) {
const users = await fetch(`${process.env.API_URL}/users`).then((res) =>
res.json()
);

return (
<>
<FilterPosts users={users} userId={searchParams.userId} />

<Suspense fallback={'loading posts...'}>
<Posts userId={searchParams.userId} />
</Suspense>

<AddPost path={'/posts'} />
</>
);
}

We can also send a URL with search results or filtered posts when we want to persist our current state across users.

Conclusion

Today’s technology means we can reduce the need for state management in React apps, without duplicating the server state (data) and BL in our clients and keep the SPA experience.

This approach boosts our application performance and helps with SEO and other core functions without adding any extra code.

I hope you enjoyed this post. Here’s the source code: https://github.com/alonronin/medium-do-we-need-state

I’d love to hear your comments, suggestions, and ideas. 🤗

--

--

Alon Valadji
Israeli Tech Radar

Alon is a leading software architect well-versed in Rozansky and Woods’ architectural methodologies. As a full-cycle JavaScript and web architecture torchbearer