- 译:101个React技巧#1组件组织
- 译:101个React技巧#2有效的设计模式与技术
- 译:101个React技巧#3Keys&Refs
- 译:101个React技巧#4组织React代码
- 译:101个React技巧#5高效状态管理
- 译:101个React技巧#6React代码优化
- 译:101个React技巧#7React代码调试技巧
- 译:101个React技巧#8测试 React代码
- 译:101个React技巧#9React hook
- 译:101个React技巧#10必知的React库/工具
- 译:101个React技巧#11React与Visual Studio Cod
- 译:101个React技巧#12React 与 TypeScript
- 译:101个React技巧#13其他技巧
28. 不要为可以从其他状态或属性派生的值创建状态
状态越多,麻烦越多。
每一个状态都可能触发重新渲染,并且会让状态重置变得麻烦。
因此,如果一个值可以从状态或属性派生出来,就不要添加新的状态。
❌ 错误做法: filteredPosts 不需要放在状态中。
function App({ posts }) {
const [filters, setFilters] = useState();
const [filteredPosts, setFilteredPosts] = useState([]);
useEffect(() => {
setFilteredPosts(filterPosts(posts, filters));
}, [posts, filters]);
return (
<Dashboard>
<Filters filters={filters} onFiltersChange={setFilters} />
{filteredPosts.length > 0 && <Posts posts={filteredPosts} />}
</Dashboard>
);
}
✅ 正确做法: filteredPosts 从 posts 和 filters 派生而来。
function App({ posts }) {
const [filters, setFilters] = useState({});
const filteredPosts = filterPosts(posts, filters);
return (
<Dashboard>
<Filters filters={filters} onFiltersChange={setFilters} />
{filteredPosts.length > 0 && <Posts posts={filteredPosts} />}
</Dashboard>
);
}
29. 将状态保持在尽可能低的层级,以减少重新渲染
每当组件内部的状态发生变化时,React 会重新渲染该组件及其所有子组件(用 memo 包装的子组件除外)。
即使这些子组件没有使用发生变化的状态,也会发生重新渲染。为了减少重新渲染,应尽可能将状态下移到组件树中。
❌ 错误做法: 当 sortOrder 改变时,LeftSidebar 和 RightSidebar 都会重新渲染。
function App() {
const [sortOrder, setSortOrder] = useState("popular");
return (
<div className="App">
<LeftSidebar />
<Main sortOrder={sortOrder} setSortOrder={setSortOrder} />
<RightSidebar />
</div>
);
}
function Main({ sortOrder, setSortOrder }) {
return (
<div>
<Button
onClick={() => setSortOrder("popular")}
active={sortOrder === "popular"}
>
热门
</Button>
<Button
onClick={() => setSortOrder("latest")}
active={sortOrder === "latest"}
>
最新
</Button>
</div>
);
}
✅ 正确做法: sortOrder 的改变只会影响 Main。
function App() {
return (
<div className="App">
<LeftSidebar />
<Main />
<RightSidebar />
</div>
);
}
function Main() {
const [sortOrder, setSortOrder] = useState("popular");
return (
<div>
<Button
onClick={() => setSortOrder("popular")}
active={sortOrder === "popular"}
>
热门
</Button>
<Button
onClick={() => setSortOrder("latest")}
active={sortOrder === "latest"}
>
最新
</Button>
</div>
);
}
30. 明确初始状态和当前状态的区别
❌ 错误做法: 不清楚 sortOrder 只是初始值,这可能会导致状态管理方面的混淆或错误。
function Main({ sortOrder }) {
const [internalSortOrder, setInternalSortOrder] = useState(sortOrder);
return (
<div>
<Button
onClick={() => setInternalSortOrder("popular")}
active={internalSortOrder === "popular"}
>
热门
</Button>
<Button
onClick={() => setInternalSortOrder("latest")}
active={internalSortOrder === "latest"}
>
最新
</Button>
</div>
);
}
✅ 正确做法: 命名清晰地表明了什么是初始状态,什么是当前状态。
function Main({ initialSortOrder }) {
const [sortOrder, setSortOrder] = useState(initialSortOrder);
return (
<div>
<Button
onClick={() => setSortOrder("popular")}
active={sortOrder === "popular"}
>
热门
</Button>
<Button
onClick={() => setSortOrder("latest")}
active={sortOrder === "latest"}
>
最新
</Button>
</div>
);
}
31. 基于前一个状态更新状态,特别是在使用 useCallback 进行记忆化时
React 允许你将一个更新函数传递给 useState 中的 set 函数。
这个更新函数使用当前状态来计算下一个状态。
每当我需要基于前一个状态更新状态时,特别是在使用 useCallback 包装的函数内部,我都会使用这种方法。事实上,这种方法可以避免将状态作为钩子的依赖项之一。
❌ 错误做法: handleAddTodo 和 handleRemoveTodo 会在 todos 改变时发生变化。
function App() {
const [todos, setToDos] = useState([]);
const handleAddTodo = useCallback(
(todo) => {
setToDos([...todos, todo]);
},
[todos]
);
const handleRemoveTodo = useCallback(
(id) => {
setToDos(todos.filter((todo) => todo.id !== id));
},
[todos]
);
return (
<div className="App">
<TodoInput onAddTodo={handleAddTodo} />
<TodoList todos={todos} onRemoveTodo={handleRemoveTodo} />
</div>
);
}
✅ 正确做法: 即使 todos 改变,handleAddTodo 和 handleRemoveTodo 也保持不变。
function App() {
const [todos, setToDos] = useState([]);
const handleAddTodo = useCallback((todo) => {
setToDos((prevTodos) => [...prevTodos, todo]);
}, []);
const handleRemoveTodo = useCallback((id) => {
setToDos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
}, []);
return (
<div className="App">
<TodoInput onAddTodo={handleAddTodo} />
<TodoList todos={todos} onRemoveTodo={handleRemoveTodo} />
</div>
);
}
32. 在 useState 中使用函数进行惰性初始化并提高性能,因为它们只被调用一次
在 useState 中使用函数可以确保初始状态只计算一次。
这可以提高性能,特别是当初始状态是从一个 “昂贵” 的操作(如从本地存储中读取)派生而来时。
❌ 错误做法: 每次组件渲染时,我们都会从本地存储中读取主题。
const THEME_LOCAL_STORAGE_KEY = "101-react-tips-theme";
function PageWrapper({ children }) {
const [theme, setTheme] = useState(
localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark"
);
const handleThemeChange = (theme) => {
setTheme(theme);
localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
};
return (
<div
className="page-wrapper"
style={{ background: theme === "dark" ? "black" : "white" }}
>
<div className="header">
<button onClick={() => handleThemeChange("dark")}>深色</button>
<button onClick={() => handleThemeChange("light")}>浅色</button>
</div>
<div>{children}</div>
</div>
);
}
✅ 正确做法: 我们只在组件挂载时从本地存储中读取数据。
function PageWrapper({ children }) {
const [theme, setTheme] = useState(
() => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark"
);
const handleThemeChange = (theme) => {
setTheme(theme);
localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
};
return (
<div
className="page-wrapper"
style={{ background: theme === "dark" ? "black" : "white" }}
>
<div className="header">
<button onClick={() => handleThemeChange("dark")}>深色</button>
<button onClick={() => handleThemeChange("light")}>浅色</button>
</div>
<div>{children}</div>
</div>
);
}
33. 使用 React 上下文来处理广泛需要的静态状态,以避免属性穿透
每当我有一些数据满足以下条件时,我就会使用 React 上下文:
- 在多个地方都需要(例如主题、当前用户等)
- 大部分是静态的或只读的(即用户不经常更改这些数据)
这种方法有助于避免属性穿透(即通过组件层次结构的多个层级传递数据或状态)。
请查看下面沙箱中的示例 👇。
34. React Context:将上下文拆分为频繁变化的部分和不常变化的部分,以提高应用性能
React 上下文的一个挑战是,每当上下文数据发生变化时,所有消费该上下文的组件都会重新渲染,即使它们没有使用发生变化的那部分上下文 🤦♀️。
解决方案是什么? 使用单独的上下文。
在下面的示例中,我们创建了两个上下文:一个用于操作(这些操作是常量),另一个用于状态(状态可能会发生变化)。
35. React Context:当值的计算不简单时,引入一个 Provider 组件
❌ 错误做法: App 内部有太多逻辑来管理主题。
const THEME_LOCAL_STORAGE_KEY = "101-react-tips-theme";
const DEFAULT_THEME = "light";
const ThemeContext = createContext({
theme: DEFAULT_THEME,
setTheme: () => null,
});
function App() {
const [theme, setTheme] = useState(
() => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || DEFAULT_THEME
);
useEffect(() => {
if (theme !== "system") {
updateRootElementTheme(theme);
return;
}
// 我们需要根据系统主题获取要应用的类
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
updateRootElementTheme(systemTheme);
// 然后监听系统主题的变化,并相应地更新根元素
const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
const listener = (event) => {
updateRootElementTheme(event.matches ? "dark" : "light");
};
darkThemeMq.addEventListener("change", listener);
return () => darkThemeMq.removeEventListener("change", listener);
}, [theme]);
const themeContextValue = {
theme,
setTheme: (theme) => {
localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
setTheme(theme);
},
};
const [selectedPostId, setSelectedPostId] = useState(undefined);
const onPostSelect = (postId) => {
// TODO: 一些日志记录
setSelectedPostId(postId);
};
const posts = useSWR("/api/posts", fetcher);
return (
<div className="App">
<ThemeContext.Provider value={themeContextValue}>
<Dashboard
posts={posts}
onPostSelect={onPostSelect}
selectedPostId={selectedPostId}
/>
</ThemeContext.Provider>
</div>
);
}
✅ 正确做法: 主题逻辑封装在 ThemeProvider 中。
function App() {
const [selectedPostId, setSelectedPostId] = useState(undefined);
const onPostSelect = (postId) => {
// TODO: 一些日志记录
setSelectedPostId(postId);
};
const posts = useSWR("/api/posts", fetcher);
return (
<div className="App">
<ThemeProvider>
<Dashboard
posts={posts}
onPostSelect={onPostSelect}
selectedPostId={selectedPostId}
/>
</ThemeProvider>
</div>
);
}
function ThemeProvider({ children }) {
const [theme, setTheme] = useState(
() => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || DEFAULT_THEME
);
useEffect(() => {
if (theme !== "system") {
updateRootElementTheme(theme);
return;
}
// 我们需要根据系统主题获取要应用的类
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
updateRootElementTheme(systemTheme);
// 然后监听系统主题的变化,并相应地更新根元素
const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
const listener = (event) => {
updateRootElementTheme(event.matches ? "dark" : "light");
};
darkThemeMq.addEventListener("change", listener);
return () => darkThemeMq.removeEventListener("change", listener);
}, [theme]);
const themeContextValue = {
theme,
setTheme: (theme) => {
localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
setTheme(theme);
},
};
return (
<div className="App">
<ThemeContext.Provider value={themeContextValue}>
{children}
</ThemeContext.Provider>
</div>
);
}
36. 考虑使用 useReducer 钩子作为轻量级的状态管理解决方案
每当我的状态中有太多值,或者状态很复杂,又不想依赖外部库时,我就会使用 useReducer。
当它与上下文结合使用以满足更广泛的状态管理需求时,尤其有效。
示例: 请参阅 #提示 34。
37. 使用 useImmer 或 useImmerReducer 简化状态更新
使用 useState 和 useReducer 等钩子时,状态必须是不可变的(即所有更改都需要创建一个新状态,而不是修改当前状态)。
这通常很难实现。
这就是 useImmer 和 useImmerReducer 提供更简单替代方案的原因。它们允许你编写 “可变” 的代码,这些代码会自动转换为不可变的更新。
❌ 繁琐的做法: 我们必须仔细确保创建一个新的状态对象。
export function App() {
const [{ email, password }, setState] = useState({
email: "",
password: "",
});
const onEmailChange = (event) => {
setState((prevState) => ({ ...prevState, email: event.target.value }));
};
const onPasswordChange = (event) => {
setState((prevState) => ({ ...prevState, password: event.target.value }));
};
return (
<div className="App">
<h1>欢迎</h1>
<p>
邮箱: <input type="email" value={email} onChange={onEmailChange} />
</p>
<p>
密码:
<input type="password" value={password} onChange={onPasswordChange} />
</p>
</div>
);
}
✅ 更简单的做法: 我们可以直接修改 draftState。
import { useImmer } from "use-immer";
export function App() {
const [{ email, password }, setState] = useImmer({
email: "",
password: "",
});
const onEmailChange = (event) => {
setState((draftState) => {
draftState.email = event.target.value;
});
};
const onPasswordChange = (event) => {
setState((draftState) => {
draftState.password = event.target.value;
});
};
/// 其余逻辑
}
38. 对于跨多个组件访问的复杂客户端状态,使用 Redux(或其他状态管理解决方案)
每当满足以下条件时,我就会使用 Redux:
- 我有一个复杂的前端应用,有很多共享的客户端状态(例如仪表盘应用)
- 我希望用户能够回退并撤销更改
- 我不希望我的组件像使用 React 上下文时那样不必要地重新渲染
- 我有太多的上下文开始变得难以控制
为了获得更流畅的体验,我建议使用 redux-tooltkit。
39. Redux:使用 Redux DevTools 调试你的状态
Redux DevTools 浏览器扩展 是调试 Redux 项目的有用工具。
它允许你实时可视化状态和操作,在刷新页面时保持状态持久化,等等。
要了解其用法,请观看这个很棒的 YouTube 视频。