UNPKG

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