UNPKG

18.6 kB Markdown View Raw
1---
2title: State Management
3---
4
5# State Management
6
7[MODES: framework, data]
8
9<br/>
10<br/>
11
12State management in React typically involves maintaining a synchronized cache of server data on the client side. However, when using React Router as your framework, most of the traditional caching solutions become redundant because of how it inherently handles data synchronization.
13
14## Understanding State Management in React
15
16In a typical React context, when we refer to "state management", we're primarily discussing how we synchronize server state with the client. A more apt term could be "cache management" because the server is the source of truth and the client state is mostly functioning as a cache.
17
18Popular caching solutions in React include:
19
20- **[Redux][redux]:** A predictable state container for JavaScript apps.
21- **[TanStack Query][tanstack_query]:** Hooks for fetching, caching, and updating asynchronous data in React.
22- **[Apollo][apollo]:** A comprehensive state management library for JavaScript that integrates with GraphQL.
23
24In certain scenarios, using these libraries may be warranted. However, with React Router's unique server-focused approach, their utility becomes less prevalent. In fact, most React Router applications forgo them entirely.
25
26## How React Router Simplifies State
27
28React Router seamlessly bridges the gap between the backend and frontend via mechanisms like loaders, actions, and forms with automatic synchronization through revalidation. This offers developers the ability to directly use server state within components without managing a cache, the network communication, or data revalidation, making most client-side caching redundant.
29
30Here's why using typical React state patterns might be an anti-pattern in React Router:
31
321. **Network-related State:** If your React state is managing anything related to the network—such as data from loaders, pending form submissions, or navigational states—it's likely that you're managing state that React Router already manages:
33 - **[`useNavigation`][use_navigation]**: This hook gives you access to `navigation.state`, `navigation.formData`, `navigation.location`, etc.
34 - **[`useFetcher`][use_fetcher]**: This facilitates interaction with `fetcher.state`, `fetcher.formData`, `fetcher.data` etc.
35 - **[`loaderData`][loader_data]**: Access the data for a route.
36 - **[`actionData`][action_data]**: Access the data from the latest action.
37
382. **Storing Data in React Router:** A lot of data that developers might be tempted to store in React state has a more natural home in React Router, such as:
39 - **URL Search Params:** Parameters within the URL that hold state.
40 - **[Cookies][cookies]:** Small pieces of data stored on the user's device.
41 - **[Server Sessions][sessions]:** Server-managed user sessions.
42 - **Server Caches:** Cached data on the server side for quicker retrieval.
43
443. **Performance Considerations:** At times, client state is leveraged to avoid redundant data fetching. With React Router, you can use the [`Cache-Control`][cache_control_header] headers within `loader`s, allowing you to tap into the browser's native cache. However, this approach has its limitations and should be used judiciously. It's usually more beneficial to optimize backend queries or implement a server cache. This is because such changes benefit all users and do away with the need for individual browser caches.
45
46As a developer transitioning to React Router, it's essential to recognize and embrace its inherent efficiencies rather than applying traditional React patterns. React Router offers a streamlined solution to state management leading to less code, fresh data, and no state synchronization bugs.
47
48## Examples
49
50### Network Related State
51
52For examples on using React Router's internal state to manage network related state, refer to [Pending UI][pending_ui].
53
54### URL Search Params
55
56Consider a UI that lets the user customize between list view or detail view. Your instinct might be to reach for React state:
57
58```tsx bad lines=[2,6,9]
59export function List() {
60 const [view, setView] = useState("list");
61 return (
62 <div>
63 <div>
64 <button onClick={() => setView("list")}>
65 View as List
66 </button>
67 <button onClick={() => setView("details")}>
68 View with Details
69 </button>
70 </div>
71 {view === "list" ? <ListView /> : <DetailView />}
72 </div>
73 );
74}
75```
76
77Now consider you want the URL to update when the user changes the view. Note the state synchronization:
78
79```tsx bad lines=[7,16,24]
80import { useNavigate, useSearchParams } from "react-router";
81
82export function List() {
83 const navigate = useNavigate();
84 const [searchParams] = useSearchParams();
85 const [view, setView] = useState(
86 searchParams.get("view") || "list",
87 );
88
89 return (
90 <div>
91 <div>
92 <button
93 onClick={() => {
94 setView("list");
95 navigate(`?view=list`);
96 }}
97 >
98 View as List
99 </button>
100 <button
101 onClick={() => {
102 setView("details");
103 navigate(`?view=details`);
104 }}
105 >
106 View with Details
107 </button>
108 </div>
109 {view === "list" ? <ListView /> : <DetailView />}
110 </div>
111 );
112}
113```
114
115Instead of synchronizing state, you can simply read and set the state in the URL directly with boring old HTML forms:
116
117```tsx good lines=[5,9-16]
118import { Form, useSearchParams } from "react-router";
119
120export function List() {
121 const [searchParams] = useSearchParams();
122 const view = searchParams.get("view") || "list";
123
124 return (
125 <div>
126 <Form>
127 <button name="view" value="list">
128 View as List
129 </button>
130 <button name="view" value="details">
131 View with Details
132 </button>
133 </Form>
134 {view === "list" ? <ListView /> : <DetailView />}
135 </div>
136 );
137}
138```
139
140### Persistent UI State
141
142Consider a UI that toggles a sidebar's visibility. We have three ways to handle the state:
143
1441. React state
1452. Browser local storage
1463. Cookies
147
148In this discussion, we'll break down the trade-offs associated with each method.
149
150#### React State
151
152React state provides a simple solution for temporary state storage.
153
154**Pros**:
155
156- **Simple**: Easy to implement and understand.
157- **Encapsulated**: State is scoped to the component.
158
159**Cons**:
160
161- **Transient**: Doesn't survive page refreshes, returning to the page later, or unmounting and remounting the component.
162
163**Implementation**:
164
165```tsx
166function Sidebar() {
167 const [isOpen, setIsOpen] = useState(false);
168 return (
169 <div>
170 <button onClick={() => setIsOpen((open) => !open)}>
171 {isOpen ? "Close" : "Open"}
172 </button>
173 <aside hidden={!isOpen}>
174 <Outlet />
175 </aside>
176 </div>
177 );
178}
179```
180
181#### Local Storage
182
183To persist state beyond the component lifecycle, browser local storage is a step-up. See our doc on [Client Data][client_data] for more advanced examples.
184
185**Pros**:
186
187- **Persistent**: Maintains state across page refreshes and component mounts/unmounts.
188- **Encapsulated**: State is scoped to the component.
189
190**Cons**:
191
192- **Requires Synchronization**: React components must sync up with local storage to initialize and save the current state.
193- **Server Rendering Limitation**: The [`window`][window_global] and [`localStorage`][local_storage_global] objects are not accessible during server-side rendering, so state must be initialized in the browser with an effect.
194- **UI Flickering**: On initial page loads, the state in local storage may not match what was rendered by the server and the UI will flicker when JavaScript loads.
195
196**Implementation**:
197
198```tsx
199function Sidebar() {
200 const [isOpen, setIsOpen] = useState(false);
201
202 // synchronize initially
203 useLayoutEffect(() => {
204 const isOpen = window.localStorage.getItem("sidebar");
205 setIsOpen(isOpen);
206 }, []);
207
208 // synchronize on change
209 useEffect(() => {
210 window.localStorage.setItem("sidebar", isOpen);
211 }, [isOpen]);
212
213 return (
214 <div>
215 <button onClick={() => setIsOpen((open) => !open)}>
216 {isOpen ? "Close" : "Open"}
217 </button>
218 <aside hidden={!isOpen}>
219 <Outlet />
220 </aside>
221 </div>
222 );
223}
224```
225
226In this approach, state must be initialized within an effect. This is crucial to avoid complications during server-side rendering. Directly initializing the React state from `localStorage` will cause errors since `window.localStorage` is unavailable during server rendering.
227
228```tsx bad lines=[4]
229function Sidebar() {
230 const [isOpen, setIsOpen] = useState(
231 // error: window is not defined
232 window.localStorage.getItem("sidebar"),
233 );
234
235 // ...
236}
237```
238
239By initializing the state within an effect, there's potential for a mismatch between the server-rendered state and the state stored in local storage. This discrepancy will lead to brief UI flickering shortly after the page renders and should be avoided.
240
241#### Cookies
242
243Cookies offer a comprehensive solution for this use case. However, this method introduces added preliminary setup before making the state accessible within the component.
244
245**Pros**:
246
247- **Server Rendering**: State is available on the server for rendering and even for server actions.
248- **Single Source of Truth**: Eliminates state synchronization hassles.
249- **Persistence**: Maintains state across page loads and component mounts/unmounts. State can even persist across devices if you switch to a database-backed session.
250- **Progressive Enhancement**: Functions even before JavaScript loads.
251
252**Cons**:
253
254- **Boilerplate**: Requires more code because of the network.
255- **Exposed**: The state is not encapsulated to a single component, other parts of the app must be aware of the cookie.
256
257**Implementation**:
258
259First we'll need to create a cookie object:
260
261```tsx
262import { createCookie } from "react-router";
263export const prefs = createCookie("prefs");
264```
265
266Next we set up the server action and loader to read and write the cookie:
267
268```tsx filename=app/routes/sidebar.tsx
269import { data, Outlet } from "react-router";
270import type { Route } from "./+types/sidebar";
271
272import { prefs } from "./prefs-cookie";
273
274// read the state from the cookie
275export async function loader({
276 request,
277}: Route.LoaderArgs) {
278 const cookieHeader = request.headers.get("Cookie");
279 const cookie = (await prefs.parse(cookieHeader)) || {};
280 return data({ sidebarIsOpen: cookie.sidebarIsOpen });
281}
282
283// write the state to the cookie
284export async function action({
285 request,
286}: Route.ActionArgs) {
287 const cookieHeader = request.headers.get("Cookie");
288 const cookie = (await prefs.parse(cookieHeader)) || {};
289 const formData = await request.formData();
290
291 const isOpen = formData.get("sidebar") === "open";
292 cookie.sidebarIsOpen = isOpen;
293
294 return data(isOpen, {
295 headers: {
296 "Set-Cookie": await prefs.serialize(cookie),
297 },
298 });
299}
300```
301
302After the server code is set up, we can use the cookie state in our UI:
303
304```tsx
305function Sidebar({ loaderData }: Route.ComponentProps) {
306 const fetcher = useFetcher();
307 let { sidebarIsOpen } = loaderData;
308
309 // use optimistic UI to immediately change the UI state
310 if (fetcher.formData?.has("sidebar")) {
311 sidebarIsOpen =
312 fetcher.formData.get("sidebar") === "open";
313 }
314
315 return (
316 <div>
317 <fetcher.Form method="post">
318 <button
319 name="sidebar"
320 value={sidebarIsOpen ? "closed" : "open"}
321 >
322 {sidebarIsOpen ? "Close" : "Open"}
323 </button>
324 </fetcher.Form>
325 <aside hidden={!sidebarIsOpen}>
326 <Outlet />
327 </aside>
328 </div>
329 );
330}
331```
332
333While this is certainly more code that touches more of the application to account for the network requests and responses, the UX is greatly improved. Additionally, state comes from a single source of truth without any state synchronization required.
334
335In summary, each of the discussed methods offers a unique set of benefits and challenges:
336
337- **React state**: Offers simple but transient state management.
338- **Local Storage**: Provides persistence but with synchronization requirements and UI flickering.
339- **Cookies**: Delivers robust, persistent state management at the cost of added boilerplate.
340
341None of these are wrong, but if you want to persist the state across visits, cookies offer the best user experience.
342
343### Form Validation and Action Data
344
345Client-side validation can augment the user experience, but similar enhancements can be achieved by leaning more towards server-side processing and letting it handle the complexities.
346
347The following example illustrates the inherent complexities of managing network state, coordinating state from the server, and implementing validation redundantly on both the client and server sides. It's just for illustration, so forgive any obvious bugs or problems you find.
348
349```tsx bad lines=[2,11,27,38,63]
350export function Signup() {
351 // A multitude of React State declarations
352 const [isSubmitting, setIsSubmitting] = useState(false);
353
354 const [userName, setUserName] = useState("");
355 const [userNameError, setUserNameError] = useState(null);
356
357 const [password, setPassword] = useState(null);
358 const [passwordError, setPasswordError] = useState("");
359
360 // Replicating server-side logic in the client
361 function validateForm() {
362 setUserNameError(null);
363 setPasswordError(null);
364 const errors = validateSignupForm(userName, password);
365 if (errors) {
366 if (errors.userName) {
367 setUserNameError(errors.userName);
368 }
369 if (errors.password) {
370 setPasswordError(errors.password);
371 }
372 }
373 return Boolean(errors);
374 }
375
376 // Manual network interaction handling
377 async function handleSubmit() {
378 if (validateForm()) {
379 setSubmitting(true);
380 const res = await postJSON("/api/signup", {
381 userName,
382 password,
383 });
384 const json = await res.json();
385 setIsSubmitting(false);
386
387 // Server state synchronization to the client
388 if (json.errors) {
389 if (json.errors.userName) {
390 setUserNameError(json.errors.userName);
391 }
392 if (json.errors.password) {
393 setPasswordError(json.errors.password);
394 }
395 }
396 }
397 }
398
399 return (
400 <form
401 onSubmit={(event) => {
402 event.preventDefault();
403 handleSubmit();
404 }}
405 >
406 <p>
407 <input
408 type="text"
409 name="username"
410 value={userName}
411 onChange={() => {
412 // Synchronizing form state for the fetch
413 setUserName(event.target.value);
414 }}
415 />
416 {userNameError ? <i>{userNameError}</i> : null}
417 </p>
418
419 <p>
420 <input
421 type="password"
422 name="password"
423 onChange={(event) => {
424 // Synchronizing form state for the fetch
425 setPassword(event.target.value);
426 }}
427 />
428 {passwordError ? <i>{passwordError}</i> : null}
429 </p>
430
431 <button disabled={isSubmitting} type="submit">
432 Sign Up
433 </button>
434
435 {isSubmitting ? <BusyIndicator /> : null}
436 </form>
437 );
438}
439```
440
441The backend endpoint, `/api/signup`, also performs validation and sends error feedback. Note that some essential validation, like detecting duplicate usernames, can only be done server-side using information the client doesn't have access to.
442
443```tsx bad
444export async function signupHandler(request: Request) {
445 const errors = await validateSignupRequest(request);
446 if (errors) {
447 return { ok: false, errors: errors };
448 }
449 await signupUser(request);
450 return { ok: true, errors: null };
451}
452```
453
454Now, let's contrast this with a React Router-based implementation. The action remains consistent, but the component is vastly simplified due to the direct utilization of server state via `actionData`, and leveraging the network state that React Router inherently manages.
455
456```tsx filename=app/routes/signup.tsx good lines=[20-22]
457import { useNavigation } from "react-router";
458import type { Route } from "./+types/signup";
459
460export async function action({
461 request,
462}: ActionFunctionArgs) {
463 const errors = await validateSignupRequest(request);
464 if (errors) {
465 return { ok: false, errors: errors };
466 }
467 await signupUser(request);
468 return { ok: true, errors: null };
469}
470
471export function Signup({
472 actionData,
473}: Route.ComponentProps) {
474 const navigation = useNavigation();
475
476 const userNameError = actionData?.errors?.userName;
477 const passwordError = actionData?.errors?.password;
478 const isSubmitting = navigation.formAction === "/signup";
479
480 return (
481 <Form method="post">
482 <p>
483 <input type="text" name="username" />
484 {userNameError ? <i>{userNameError}</i> : null}
485 </p>
486
487 <p>
488 <input type="password" name="password" />
489 {passwordError ? <i>{passwordError}</i> : null}
490 </p>
491
492 <button disabled={isSubmitting} type="submit">
493 Sign Up
494 </button>
495
496 {isSubmitting ? <BusyIndicator /> : null}
497 </Form>
498 );
499}
500```
501
502The extensive state management from our previous example is distilled into just three code lines. We eliminate the necessity for React state, change event listeners, submit handlers, and state management libraries for such network interactions.
503
504Direct access to the server state is made possible through `actionData`, and network state through `useNavigation` (or `useFetcher`).
505
506As bonus party trick, the form is functional even before JavaScript loads (see [Progressive Enhancement][progressive_enhancement]). Instead of React Router managing the network operations, the default browser behaviors step in.
507
508If you ever find yourself entangled in managing and synchronizing state for network operations, React Router likely offers a more elegant solution.
509
510[redux]: https://redux.js.org/
511[tanstack_query]: https://tanstack.com/query/latest
512[apollo]: https://www.apollographql.com/
513[use_navigation]: https://api.reactrouter.com/v7/functions/react-router.useNavigation
514[use_fetcher]: https://api.reactrouter.com/v7/functions/react-router.useFetcher
515[loader_data]: ../start/framework/data-loading
516[action_data]: ../start/framework/actions
517[cookies]: ./sessions-and-cookies#cookies
518[sessions]: ./sessions-and-cookies#sessions
519[cache_control_header]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
520[pending_ui]: ../start/framework/pending-ui
521[client_data]: ../how-to/client-data
522[window_global]: https://developer.mozilla.org/en-US/docs/Web/API/Window/window
523[local_storage_global]: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
524[progressive_enhancement]: ./progressive-enhancement
525
\No newline at end of file