不使用useCallback/useMemo等阻止Button重渲染及相关问题
React Todo组件集问题解答
组件功能与渲染效果
该组件集包含App、Button、Todo三个组件,点击任意按钮可通过API获取对应ID的Todo数据并展示。渲染效果为:页面顶部显示"Hello CodeSandbox"标题,下方排列4个按钮(Button 1至Button 4),按钮下方展示Todo的标题和描述内容。
原组件代码
App.jsx
import { useEffect, useState } from 'react' import './App.css' import Button from './Button' import Todo from './Todo' import axios from 'axios' export default function App() { const arr = [1, 2, 3, 4]; const [todo, setTodo] = useState({title:"",description:""}); useEffect(()=>eventFunc(1),[]); function eventFunc(id) { (async () => { const res = await axios.get(`https://sum-server.100xdevs.com/todo?id=${id}`); setTodo(res.data.todo); })(); } return ( <> <h1>Hello CodeSandbox</h1> {arr.map((id) => ( <Button key={id} eventFunc={eventFunc} id={id} /> ))} <Todo todo={todo} /> </> ) }
Button.jsx
export default function Button({ id, eventFunc }) { return <button onClick={()=>eventFunc(id)}key={id}> Button {id}</button>; }
Todo.jsx
export default function Todo({todo}){ return ( <> <h1>{todo.title}</h1> <h3>{todo.description}</h3> </> ) }
待解决的三个技术问题
- 不使用自定义hooks、
useCallback()或useMemo(),如何阻止Button组件重渲染? - 是否可通过API调用将todo的初始state值设为第一个todo而非空对象?
- 尝试将
eventFunc()移至App()函数外部,并使用memo()包裹Button组件的方案失败,出现错误「Uncaught TypeError: Cannot read properties of undefined (reading 'title') at Todo (Todo.jsx:4:19)」,该错误的原因是什么?
失败方案代码
App.jsx
import { useEffect, useState } from 'react' import './App.css' import Button from './Button' import Todo from './Todo' import axios from 'axios' function eventFunc(id) { (async () => { const res = await axios.get(`https://sum-server.100xdevs.com/todo?id=${id}`); return res.data.todo; })(); } export default function App() { const arr = [1, 2, 3, 4]; const [todo, setTodo] = useState({title:"",description:""}); useEffect(()=>setTodo(eventFunc(1)),[]); return ( <> <h1>Hello CodeSandbox</h1> {arr.map((id) => ( <Button key={id} eventFunc={eventFunc} id={id} /> ))} <Todo todo={todo} /> </> ) }
Button.jsx
import {memo} from 'react' export default memo(function Button({ id, eventFunc }) { return <button onClick={()=>eventFunc(id)}key={id}> Button {id}</button>; })
问题解答
1. 不使用自定义hooks、useCallback()或useMemo(),阻止Button组件重渲染的方法
可以将eventFunc定义在App组件外部,让它接收setTodo作为参数,确保函数引用不会随App组件渲染而变化,再配合memo包裹Button组件:
- 定义外部
eventFunc:async function eventFunc(setTodo, id) { const res = await axios.get(`https://sum-server.100xdevs.com/todo?id=${id}`); setTodo(res.data.todo); } - App组件中传递
setTodo给Button:{arr.map((id) => ( <Button key={id} eventFunc={() => eventFunc(setTodo, id)} id={id} /> ))} - 用
memo包裹Button组件,这样当eventFunc引用稳定时,Button不会因App重渲染而重复渲染。
2. 是否可通过API调用将todo的初始state值设为第一个todo而非空对象?
可以,但不能直接在useState初始值中执行异步请求(useState初始值为同步逻辑),推荐两种实现方式:
- 方式一:使用异步初始化函数
注意:这种方式下初始state是Promise对象,需要在Todo组件中处理加载状态,避免读取属性失败。const [todo, setTodo] = useState(async () => { const res = await axios.get(`https://sum-server.100xdevs.com/todo?id=1`); return res.data.todo; }); - 方式二:在useEffect中发起初始请求
优化初始state为null,并在Todo组件中做空值判断:// App.jsx const [todo, setTodo] = useState(null); useEffect(() => { async function fetchInitialTodo() { const res = await axios.get(`https://sum-server.100xdevs.com/todo?id=1`); setTodo(res.data.todo); } fetchInitialTodo(); }, []); // Todo.jsx export default function Todo({todo}){ if (!todo) return <div>加载中...</div>; return ( <> <h1>{todo.title}</h1> <h3>{todo.description}</h3> </> ) }
3. 失败方案错误原因分析
错误的核心原因有两点:
- eventFunc无有效返回值:外部
eventFunc内部的自执行异步函数返回了Todo数据,但eventFunc本身没有返回这个值,导致eventFunc(1)调用后返回undefined。在useEffect中执行setTodo(eventFunc(1))会把todo设为undefined,Todo组件读取undefined.title时触发报错。 - 异步逻辑处理错误:即使修复返回值,
eventFunc返回的是Promise对象,直接传给setTodo会将state设为Promise,而非期望的Todo数据,同样会导致读取属性失败。
正确的处理方式是在useEffect中等待异步请求完成后再更新state:
// 修复后的eventFunc async function eventFunc(id) { const res = await axios.get(`https://sum-server.100xdevs.com/todo?id=${id}`); return res.data.todo; } // App.jsx中的useEffect useEffect(()=>{ async function fetchInitial() { const initialTodo = await eventFunc(1); setTodo(initialTodo); } fetchInitial(); },[]);
内容的提问来源于stack exchange,提问作者Intangible _pg18
相关产品推荐
相关产品推荐

