React组件、状态管理、代码优化的技巧
作者:夕水
文章总结了React组件设计、状态管理、代码组织和优化的技巧,它涵盖了使用Fragment、props解构、defaultProps、key和ref的使用、渲染性能优化等方面。
一. 组件相关
1. 使用自闭合组件
// 不好的写法 <Component></Component> // 推荐写法 <Component />
2. 推荐使用Fragment组件而不是 DOM 元素来分组元素
在 React 中,每个组件必须返回单个元素。不要将多个元素包装在 <div> 或 <span> 中,而是使用 <Fragment> 来保持 DOM 整洁。
不好的写法:使用 div 会使 DOM 变得杂乱,并且可能需要更多 CSS 代码。
import Header from "./header"; import Content from "./content"; import Footer from "./footer"; const Test = () => { return ( <div> <Header /> <Content /> <Footer /> </div> ); };
推荐写法: <Fragment> 包装元素而不影响 DOM 结构。
import Header from "./header"; import Content from "./content"; import Footer from "./footer"; const Test = () => { return ( // 如果元素不需要添加属性,则可以使用简写形式<></> <Fragment> <Header /> <Content /> <Footer /> </Fragment> ); };
3. 使用 React fragment 简写 <></>(除非你需要设置一个 key 属性)
不好写法:下面的代码有点冗余。
const Test = () => { return ( <Fragment> <Header /> <Content /> <Footer /> </Fragment> ); };
推荐写法:
const Test = () => { return ( <> <Header /> <Content /> <Footer /> </> ); };
除非你需要一个 key 属性。
const Tools = ({ tools }) => { return ( <Container> { tools?.map((item, index) => { <Fragment key={`${item.id}-${index}`}> <span>{ item.id }</span> <span>{ item.name }</span> <Fragment> }) } </Container> ) }
4. 优先分散使用 props,而不是单独访问每个 props
不好的写法: 下面的代码更难阅读(特别是在项目较大时)。
const TodoLists = (props) => ( <div className="todo-list"> {props.todoList?.map((todo, index) => ( <div className="todo-list-item" key={todo.uuid}> <p onClick={() => props.seeDetail?.(todo)}> {todo?.uuid}:{todo.text} </p> <div className="todo-list-item-btn-group"> <button type="button" onClick={() => props.handleEdit?.(todo, index)}> 编辑 </button> <button type="button" onClick={() => props.handleDelete?.(todo, index)} > 删除 </button> </div> </div> ))} </div> ); export default TodoLists;
推荐写法: 下面的代码更加简洁。
const TodoLists = ({ todoList, seeDetail, handleEdit, handleDelete }) => ( <div className="todo-list"> {todoList?.map((todo, index) => ( <div className="todo-list-item" key={todo.uuid}> <p onClick={() => seeDetail?.(todo)}> {todo?.uuid}:{todo.text} </p> <div className="todo-list-item-btn-group"> <button type="button" onClick={() => handleEdit?.(todo, index)}> 编辑 </button> <button type="button" onClick={() => handleDelete?.(todo, index)}> 删除 </button> </div> </div> ))} </div> ); export default TodoLists;
5. 设置 props 的默认值时,在解构时进行
不好的写法: 你可能需要在多个地方定义默认值并引入新变量。
const Text = ({ size, type }) => { const Component = type || "span"; const comSize = size || "mini"; return <Component size={comSize} />; };
推荐写法,直接在对象解构里给出默认值。
const Text = ({ size = "mini", type: Component = "span" }) => { return <Component size={comSize} />; };
6. 传递字符串类型属性时删除花括号。
不好的写法:带花括号的写法
<button type={"button"} className={"btn"}> 按钮 </button>
推荐写法: 不需要花括号
<button type="button" className="btn"> 按钮 </button>
7. 在使用 value && <Component {...props}/> 之前确保 value 值是布尔值,以防止显示意外的值。
不好的写法: 当列表的长度为 0,则有可能显示 0。
const DataList = ({ data }) => { return <Container>{data.length && <List data={data} />}</Container>; };
推荐写法: 当列表没有数据时,则不会渲染任何东西。
const DataList = ({ data }) => { return <Container>{data.length > 0 && <List data={data} />}</Container>; };
8. 使用函数(内联或非内联)避免中间变量污染你的上下文
不好的写法: 变量 totalCount 和 totalPrice 使组件的上下文变得混乱。
const GoodList = ({ goods }) => { if (goods.length === 0) { return <>暂无数据</>; } let totalCount = 0; let totalPrice = 0; goods.forEach((good) => { totalCount += good.count; totalPrice += good.price; }); return ( <> 总数量:{totalCount};总价:{totalPrice} </> ); };
推荐写法: 将变量 totalCount 和 totalPrice 控制在一个函数内。
const GoodList = ({ goods }) => { if (goods.length === 0) { return <>暂无数据</>; } // 使用函数 const { totalCount, totalPrice, } = () => { let totalCount = 0, totalPrice = 0; goods.forEach((good) => { totalCount += good.count; totalPrice += good.price; }); return { totalCount, totalPrice }; }; return ( <> 总数量:{totalCount};总价:{totalPrice} </> ); };
个人更喜欢的写法: 封装成 hooks 来使用。
const useTotalGoods = ({ goods }) => { let totalCount = 0, totalPrice = 0; goods.forEach((good) => { totalCount += good.count; totalPrice += good.price; }); return { totalCount, totalPrice }; }; const GoodList = ({ goods }) => { if (goods.length === 0) { return <>暂无数据</>; } const { totalCount, totalPrice } = useTotalGoods(goods); return ( <> 总数量:{totalCount};总价:{totalPrice} </> ); };
9. 使用柯里化函数重用逻辑(并正确缓存回调函数)
不好的写法: 表单更新字段重复。
const UserLoginForm = () => { const [{ username, password }, setFormUserState] = useState({ username: "", password: "", }); return ( <> <h1>登陆</h1> <form> <div class="form-item"> <label>用户名:</label> <input placeholder="请输入用户名" value={username} onChange={(e) => setFormUserState((state) => ({ ...state, username: e.target.value, })) } /> </div> <div class="form-item"> <label>密码:</label> <input placeholder="请输入密码" value={username} type="password" onChange={(e) => setFormUserState((state) => ({ ...state, password: e.target.value, })) } /> </div> </form> </> ); };
推荐写法: 引入 createFormValueChangeHandler 方法,为每个字段返回正确的处理方法。
笔记: 如果你启用了 ESLint 规则 jsx-no-bind,此技巧尤其有用。你只需将柯里化函数包装在 useCallback 中。
const UserLoginForm = () => { const [{ username, password }, setFormUserState] = useState({ username: "", password: "", }); const createFormValueChangeHandler = (field: string) => { return (e) => { setFormUserState((state) => ({ ...state, [field]: e.target.value, })); }; }; return ( <> <h1>登陆</h1> <form> <div class="form-item"> <label>用户名:</label> <input placeholder="请输入用户名" value={username} onChange={createFormValueChangeHandler("username")} /> </div> <div class="form-item"> <label>密码:</label> <input placeholder="请输入密码" value={username} type="password" onChange={createFormValueChangeHandler("password")} /> </div> </form> </> ); };
10. 将不依赖组件 props/state 的数据移到组件外部,以获得更干净(和更高效)的代码
不好的写法: OPTIONS 和 renderOption 不需要位于组件内部,因为它们不依赖任何 props 或状态。此外,将它们保留在内部意味着每次组件渲染时我们都会获得新的对象引用。如果我们将 renderOption 传递给包裹在 memo 中的子组件,则会破坏缓存功能。
const ToolSelector = () => { const options = [ { label: "html工具", value: "html-tool", }, { label: "css工具", value: "css-tool", }, { label: "js工具", value: "js-tool", }, ]; const renderOption = ({ label, value, }: { label?: string; value?: string; }) => <Option value={value}>{label}</Option>; return ( <Select placeholder="请选择工具"> {options.map((item, index) => ( <Fragment key={`${item.value}-${index}`}>{renderOption(item)}</Fragment> ))} </Select> ); };
推荐写法: 将它们移出组件以保持组件干净和引用稳定。
const options = [ { label: "html工具", value: "html-tool", }, { label: "css工具", value: "css-tool", }, { label: "js工具", value: "js-tool", }, ]; const renderOption = ({ label, value }: { label?: string; value?: string }) => ( <Option value={value}>{label}</Option> ); const ToolSelector = () => { return ( <Select placeholder="请选择工具"> {options.map((item, index) => ( <Fragment key={`${item.value}-${index}`}>{renderOption(item)}</Fragment> ))} </Select> ); };
笔记: 在这个示例中,你可以通过使用选项元素内联来进一步简化。
const options = [ { label: "html工具", value: "html-tool", }, { label: "css工具", value: "css-tool", }, { label: "js工具", value: "js-tool", }, ]; const ToolSelector = () => { return ( <Select placeholder="请选择工具"> {options.map((item, index) => ( <Option value={item.value} key={`${item.value}-${index}`}> {item.label} </Option> ))} </Select> ); };
11. 存储列表组件中选定的对象时,存储对象 ID,而不是整个对象
不好的写法: 如果选择了某个对象但随后它发生了变化(即,我们收到了相同 ID 的全新对象引用),或者该对象不再存在于列表中,则 selectedItem 将保留过时的值或变得不正确。
const List = ({ data }) => { // 引用的是整个选中的是对象 const [selectedItem, setSelectedItem] = useState<Item | undefined>(); return ( <> {selectedItem && <div>{selectedItem.value}</div>} <List data={data} onSelect={setSelectedItem} selectedItem={selectedItem} /> </> ); };
推荐写法: 我们通过 ID(应该是稳定的)存储所选列表对象。这确保即使列表对象从列表中删除或其某个属性发生变化,UI 也应该正确。
const List = ({ data }) => { const [selectedItemId, setSelectedItemId] = useState<string | number>(); // 我们从列表中根据选中id查找出选定的列表对象 const selectedItem = data.find((item) => item.id === selectedItemId); return ( <> {selectedItemId && <div>{selectedItem.value}</div>} <List data={data} onSelect={setSelectedItemId} selectedItemId={selectedItemId} /> </> ); };
12. 如果需要多次用到 prop 里面的值,那就引入一个新的组件
不好的写法: 由于 type === null 的检查使得代码变得混乱。
注意: 由于hooks 规则,我们不能提前返回 null。
const CreatForm = ({ type }) => { const formList = useMemo(() => { if (type === null) { return []; } return getFormList({ type }); }, [type]); const onHandleChange = useCallback( (id) => { if (type === null) { return; } // do something }, [type] ); if (type === null) { return null; } return ( <> {formList.map(({ value, id, ...rest }, index) => ( <item.component value={value} onChange={onHandleChange} key={id} {...rest} /> ))} </> ); };
推荐写法: 我们引入了一个新组件 FormLists,它采用定义的表单项组件并且更加简洁。
const FormList = ({ type }) => { const formList = useMemo(() => getFormList({ type }), [type]); const onHandleChange = useCallback( (id) => { // do something }, [type] ); return ( <> {formList.map(({ value, id, ...rest }, index) => ( <item.component value={value} onChange={onHandleChange} key={id} {...rest} /> ))} </> ); }; const CreateForm = ({ type }) => { if (type === null) { return null; } return <FormList type={type} />; };
13. 将所有状态(state)和上下文(context)分组到组件顶部
当所有状态和上下文都位于顶部时,很容易发现哪些因素会触发组件重新渲染。
不好的写法: 状态和上下文分散,难以跟踪。
const LoginForm = () => { const [username, setUsername] = useState(""); const onHandleChangeUsername = (e) => { setUserName(e.target.value); }; const [password, setPassword] = useState(""); const onHandleChangePassword = (e) => { setPassword(e.target.value); }; const theme = useContext(themeContext); return ( <div class={`login-form login-form-${theme}`}> <h1>login</h1> <form> <div class="login-form-item"> <label>用户名:</label> <input value={username} onChange={onHandleChangeUsername} placeholder="请输入用户名" /> </div> <div class="login-form-item"> <label>密码:</label> <input value={password} onChange={onHandleChangePassword} placeholder="请输入密码" type="password" /> </div> </form> </div> ); };
推荐写法: 所有状态和上下文都集中在顶部,以便于快速定位。
const LoginForm = () => { // context const theme = useContext(themeContext); // state const [password, setPassword] = useState(""); const [username, setUsername] = useState(""); // method const onHandleChangeUsername = (e) => { setUserName(e.target.value); }; const onHandleChangePassword = (e) => { setPassword(e.target.value); }; return ( <div class={`login-form login-form-${theme}`}> <h1>login</h1> <form> <div class="login-form-item"> <label>用户名:</label> <input value={username} onChange={onHandleChangeUsername} placeholder="请输入用户名" /> </div> <div class="login-form-item"> <label>密码:</label> <input value={password} onChange={onHandleChangePassword} placeholder="请输入密码" type="password" /> </div> </form> </div> ); };
二. 有效的设计模式与技巧
14. 利用 children 属性来获得更清晰的代码(以及性能优势)
利用子组件 props 来获得更简洁的代码(和性能优势)。使用子组件 props 有几个好处:
- 好处 1:你可以通过将 props 直接传递给子组件而不是通过父组件路由来避免 prop 混入。
- 好处 2:你的代码更具可扩展性,因为你可以轻松修改子组件而无需更改父组件。
- 好处 3:你可以使用此技巧避免重新渲染组件(参见下面的示例)。
不好的写法: 每当 Timer 渲染时,OtherSlowComponent 都会渲染,每次当前时间更新时都会发生这种情况。
const Container = () => <Timer />; const Timer = () => { const [time, setTime] = useState(0); useEffect(() => { const intervalId = setInterval(() => setTime(new Date()), 1000); return () => { clearInterval(intervalId); }; }, []); return ( <> <h1>当前时间:{dayjs(time).format("YYYY-MM-DD HH:mm:ss")}</h1> <OtherSlowComponent /> </> ); };
推荐写法: Timer 呈现时,OtherSlowComponent 不会呈现。
const Container = () => ( <Timer> <OtherSlowComponent /> </Timer> ); const Timer = ({ children }) => { const [time, setTime] = useState(0); useEffect(() => { const intervalId = setInterval(() => setTime(new Date()), 1000); return () => { clearInterval(intervalId); }; }, []); return ( <> <h1>当前时间:{dayjs(time).formate("YYYY-MM-DD HH:mm:ss")}</h1> {children} </> ); };
15. 使用复合组件构建可组合代码
像搭积木一样使用复合组件,将它们拼凑在一起以创建自定义 UI。这些组件在创建库时效果极佳,可生成富有表现力且高度可扩展的代码。以下是一个以reach.ui为示例的代码:
<Menu> <MenuButton> 操作吧 <span aria-hidden>▾</span> </MenuButton> <MenuList> <MenuItem onSelect={() => alert("下载")}>下载</MenuItem> <MenuItem onSelect={() => alert("复制")}>创建一个复制</MenuItem> <MenuLink as="a" href="https://reach.tech/menu-button/" rel="external nofollow" > 跳转链接 </MenuLink> </MenuList> </Menu>
16. 使用渲染函数或组件函数 props 使你的代码更具可扩展性
假设我们想要显示各种列表,例如消息、个人资料或帖子,并且每个列表都应该可排序。
为了实现这一点,我们引入了一个 List 组件以供重复使用。我们可以通过两种方式解决这个问题:
不好的写法:选项 1。
List 处理每个项目的渲染及其排序方式。这是有问题的,因为它违反了开放封闭原则。每当添加新的项目类型时,此代码都会被修改。
List.tsx:
export interface ListItem { id: string; } // 不好的列表组件写法 // 我们还需要了解这些接口 type PostItem = ListItem & { title: string }; type UserItem = ListItem & { name: string; date: Date }; type ListNewItem = | { type: "post"; value: PostItem } | { type: "user"; value: UserItem }; interface BadListProps<T extends ListNewItem> { type: T["type"]; items: Array<T["value"]>; } const SortList = <T extends ListNewItem>({ type, items }: BadListProps<T>) => { const sortItems = [...items].sort((a, b) => { // 我们还需注意这里的比较逻辑,这里或者直接使用下方导出的比较函数 return 0; }); return ( <> <h2>{type === "post" ? "帖子" : "用户"}</h2> <ul className="sort-list"> {sortItems.map((item, index) => ( <li className="sort-list-item" key={`${item.id}-${index}`}> {(() => { switch (type) { case "post": return (item as PostItem).title; case "user": return ( <> <span>{(item as UserItem).name}</span> <span> - </span> <em> 加入时间: {(item as UserItem).date.toDateString()} </em> </> ); } })()} </li> ))} </ul> </> ); }; export function compareStrings(a: string, b: string): number { return a < b ? -1 : a == b ? 0 : 1; }
推荐写法:选项 2。
List 采用渲染函数或组件函数,仅在需要时调用它们。
List.tsx:
export interface ListItem { id: string; } interface ListProps<T extends ListItem> { items: T[]; // 列表数据 header: React.ComponentType; // 头部组件 itemRender: (item: T) => React.ReactNode; // 列表项 itemCompare: (a: T, b: T) => number; // 列表项自定义排序函数 } const SortList = <T extends ListItem>({ items, header: Header, itemRender, itemCompare, }: ListProps<T>) => { const sortedItems = [...items].sort(itemCompare); return ( <> <Header /> <ul className="sort-list"> {sortedItems.map((item, index) => ( <li className="sort-list-item" key={`${item.id}-${index}`}> {itemRender(item)} </li> ))} </ul> </> ); }; export default SortList;
完整示例代码可前往这里查看。
17. 处理不同情况时,使用 value === case && <Component /> 以避免保留旧状态
不好的写法: 在如下示例中,在切换时计数器 count 不会重置。发生这种情况的原因是,在渲染同一组件时,其状态在currentTab更改后保持不变。
tab.tsx:
const tabList = [ { label: "首页", value: "tab-1", }, { label: "详情页", value: "tab-2", }, ]; export interface TabItem { label: string; value: string; } export interface TabProps { tabs: TabItem[]; currentTab: string | TabItem; onTab: (v: string | TabItem) => void; labelInValue?: boolean; } const Tab: React.FC<TabProps> = ({ tabs = tabList, currentTab, labelInValue, onTab, }) => { const currentTabValue = useMemo( () => (labelInValue ? (currentTab as TabItem)?.value : currentTab), [currentTab, labelInValue] ); return ( <div className="tab"> {tabs?.map((item, index) => ( <div className={`tab-item${ currentTabValue === item.value ? " active" : "" }`} key={`${item.value}-${index}`} onClick={() => onTab?.(labelInValue ? item : item.value)} > {item.label} </div> ))} </div> ); }; export default Tab;
Resource.tsx:
export interface ResourceProps { type: string; } const Resource: React.FC<ResourceProps> = ({ type }) => { const [count, setCount] = useState(0); const onHandleClick = () => { setCount((c) => c + 1); }; return ( <div className="tab-content"> 你当前在{type === "tab-1" ? "首页" : "详情页"}, <button onClick={onHandleClick} className="btn" type="button"> 点击我 </button> 增加访问{count}次数 </div> ); };
推荐写法: 根据 currentTab 渲染组件或在类型改变时使用 key 强制重新渲染组件。
function App() { const [currentTab, setCurrentTab] = useState("tab-1"); return ( <> <Tab currentTab={currentTab} onTab={(v) => setCurrentTab(v as string)} /> {currentTab === "tab-1" && <Resource type="tab-1" />} {currentTab === "tab-2" && <Resource type="tab-2" />} </> ); } // 使用key属性 function App() { const [currentTab, setCurrentTab] = useState("tab-1"); return ( <> <Tab currentTab={currentTab} onTab={(v) => setCurrentTab(v as string)} /> <Resource type={currentTab} key={currentTab} /> </> ); }
完整示例代码可前往这里查看。
18. 始终使用错误边界处理组件渲染错误
默认情况下,如果你的应用程序在渲染过程中遇到错误,整个 UI 都会崩溃。
为了防止这种情况,请使用错误边界来:
- 即使发生错误,也要保持应用程序的某些部分正常运行。
- 显示用户友好的错误消息并可选择跟踪错误。
提示:你可以使用 react-error-boundary 库。
三. key 与 ref
19. 使用 crypto.randomUUID 或 Math.random 生成 key
map 调用(也就是列表渲染)中的 JSX 元素始终需要 key。
假设你的元素还没有 key。在这种情况下,你可以使用 crypto.randomUUID、Math.random 或 uuid 库生成唯一 ID。
注意:请注意,旧版浏览器中未定义 crypto.randomUUID。
20. 确保你的列表项 id 是稳定的(即:它们在渲染中是不会发生变化的)
尽可能的让 id/key 可以稳定。
否则,React 可能会无用地重新渲染某些组件,或者触发一些功能异常,如下例所示。
不好的写法: 每次 App 组件渲染时 selectItemId 都会发生变化,因此设置 id 的值将永远不会正确。
const App = () => { const [items, setItems] = useState([]); const [selectItemId, setSelectItemId] = useState(undefined); const loadItems = () => { fetchItems().then((res) => setItems(res)); }; // 请求列表 useEffect(() => { loadItems(); }, []); // 添加列表id,这是一种很糟糕的做法 const newItems = items.map((item) => ({ ...item, id: crypto.randomUUID() })); return ( <List items={newItems} selectedItemId={selectItemId} onSelectItem={setSelectItemId} /> ); };
推荐写法: 当我们获取列表项的时候添加 id。
const App = () => { const [items, setItems] = useState([]); const [selectItemId, setSelectItemId] = useState(undefined); const loadItems = () => { // 获取列表数据并通过 id 保存 fetchItems().then((res) => // 一旦获得结果,我们就会添加“id” setItems(res.map((item) => ({ ...item, id: crypto.randomUUID() }))) ); }; // 请求列表 useEffect(() => { loadItems(); }, []); return ( <List items={items} selectedItemId={selectItemId} onSelectItem={setSelectItemId} /> ); };
21. 策略性地使用 key 属性来触发组件重新渲染
想要强制组件从头开始重新渲染?只需更改其 key 属性即可。
在下面的示例中,我们使用此技巧在切换到新选项卡时重置错误边界。(该示例基于前面第 17 点所展示的示例改造)
Resource.tsx:
export interface ResourceProps { type: string; } const Resource: React.FC<ResourceProps> = ({ type }) => { const [count, setCount] = useState(0); const onHandleClick = () => { setCount((c) => c + 1); }; // 新增抛出异常的代码 useEffect(() => { if (type === "tab-1") { throw new Error("该选项不可切换"); } }, []); return ( <div className="tab-content"> 你当前在{type === "tab-1" ? "首页" : "详情页"}, <button onClick={onHandleClick} className="btn" type="button"> 点击我 </button> 增加访问{count}次数 </div> ); };
App.tsx:
import { ErrorBoundary } from "react-error-boundary"; const App = () => { const [currentTab, setCurrentTab] = useState("tab-1"); return ( <> <Tab currentTab={currentTab} onTab={(v) => setCurrentTab(v as string)} /> <ErrorBoundary fallback={<div className="error">组件渲染发生了一些错误</div>} key={currentTab} // 如果没有key属性,当currentTab值为“tab-2”时也会呈现错误 > <Resource type={currentTab} /> </ErrorBoundary> </> ); };
完整示例代码可前往这里查看。
22. 使用 ref 回调函数执行诸如监控大小变化和管理多个节点元素等任务。
你知道可以将函数传递给 ref 属性而不是 ref 对象吗?
它的工作原理如下:
- 当 DOM 节点添加到屏幕时,React 会以 DOM 节点作为参数调用该函数。
- 当 DOM 节点被移除时,React 会以 null 调用该函数。
在下面的示例中,我们使用此技巧跳过 useEffect。
不好的写法: 使用 useEffect 关注输入框焦点
const FocusInput = () => { const ref = useRef<HTMLInputElement>(); useEffect(() => { ref.current?.focus(); }, []); return <input ref={ref} type="text" />; };
推荐写法: 我们在输入可用时立即聚焦输入。
const FocusInput = () => { const ref = useCallback((node) => node?.focus(), []); return <input ref={ref} type="text" />; };
四. 组织 react 代码
23. 将 React 组件与其资源(例如样式、图像等)放在一起
始终将每个 React 组件与相关资源(如样式和图像)放在一起。
这样,当不再需要组件时,可以更轻松地删除它们。
它还简化了代码导航,因为你需要的一切都集中在一个地方。
24. 限制组件文件大小
包含大量组件和导出内容的大文件可能会令人困惑。
此外,随着更多内容的添加,它们往往会变得更大。
因此,请以合理的文件大小为目标,并在合理的情况下将组件拆分为单独的文件。
25. 限制功能组件文件中的返回语句数量
功能组件中的多个返回语句使得很难看到组件返回的内容。
对于我们可以搜索渲染术语的类组件来说,这不是问题。
一个方便的技巧是尽可能使用不带括号的箭头函数(VSCode 有一个针对此的操作)。
不好的写法: 更难发现组件返回语句。
export interface UserInfo { id: string; name: string; age: number; } export interface UserListProps { users: UserInfo[]; searchUser: string; onSelectUser: (u: UserInfo) => void; } const UserList: React.FC<UserListProps> = ({ users, searchUser, onSelectUser, }) => { // 多余return语句 const filterUsers = users?.filter((user) => { return user.name.includes(searchUser); }); const onSelectUserHandler = (user) => { // 多余return语句 return () => { onSelectUser(user); }; }; return ( <> <h2>用户列表</h2> <ul> {filterUsers.map((user, index) => { return ( <li key={`${user.id}-${index}`} onClick={onSelectUserHandler(user)}> <p> <span>用户id</span> <span>{user.id}</span> </p> <p> <span>用户名</span> <span>{user.name}</span> </p> <p> <span>用户年龄</span> <span>{user.age}</span> </p> </li> ); })} </ul> </> ); };
推荐写法: 组件仅有一个返回语句。
export interface UserInfo { id: string; name: string; age: number; } export interface UserListProps { users: UserInfo[]; searchUser: string; onSelectUser: (u: UserInfo) => void; } const UserList: React.FC<UserListProps> = ({ users, searchUser, onSelectUser, }) => { const filterUsers = users?.filter((user) => user.name.includes(searchUser)); const onSelectUserHandler = (user) => () => onSelectUser(user); return ( <> <h2>用户列表</h2> <ul> {filterUsers.map((user, index) => ( <li key={`${user.id}-${index}`} onClick={onSelectUserHandler(user)}> <p> <span>用户id</span> <span>{user.id}</span> </p> <p> <span>用户名</span> <span>{user.name}</span> </p> <p> <span>用户年龄</span> <span>{user.age}</span> </p> </li> ))} </ul> </> ); };
26. 优先使用命名导出而不是默认导出
让我们比较一下这两种方法:
//默认导出 export default function App() { // 组件内容 } // 命名导出 export function App() { // 组件内容 }
我们现在就像如下这样导入组件:
// 默认导入 import App from "/path/to/App"; // 命名导入 import { App } from "/path/to/App";
默认导出存在如下一些问题:
- 如果组件被重命名,编辑器将不会自动重命名导出。
例如,如果将 App 重命名为 Index,我们将得到以下内容:
// 默认导入名字并未更改 import App from "/path/to/Index"; // 命名导入名字已更改 import { Index } from "/path/to/Index";
- 很难看出从具有默认导出的文件中导出了什么。
例如,在命名导入的情况下,一旦我们输入 import { } from "/path/to/file"
,当我将光标放在括号内时就会获得自动完成功能。
- 默认导出很难重新再导出。
例如,如果我想从 index 文件重新导出 App 组件,我必须执行以下操作:
export { default as App } from "/path/to/App";
使用命名导出的解决方案更加直接。
export { App } from "/path/to/App";
因此,建议默认使用命名导出。
注意:即使你使用的是 React lazy,你仍然可以使用命名导出。请参阅此处的介绍示例。
五. 高效的状态管理
27. 永远不要为可以从其他 state 或 props 派生的值创建新的 state
state 越多 = 麻烦越多。
每个 state 都可能触发重新渲染,并使重置 state 变得麻烦。
因此,如果可以从 state 或 props 中派生出值,则跳过添加新的 state。
不好的做法:filteredUsers 不需要处于 state 中。
const FilterUserComponent = ({ users }) => { const [filters, setFilters] = useState([]); // 创建了新的state const [filteredUsers, setFilteredUsers] = useState([]); const filterUsersMethod = (filters, users) => { // 过滤逻辑方法 }; useEffect(() => { setFilteredUsers(filterUsersMethod(filters, users)); }, [users, filters]); return ( <Card> <Filters filters={filters} onChangeFilters={setFilters} /> {filteredUsers.length > 0 && <UserList users={filteredUsers} />} </Card> ); };
推荐做法: filteredUsers 由 users 和 filters 决定。
const FilterUserComponent = ({ users }) => { const [filters, setFilters] = useState([]); const filterUsersMethod = (filters, users) => { // 过滤逻辑方法 }; const filteredUsers = filterUsersMethod(filters, users); return ( <Card> <Filters filters={filters} onChangeFilters={setFilters} /> {filteredUsers.length > 0 && <UserList users={filteredUsers} />} </Card> ); };
28. 将 state 创建在仅需要更新的组件内部,以减少组件的重新渲染
每当组件内部的状态发生变化时,React 都会重新渲染该组件及其所有子组件(包裹在 memo 中的子组件除外)。
即使这些子组件不使用已更改的状态,也会发生这种情况。为了最大限度地减少重新渲染,请尽可能将状态移到组件树的下方。
不好的做法: 当 type 发生改变时,会使不依赖 type 状态的 LeftList 和 RightList 组件也触发重新渲染。
const App = () => { const [type, setType] = useState(""); return ( <Container> <LeftList /> <Main type={type} setType={setType} /> <RightList /> </Container> ); }; const mainBtnList = [ { label: "首页", value: "home", }, { label: "详情页", value: "detail", }, ]; const Main = ({ type, setType }) => { return ( <> {mainBtnList.map((item, index) => ( <Button className={`${type.value === type ? "active" : ""}`} key={`${item.value}-${index}`} onClick={() => setType(item.value)} > {item.label} </Button> ))} </> ); };
推荐做法: 将状态耦合到 Main 组件内部,仅影响 Main 组件的重新渲染。
const App = () => { return ( <Container> <LeftList /> <Main /> <RightList /> </Container> ); }; const mainBtnList = [ { label: "首页", value: "home", }, { label: "详情页", value: "detail", }, ]; const Main = () => { const [type, setType] = useState(""); return ( <> {mainBtnList.map((item, index) => ( <Button className={`${type.value === type ? "active" : ""}`} key={`${item.value}-${index}`} onClick={() => setType(item.value)} > {item.label} </Button> ))} </> ); };
29. 定义需要明确初始状态和当前状态的区别
不好的做法: 不清楚 userInfo 只是初始值,这可能会导致状态管理的混乱或错误。
const UserInfo = ({ userInfo }) => { const [userInfo, setUserInfo] = useState(userInfo); return ( <Card> <Title>当前用户: {userInfo?.name}</Title> <UserInfoDetail detail={userInfo?.detail} /> </Card> ); };
推荐做法: 命名可以清楚地表明什么是初始状态,什么是当前状态。
const UserInfo = ({ initialUserInfo }) => { const [userInfo, setUserInfo] = useState(initialUserInfo); return ( <Card> <Title>当前用户: {userInfo?.name}</Title> <UserInfoDetail detail={userInfo?.detail} /> </Card> ); };
30. 根据之前的状态更新状态,尤其是在使用 useCallback 进行缓存时
React 允许你将更新函数从 useState 传递给 set 函数。
此更新函数使用当前状态来计算下一个状态。
每当需要根据之前状态更新状态时,都可以使用此行为,尤其是在使用 useCallback 包装的函数内部。事实上,这种方法可以避免将状态作为钩子依赖项之一。
不好的做法: 无论什么时候,当 todoList 变化的时候,onHandleAddTodo 和 onHandleRemoveTodo 都会跟着改变。
const App = () => { const [todoList, setTodoList] = useState([]); const onHandleAddTodo = useCallback( (todo) => { setTodoList([...todoList, todo]); }, [todoList] ); const onHandleRemoveTodo = useCallback( (todo) => { setTodoList([...todoList].filter((item) => item.id !== todo.id)); }, [todoList] ); return ( <div className="App"> <TodoInput onAddTodo={onHandleAddTodo} /> <TodoList todoList={todoList} onRemoveTodo={onHandleRemoveTodo} /> </div> ); };
推荐做法: 即使 todoList 发生变化,onHandleAddTodo 和 onHandleRemoveTodo 仍然保持不变。
const App = () => { const [todoList, setTodoList] = useState([]); const onHandleAddTodo = useCallback((todo) => { setTodoList((prevTodoList) => [...prevTodoList, todo]); }, []); const onHandleRemoveTodo = useCallback((todo) => { setTodoList((prevTodoList) => [...prevTodoList].filter((item) => item.id !== todo.id) ); }, []); return ( <div className="App"> <TodoInput onAddTodo={onHandleAddTodo} /> <TodoList todoList={todoList} onRemoveTodo={onHandleRemoveTodo} /> </div> ); };
31. 使用 useState 中的函数进行延迟初始化并提高性能,因为它们只被调用一次。
在 useState 中使用函数可确保初始状态仅计算一次。
这可以提高性能,尤其是当初始状态来自“昂贵”操作(例如从本地存储读取)时。
不好的做法:每次组件渲染时,我们都会从本地存储读取主题。
const THEME_LOCAL_STORAGE_KEY = "page_theme_key"; const Theme = ({ theme, onChangeTheme }) => { // .... }; const App = ({ children }) => { const [theme, setTheme] = useState( localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark" ); const onChangeTheme = (theme: string) => { setTheme(theme); localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme); }; return ( <div className={`app${theme ? ` ${theme}` : ""}`}> <Theme onChange={onChangeTheme} theme={theme} /> <div>{children}</div> </div> ); };
推荐做法: 当组件挂载时,我们仅只会读取本地存储一次。
// ... const App = ({ children }) => { const [theme, setTheme] = useState( () => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark" ); const onChangeTheme = (theme: string) => { setTheme(theme); localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme); }; return ( <div className={`app${theme ? ` ${theme}` : ""}`}> <Theme onChange={onChangeTheme} theme={theme} /> <div>{children}</div> </div> ); };
32. 使用 React 上下文来处理广泛需要的静态状态,以防止 prop 钻取
每当我有一些数据时,我都会使用 React 上下文:
- 在多个地方都需要(例如,主题、当前用户等)
- 主要是静态或只读的(即,用户不能/不会经常更改数据)
- 这种方法有助于避免 prop 钻取(即,通过组件层次结构的多个层传递数据或状态)。
来看一个示例的部分代码:
context.ts
// UserInfo接口来自测试数据 export const userInfoContext = createContext<string | UserInfoData>("loading"); export const useUserInfo = <T extends UserInfoData>() => { const value = useContext(userInfoContext); if (value == null) { throw new Error("Make sure to wrap the userInfoContext inside provider"); } return value as T; };
App.tsx
function App() { const [userInfoData, setUserInfoData] = useState<UserInfoData | string>( "loading" ); useEffect(() => { getCurrentUser().then(setUserInfoData); }, []); if (userInfoData === "loading") { return <Loading />; } return ( <div className="app"> <userInfoContext.Provider value={userInfoData}> <Header /> <Sidebar /> <Main /> </userInfoContext.Provider> </div> ); }
header.tsx:
const Header: React.FC<HeaderProps> = (props) => { // 使用context const userInfo = useUserInfo(); return ( <header className="header" {...props}> 欢迎回来{userInfo?.name} </header> ); };
main.tsx:
const Main: React.FC<MainProps> = ({ title }) => { const { posts } = useUserInfo(); return ( <div className="main"> <h2 className="title">{title}</h2> <ul className="list"> {posts?.map((post, index) => ( <li className="list-item" key={`${post.id}-${index}`}> {post.title} </li> ))} </ul> </div> ); };
33. React Context:将 react 上下文分为经常变化的部分和不经常变化的部分,以提高应用程序性能
React 上下文的一个挑战是,只要上下文数据发生变化,所有使用该上下文的组件都会重新渲染,即使它们不使用发生变化的上下文部分。
解决方案是什么?使用单独的上下文。
在下面的示例中,我们创建了两个上下文:一个用于操作(常量),另一个用于状态(可以更改)。
export interface TodosInfoItem { id?: string; title?: string; completed?: boolean; } export interface TodosInfo { search?: string; todos: TodosInfoItem[]; } export const todosStateContext = createContext<TodosInfo>(void 0); export const todosActionContext = createContext<Dispatch<ReducerActionParams>>( void 0 ); export interface ReducerActionParams extends TodosInfoItem { type?: string; value?: string; } export const getTodosReducer = ( state: TodosInfo, action: ReducerActionParams ) => { switch (action.type) { case TodosActionType.ADD_TODO: return { ...state, todos: [ ...state.todos, { id: crypto.randomUUID(), title: action.title, completed: false, }, ], }; case TodosActionType.REMOVE_TODO: return { ...state, todos: [...state.todos].filter((item) => item.id !== action.id), }; case TodosActionType.TOGGLE_TODO_STATUS: return { ...state, todos: [...state.todos].map((item) => item.id === action.id ? { ...item, completed: !item.completed } : item ), }; case TodosActionType.SET_SEARCH_TERM: return { ...state, search: action.value, }; default: return state; } };
完整示例代码前往这里查看。
34. React Context:当值计算不直接时,引入 Provider 组件
不好的做法:App 内部有太多逻辑来管理 theme context。
const THEME_LOCAL_STORAGE_KEY = "current-project-theme"; const DEFAULT_THEME = "light"; const ThemeContext = createContext({ theme: DEFAULT_THEME, setTheme: () => null, }); const 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 [selectedUserId, setSelectedUserId] = useState(undefined); const onUserSelect = (id) => { // 待做:一些逻辑 setSelectedUserId(id); }; const users = useSWR("/api/users", fetcher); return ( <div className="App"> <ThemeContext.Provider value={themeContextValue}> <UserList users={users} onUserSelect={onUserSelect} selectedUserId={selectedUserId} /> </ThemeContext.Provider> </div> ); };
推荐:主题 context 相关的逻辑封装在 ThemeProvider 中。
const THEME_LOCAL_STORAGE_KEY = "current-project-theme"; const DEFAULT_THEME = "light"; const ThemeContext = createContext({ theme: DEFAULT_THEME, setTheme: () => null, }); const 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> ); }; const App = () => { const [selectedUserId, setSelectedUserId] = useState(undefined); const onUserSelect = (id) => { // 待做:一些逻辑 setSelectedUserId(id); }; const users = useSWR("/api/users", fetcher); return ( <div className="App"> <ThemeProvider> <UserList users={users} onUserSelect={onUserSelect} selectedUserId={selectedUserId} /> </ThemeProvider> </div> ); };
35. 考虑使用 useReducer hook 作为轻量级状态管理解决方案
每当我的状态或复杂状态中的值太多并且不想依赖外部库时,我都会使用 useReducer。
当与上下文结合使用时,它对于更广泛的状态管理需求特别有效。
示例:这里。
36. 使用 useImmer 或 useImmerReducer 简化状态更新
使用 useState 和 useReducer 等钩子时,状态必须是不可变的(即,所有更改都需要创建新状态,而不是修改当前状态)。
这通常很难实现。
这就是 useImmer 和 useImmerReducer 提供更简单的替代方案的地方。它们允许你编写自动转换为不可变更新的“可变”代码。
不好的做法: 我们必须小心确保我们正在创建一个新的状态对象。
export const 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> <div class="form-item"> <label>邮箱号: </label> <input type="email" value={email} onChange={onEmailChange} /> </div> <div className="form-item"> <label>密码:</label> <input type="password" value={password} onChange={onPasswordChange} /> </div> </div> ); };
推荐做法: 更直接一点,我们可以直接修改 draftState。
import { useImmer } from "use-immer"; export const 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; }); }; // 剩余代码 };
37. 使用 Redux(或其他状态管理解决方案)来跨多个组件访问复杂的客户端状态
每当出现以下情况时,我都会求助于 Redux:
我有一个复杂的 FE 应用程序,其中包含大量共享的客户端状态(例如,仪表板应用程序)
- 我希望用户能够回到过去并恢复更改。
- 我不希望我的组件像使用 React 上下文那样不必要地重新渲染。
- 我有太多开始难以控制的上下文。
为了获得简化的体验,我建议使用 redux-tooltkit。
💡 注意:你还可以考虑 Redux 的其他替代方案,例如 Zustand 或 Recoil。
38. Redux:使用 Redux DevTools 调试你的状态
Redux DevTools 浏览器扩展是调试 Redux 项目的有用工具。
它允许你实时可视化你的状态和操作,在刷新时保持状态持久性等等。
要了解它的用途,请观看这个精彩的视频。
六. React 代码优化
39. 使用 memo 防止不必要的重新渲染
当处理渲染成本高昂且父组件频繁更新的组件时,将它们包装在 memo 中可能会改变渲染规则。
memo 确保组件仅在其 props 发生变化时重新渲染,而不仅仅是因为其父组件重新渲染。
在以下示例中,我通过 useGetInfoData 从服务器获取一些数据。如果数据没有变化,将 UserInfoList 包装在 memo 中将阻止它在数据的其他部分更新时重新渲染。
export const App = () => { const { currentUserInfo, users } = useGetInfoData(); return ( <div className="App"> <h1>信息面板</h1> <CurrentUserInfo data={currentUserInfo} /> <UserInfoList users={users} /> </div> ); }; const UserInfoList = memo(({ users }) => { // 剩余实现 });
一旦 React 编译器变得稳定,这个小技巧可能就不再有用了。
40. 用 memo 指定一个相等函数来指示 React 如何比较 props。
默认情况下,memo 使用Object.is将每个 prop 与其先前的值进行比较。
但是,对于更复杂或特定的场景,指定自定义相等函数可能比默认比较或重新渲染更有效。
示例如下:
const UserList = memo( ({ users }) => { return <div>{JSON.stringify(users)}</div>; }, (prevProps, nextProps) => { // 仅当最后一个用户或列表大小发生变化时才重新渲染 const prevLastUser = prevProps.users[prevProps.users.length - 1]; const nextLastUser = nextProps.users[nextProps.users.length - 1]; return ( prevLastUser.id === nextLastUser.id && prevProps.users.length === nextProps.users.length ); } );
41.声明缓存组件时,优先使用命名函数而不是箭头函数
定义缓存组件时,使用命名函数而不是箭头函数可以提高 React DevTools 中的清晰度。
箭头函数通常会导致像 _c2
这样的通用名称,这会使调试和分析更加困难。
不好的做法:对缓存组件使用箭头函数会导致 React DevTools 中的名称信息量较少。
const UserInfoList = memo(({ users }) => { // 剩余实现逻辑 });
推荐做法: 该组件的名称将在 DevTools 中可见。
const UserInfoList = memo(function UserInfoList({ users }) { // 剩余实现逻辑 });
42. 使用 useMemo 缓存昂贵的计算或保留引用
我通常会使用 useMemo:
- 当我有昂贵的计算,不应该在每次渲染时重复这些计算时。
- 如果计算值是非原始值,用作 useEffect 等钩子中的依赖项。
- 计算出的非原始值将作为 prop 传递给包裹在 memo 中的组件;否则,这将破坏缓存,因为 React 使用 Object.is 来检测 props 是否发生变化。
不好的做法:UserInfoList 的 memo 不会阻止重新渲染,因为每次渲染时都会重新创建样式。
export const UserInfo = () => { const { profileInfo, users, baseStyles } = useGetUserInfoData(); // 每次重新渲染我们都会得到一个样式对象 const styles = { ...baseStyles, margin: 10 }; return ( <div className="App"> <h1>用户页</h1> <Profile data={profileInfo} /> <UserInfoList users={users} styles={styles} /> </div> ); }; const UserInfoList = memo(function UserInfoListFn({ users, styles }) { /// 剩余实现 });
推荐做法: useMemo 的使用确保只有当 baseStyles 发生变化时,styles 才会发生变化,从而使 memo 能够有效防止不必要的重新渲染。
export const UserInfo = () => { const { profileInfo, users, baseStyles } = useGetUserInfoData(); // 每次重新渲染我们都会得到一个样式对象 const styles = useMemo(() => ({ ...baseStyles, margin: 10 }), [baseStyles]); return ( <div className="App"> <h1>用户页</h1> <Profile data={profileInfo} /> <UserInfoList users={users} styles={styles} /> </div> ); }; const UserInfoList = memo(function UserInfoListFn({ users, styles }) { /// 剩余实现 });
43. 使用 useCallback 缓存函数
useCallback 与 useMemo 类似,但专为缓存函数而设计。
不好的做法:每当 theme 发生变化时,handleThemeChange 都会被调用两次,并且我们会将日志推送到服务器两次。
const useTheme = () => { const [theme, setTheme] = useState("light"); // 每次渲染`handleThemeChange`都会改变 // 因此,每次渲染后都会触发该效果 const handleThemeChange = (newTheme) => { sendLog(["Theme changed"], { context: { theme: newTheme, }, }); setTheme(newTheme); }; useEffect(() => { const dqMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); handleThemeChange(dqMediaQuery.matches ? "dark" : "light"); const listener = (event) => { handleThemeChange(event.matches ? "dark" : "light"); }; dqMediaQuery.addEventListener("change", listener); return () => { dqMediaQuery.removeEventListener("change", listener); }; }, [handleThemeChange]); return theme; };
推荐做法:将 handleThemeChange 包装在 useCallback 中可确保仅在必要时重新创建它,从而减少不必要的执行。
const handleThemeChange = useCallback((newTheme) => { sendLog(["Theme changed"], { context: { theme: newTheme, }, }); setTheme(newTheme); }, []);
44. 缓存回调函数或使用程序钩子返回的值以避免性能问题
当你创建自定义钩子与他人共享时,记住返回的值和函数至关重要。
这种做法可以使你的钩子更高效,并防止任何使用它的人出现不必要的性能问题。
不好的做法:loadData 没有被缓存并产生了性能问题。
const useLoadData = (fetchData) => { const [result, setResult] = useState({ type: "pending", }); const loadData = async () => { setResult({ type: "loading" }); try { const data = await fetchData(); setResult({ type: "loaded", data }); } catch (err) { setResult({ type: "error", error: err }); } }; return { result, loadData }; };
推荐做法: 我们缓存所有内容,因此不会出现意外的性能问题。
const useLoadData = (fetchData) => { const [result, setResult] = useState({ type: "pending", }); // 包裹在 `useRef` 中并使用 `ref` 值,这样函数就不会改变 const fetchDataRef = useRef(fetchData); useEffect(() => { fetchDataRef.current = fetchData; }, [fetchData]); // 包裹在 `useCallback` 中并使用 `ref` 值,这样函数就不会改变 const loadData = useCallback(async () => { setResult({ type: "loading" }); try { const data = await fetchDataRef.current(); setResult({ type: "loaded", data }); } catch (err) { setResult({ type: "error", error: err }); } }, []); // 使用useMemo缓存值 return useMemo(() => ({ result, loadData }), [result, loadData]); };
45. 利用懒加载和 Suspense 让你的应用加载更快
构建应用时,请考虑对以下代码使用懒加载和 Suspense:
- 加载成本高。
- 仅与某些用户相关(如高级功能)。
- 对于初始用户交互而言并非立即需要。
在下面的示例,Slider 资源(JS + CSS)仅在你单击卡片后加载。
//... const LazyLoadedSlider = lazy(() => import("./Slider")); //... const App = () => { // .... return ( <div className="container"> {/* .... */} {selectedUser != null && ( <Suspense fallback={<div>Loading...</div>}> <LazyLoadedSlider avatar={selectedUser.avatar} name={selectedUser.name} address={selectedUser.address} onClose={closeSlider} /> </Suspense> )} </div> ); };
46. 限制网络以模拟慢速网络
你知道可以直接在 Chrome 中模拟慢速互联网连接吗?
这在以下情况下尤其有用:
- 用户报告加载时间缓慢,而你无法在更快的网络上复制。
- 你正在实施懒加载,并希望观察文件在较慢条件下的加载方式,以确保适当的加载状态。
47. 使用 react-window 或 react-virtuoso 高效渲染列表
切勿一次性渲染一长串项目,例如聊天消息、日志或无限列表。
这样做可能会导致浏览器卡死崩溃。相反,可以使用虚拟化列表,这意味着仅渲染可能对用户可见的项目子集。
react-window、react-virtuoso 或 @tanstack/react-virtual 等库就是为此目的而设计的。
不好的做法:NonVirtualList 会同时呈现所有 50,000 条日志行,即使它们不可见。
const NonVirtualList = ({ items }: { items: LogLineItem[] }) => { return ( <div style={{ height: "100%" }}> {items?.map((log, index) => ( <div key={log.id} style={{ padding: "5px", borderBottom: index === items.length - 1 ? "none" : "1px solid #535455", }} > <LogLine log={log} index={index} /> </div> ))} </div> ); };
推荐做法: VirtualList
仅渲染可能可见的项目。
const VirtualList = ({ items }: { items: LogLineItem[] }) => { return ( <Virtuoso style={{ height: "100%" }} data={items} itemContent={(index, log) => ( <div key={log.id} style={{ padding: "5px", borderBottom: index === items.length - 1 ? "none" : "1px solid #535455", }} > <LogLine log={log} index={index} /> </div> )} /> ); };
你可以在这个完整的示例中在两个选项之间切换,并注意使用 NonVirtualList
时应用程序的性能有多糟糕。
七. 总结
到此这篇关于React组件、状态管理、代码优化的技巧的文章就介绍到这了,更多相关React实践小技巧内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!