已发布·持续改进
React 指南 · 4/6
本章目前仅提供英文版。
This chapter covers lifting state up, context, reducers, data fetching and forms.
When two components need the same data, move the state to their closest common parent and pass the value and a setter down as props, so the data has a single source of truth.
import { useState } from "react";
function TemperatureInput({ value, onChange }: { value: number; onChange: (v: number) => void }) {
return <input type="number" value={value} onChange={(e) => onChange(Number(e.target.value))} />;
}
export function Thermostat() {
const [celsius, setCelsius] = useState(20);
return (
<div>
<TemperatureInput value={celsius} onChange={setCelsius} />
<p>{(celsius * 9) / 5 + 32} °F</p>
</div>
);
}Passing values like the theme through many levels of props (prop drilling) gets tedious. Create a context with createContext, provide a value near the top, and any component below can read it with useContext. Current React renders the context itself as the provider; older code uses <ThemeContext.Provider>.
import { createContext, useContext } from "react";
type Theme = "light" | "dark";
const ThemeContext = createContext<Theme>("light");
export function App({ theme }: { theme: Theme }) {
return (
<ThemeContext value={theme}>
<Toolbar />
</ThemeContext>
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return <div className={`toolbar toolbar-${theme}`}>Theme: {theme}</div>;
}Components reading a context re-render when its value changes, so split contexts by purpose.
useReducer gathers many kinds of updates into one function: components dispatch actions describing what happened, and the reducer computes the next state. A discriminated union type for actions catches unhandled cases.
import { useReducer } from "react";
type Todo = { id: number; text: string; done: boolean };
type Action =
| { type: "added"; text: string }
| { type: "toggled"; id: number };
function todosReducer(todos: Todo[], action: Action): Todo[] {
switch (action.type) {
case "added":
return [...todos, { id: Date.now(), text: action.text, done: false }];
case "toggled":
return todos.map((t) => (t.id === action.id ? { ...t, done: !t.done } : t));
}
}
export function useTodos() {
const [todos, dispatch] = useReducer(todosReducer, []);
return { todos, add: (text: string) => dispatch({ type: "added", text }), dispatch };
}A reducer plus context covers global state in small apps; larger apps may benefit from Zustand or Redux Toolkit.
You can call fetch inside an effect, but abort the request in the cleanup function to avoid race conditions between responses.
import { useEffect, useState } from "react";
type User = { id: number; name: string };
export function UserName({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then((res) => res.json() as Promise<User>)
.then(setUser)
.catch((err) => {
if (err.name !== "AbortError") console.error(err); // ignore aborts
});
return () => controller.abort();
}, [userId]);
return <span>{user ? user.name : "Loading…"}</span>;
}Caching, retries, loading and error states and background refetching are hard to get right by hand, so real apps often use TanStack Query. Wrap the app in QueryClientProvider in main.tsx, then:
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
type Todo = { id: number; title: string };
export function Todos() {
const queryClient = useQueryClient();
const { data, isPending, error } = useQuery({
queryKey: ["todos"],
queryFn: async (): Promise<Todo[]> => (await fetch("/api/todos")).json(),
});
const addTodo = useMutation({
mutationFn: (title: string) =>
fetch("/api/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title }),
}),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["todos"] }),
});
if (isPending) return <p>Loading…</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<>
<ul>{data.map((t) => <li key={t.id}>{t.title}</li>)}</ul>
<button onClick={() => addTodo.mutate("New task")}>Add</button>
</>
);
}Controlled components bind an input's value to state, which helps when validating on every keystroke. Current React also accepts a function in <form action={...}>, with useActionState reporting the result and pending state.
import { useActionState } from "react";
type State = { error: string | null };
async function subscribe(_prev: State, formData: FormData): Promise<State> {
const email = String(formData.get("email") ?? "");
if (!email.includes("@")) return { error: "Please enter a valid email." };
await fetch("/api/subscribe", { method: "POST", body: formData });
return { error: null };
}
export function SubscribeForm() {
const [state, formAction, isPending] = useActionState(subscribe, { error: null });
return (
<form action={formAction}>
<input name="email" type="email" required />
<button disabled={isPending}>{isPending ? "Sending…" : "Subscribe"}</button>
{state.error && <p role="alert">{state.error}</p>}
</form>
);
}For large forms with complex validation, React Hook Form is a good companion.
useReducer for complex updates.useActionState.
0 条评论
登录 · 登录后即可发表评论。
来发表第一条评论吧。