You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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只读取自己对应的数据:

  1. 更新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: {}, // 初始化为空对象
    }
)
  1. 更新获取数据的action:
const getDetailsFromContext = dispatch => async (name) => {
    const details = await API.fetch(name);
    dispatch({
        type: 'FETCH_DETAILS',
        payload: { name, data: details }
    })
};
  1. 修改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对象每次渲染都会重新创建,导致订阅组件重渲染。可以通过以下方式优化:

  1. 使用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>
    );
};
  1. 给ListItem加上React.memo:
const ListItem = React.memo(({ name }) => {
  // 组件内容不变
});

现在每个ListItem读取的是自己对应的数据,当其他列表项更新时,当前ListItem的currentDetails不会变化,React.memo可以阻止不必要的重渲染。


方法三:判断场景是否需要Context

如果这些详情数据不需要在其他组件共享,只是ListItem自己使用,那完全没必要用Context,保持原组件内部state的方式更高效,也不会有重渲染和数据冲突问题。只有当多个无关组件需要共享这些数据时,才适合用Context。


内容的提问来源于stack exchange,提问作者user6377312

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 18:47:47