为啥我的组件在使用 React 的 Context API 和 useEffect 挂钩时会渲染两次?

Posted

技术标签:

【中文标题】为啥我的组件在使用 React 的 Context API 和 useEffect 挂钩时会渲染两次?【英文标题】:Why does my component render twice when using React's Context API and the useEffect hook?为什么我的组件在使用 React 的 Context API 和 useEffect 挂钩时会渲染两次? 【发布时间】:2021-01-06 21:44:31 【问题描述】:

我正在尝试了解 Context API 如何与 React 挂钩(特别是 useEffectuseReducer)一起工作。 我不确定为什么我的一个组件 (ContactListPage) 似乎要渲染两次。

演示的最小示例:

index.js

ReactDOM.render(
  <ContactContextProvider>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </ContactContextProvider>,
  document.getElementById("root")
);

contact-context.js

export const ContactContext = createContext();

function reducer(state, action) 
  switch (action.type) 
    case "FETCH_CONTACTS": 
      return  ...state, contacts: action.payload ;
    
    default:
      throw new Error();
  


export const ContactContextProvider = (props) => 
  const [state, dispatch] = useReducer(reducer,  contacts: [] );
  const store = React.useMemo(() => ( state, dispatch ), [state, dispatch]);

  return (
    <ContactContext.Provider value=store>
      props.children
    </ContactContext.Provider>
  );
;

ContactListPage.js

export default function ContactListPage() 
  const  state, dispatch  = useContext(ContactContext);
  console.log(state);

  useEffect(() => 
    const fetchData = async () => 
      try 
        // Make Ajax request for contacts here
        dispatch(
          type: "FETCH_CONTACTS",
          payload: [ name: "Bob" ]
        );
       catch (error) 
        // handle error
      
    ;
    fetchData();
  , []); // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <div>
      <h1>List of Contacts</h1>
    </div>
  );

这里是the app running in a CodeSandbox。

ContactListPage 组件首次渲染时,控制台会记录以下内容:

Object contacts: Array[0]
Object contacts: Array[1]

这对我来说很有意义。最初contacts 数组是空的,然后在ContactListPage 组件的useEffect 钩子中,我调度了一个动作来添加一个联系人,因此该组件使用更新的联系人列表重新渲染。

当我离开这个视图(我正在使用 React 路由器),然后我导航回来时,问题就出现了。在这种情况下,我看到:

Object contacts: Array[1]
Object contacts: Array[1]

我原以为 console.log 语句只会触发一次,因为联系人数组没有改变。

我整个下午都在谷歌上搜索这个。我在 SO 上找到的其他答案都没有对我有用。

那么,我的问题是:为什么console.log 语句总是触发两次?


注意要在 CodeSandbox 中重现此内容:

单击添加联系人链接并观察 CodeSandbox 控制台中的第一个输出。 单击联系人列表链接并观察 CodeSandbox 控制台中的第二个输出。

【问题讨论】:

【参考方案1】:

所以这里的useEffect 实现发生在第一次渲染组件时。那是你得到结果的时候:

Object contacts: Array[0] Object contacts: Array[1]

由于您仍在应用程序中,因此在第一次渲染组件时调度的操作将保存在状态中。由于您已导航离开组件 .i.e.组件不再在 dom 中,当您在 dom 中再次加载组件时,因此在初始渲染后,useEffect 发生并在效果完成后渲染您的组件,因为您正在向减速器调度另一个动作,即使您有价值。

由于调度了一个新操作,这被视为状态更改,此时您会看到以下内容:

Object contacts: Array[1] Object contacts: Array[1]

您可以通过在减速器的这一行添加console.log 来测试:

现在您可以添加诸如检查或标志之类的内容,以确认数据已加载并且您不必再次进行提取。

所以我在您的useEffect 中添加了以下检查:

 if (state.contacts.length === 0) 
      fetchData();
 

还有尤里卡!!!少一个渲染。

我已经在link 上试用并修改了您的沙盒。因此,如果您想最大程度地减少不必要的调用和应用程序上的渲染,最好有这些检查。

【讨论】:

【参考方案2】:

这是因为当您更改路线时,您的组件再次挂载。所以当你从contact list 转到add contact 时:

您的 reducer 已将其设置为 name:bob

现在再次回到您的component list 组件。由于useContext 初始状态,组件正在发生两件事(钩子也会导致重新渲染),另一件事是由于您的useEffect 而导致,即您在安装组件时调度您的FETCH_CONTACTS

如果您在首次加载时看到控制台:


Object contacts: Array[0] 
Object contacts: Array[1]

观察控制台返回两条语句,第一个状态是联系人为空,第二个状态是使用reducer更新联系人]

现在,当您来自添加联系人页面时,控制台会显示:

Object contacts: Array[1]
Object contacts: Array[1]

两者具有相同的值,第一个来自上下文初始状态,第二个再次出现,因为 reducer 再次更新值。

如果您想看到自己添加更多控制台以清楚起见。在此处查看组件是如何安装和卸载的:https://codesandbox.io/s/elegant-ishizaka-enck6?file=/src/components/ContactListPage.js:478-492

【讨论】:

以上是关于为啥我的组件在使用 React 的 Context API 和 useEffect 挂钩时会渲染两次?的主要内容,如果未能解决你的问题,请参考以下文章

React Context 不渲染组件

为啥我的 React 组件列表在使用 Index 作为 key prop 时不会弄乱?

使用 React Context 时如何修复此错误

React Context API:消费者组件中未定义值

为啥 Enzyme 不从 React 组件加载样式?

React Context API似乎重新渲染每个组件