React中使用Context如何避免列表项全局重渲染并仅更新指定项
问题分析与解决方案
我有一个列表,渲染多个ListItem组件,每个ListItem点击按钮可获取更多数据。当将API请求结果存储在组件自身state中时,功能正常;但使用Context结合useReducer管理数据时,点击某个ListItem后,所有ListItem都会重渲染,且全部显示相同数据。尝试使用React.memo无效,因为每次获取的新数据对所有列表项来说都是新值。
原正常工作的组件代码
const list = [{_id: "12345", name: "apples"}, {_id: "abcde", name: "pineapple"}, {_id: "54321", name: "bananas"}]; const detailsData = [ {_id: "15", name: "apples", info:"info about an apple"}, {_id: "abc", name: "pineapple",info:"info about a pineapple"}, {_id: "1", name: "apples", info:"info about apple being green"}, {_id: "2", name: "apples", info:"info about apple being round"} ]; function App() { return ( <div className="App"> {list.map((listItem, index) => { return ( <ListItem name={listItem.name} key={index} /> ) })} </div> ); } const ListItem = ({name}) => { const [showMore, setShowMore] = useState(false); const [details, setDetails] = useState([]) const getDetails = async (name) => { //here it works fine as long as I make api call here + save details in component state new Promise((resolve) => { //fake api call const details = detailsData.filter(detail => detail.name === name); setDetails(details) }) }; const handleOnClick = async (name) => { setShowMore(!showMore) getDetails(name); } return ( <div> <p>{name}</p> <button onClick={() => handleOnClick(name)}>see details</button> { showMore && <Details details={details}/> } </div> ) }; const Details = ({details}) => { return ( <div> {details.length > 0 ? details.map((detail, i) => { return <p key={i}>{detail.info}</p> }) : <p>No info here</p>} </div> ) };
使用Context后的代码片段
... const ListItem = ({name}) => { const [showMore, setShowMore] = useState(false); const {state: {detailsFromContext}, getDetailsFromContext} = useContext(SearchContext); const handleOnClick = async (name) => { setShowMore(!showMore) getDetailsFromContext(name); } ... return ( <div> <p>{name}</p> <button onClick={() => handleOnClick(name)}>see details</button> { showMore && <Details details={detailsFromContext}/> } </div> ) };
Context实现代码
export default (reducer, actions, defaultValue) => { const Context = createContext(); const Provider = ({children}) => { const [state, dispatch] = useReducer(reducer, defaultValue) const boundActions = {}; for (let key in actions) { boundActions[key] = actions[key](dispatch); } return ( <Context.Provider value={{state, ...boundActions}}> {children} </Context.Provider> ); }; return { Context, Provider}; }; const searchReducer = (state, action) => { switch (action.type) { case'FETCH_DETAILS': return {...state, detailsFromContext: action.payload} default: return state; } }; const getDetailsFromContext = dispatch => async (name) => { ... const detailsFromContext = await API.fetch(name); dispatch({type: 'FETCH_DETAILS', payload: detailsFromContext}) ... }; export const {Provider, Context} = createDataContext( searchReducer, {getDetailsFromContext}, { detailsFromContext: [], } )
问题根源
- 全局状态冲突:当前Context的
detailsFromContext是单一数组,点击任意ListItem都会覆盖这个全局状态,导致所有ListItem读取到相同的最新数据。 - 不必要重渲染:Context状态更新时,所有订阅该Context的组件(即所有ListItem)都会触发重渲染——因为Context的
value对象每次都会重新创建({state, ...boundActions}是新对象),即使React.memo也无法阻止,因为引用发生了变化。
解决方法
方法一:修改Context状态结构,按列表项标识存储数据
把全局状态改成键值对,以列表项的唯一标识(比如name或_id)作为key,存储对应的数据,这样每个ListItem只读取自己对应的数据:
- 更新Context的初始状态和reducer:
const searchReducer = (state, action) => { switch (action.type) { case 'FETCH_DETAILS': return { ...state, detailsFromContext: { ...state.detailsFromContext, // 以当前列表项的name为key存储详情 [action.payload.name]: action.payload.data } } default: return state; } }; export const { Provider, Context } = createDataContext( searchReducer, { getDetailsFromContext }, { detailsFromContext: {}, // 初始化为空对象 } )
- 更新获取数据的action:
const getDetailsFromContext = dispatch => async (name) => { const details = await API.fetch(name); dispatch({ type: 'FETCH_DETAILS', payload: { name, data: details } }) };
- 修改ListItem组件,读取对应key的数据:
const ListItem = ({ name }) => { const [showMore, setShowMore] = useState(false); const { state: { detailsFromContext }, getDetailsFromContext } = useContext(SearchContext); // 只读取当前name对应的详情,没有则返回空数组 const currentDetails = detailsFromContext[name] || []; const handleOnClick = async () => { setShowMore(!showMore); // 避免重复请求已加载的数据 if (!currentDetails.length) { getDetailsFromContext(name); } } return ( <div> <p>{name}</p> <button onClick={handleOnClick}>see details</button> {showMore && <Details details={currentDetails} />} </div> ) };
方法二:优化Context避免不必要重渲染
即使修改了状态结构,Context的value对象每次渲染都会重新创建,导致订阅组件重渲染。可以通过以下方式优化:
- 使用
useMemo缓存value对象,只有当state或boundActions变化时才重新创建:
const Provider = ({ children }) => { const [state, dispatch] = useReducer(reducer, defaultValue); // 缓存绑定后的action,避免每次渲染重新创建 const boundActions = useMemo(() => { const actionsObj = {}; for (let key in actions) { actionsObj[key] = actions[key](dispatch); } return actionsObj; }, [actions, dispatch]); // 缓存Context的value const contextValue = useMemo(() => ({ state, ...boundActions }), [state, boundActions]); return ( <Context.Provider value={contextValue}> {children} </Context.Provider> ); };
- 给
ListItem加上React.memo:
const ListItem = React.memo(({ name }) => { // 组件内容不变 });
现在每个ListItem读取的是自己对应的数据,当其他列表项更新时,当前ListItem的currentDetails不会变化,React.memo可以阻止不必要的重渲染。
方法三:判断场景是否需要Context
如果这些详情数据不需要在其他组件共享,只是ListItem自己使用,那完全没必要用Context,保持原组件内部state的方式更高效,也不会有重渲染和数据冲突问题。只有当多个无关组件需要共享这些数据时,才适合用Context。
内容的提问来源于stack exchange,提问作者user6377312
相关产品推荐
相关产品推荐

