为啥带有 useContext 触发器的自定义路由 HOC 会重新渲染?

Posted

技术标签:

【中文标题】为啥带有 useContext 触发器的自定义路由 HOC 会重新渲染?【英文标题】:Why does custom route HOC with useContext triggers re-render?为什么带有 useContext 触发器的自定义路由 HOC 会重新渲染? 【发布时间】:2021-03-13 12:33:50 【问题描述】:

我正在使用钩子探索反应上下文,并注意到为什么每次上下文状态更改时我的自定义路由都会重新呈现。在我的示例中,我使用切换加载状态的 useReducer 钩子创建了一个上下文提供程序。

而且,我一直在学习这一点,

只要提供的值发生变化,React 上下文提供者就会导致其消费者重新呈现。

但是,当上下文在常规父组件中使用时,它不会重新渲染。

这是解决此问题的 codesandbox

index.js

import React from "react";
import ReactDOM from "react-dom";

import Provider from './Provider';
import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <Provider>
      <App />
    </Provider>
  </React.StrictMode>,
  rootElement
);

Provider.js

import React from 'react';

export const AppContext = React.createContext();
export const DispatchContext = React.createContext();

const initState =  isLoading: false 

const reducer = (initState, action) => 
    switch(action.type) 
      case 'LOADING_ON':
      return 
        isLoading: true
      
      case 'LOADING_OFF':
      return 
        isLoading: false
      
      default: 
        throw new Error('Unknown action type');
    


const Provider = ( children ) => 
  const [state, dispatch] = React.useReducer(reducer,  isLoading: false );

  React.useEffect(() => 
    console.log('PROVIDER MOUNTED');
  , []);

  return (  
    <DispatchContext.Provider value=dispatch>
      <AppContext.Provider value= app: state >
        children
      </AppContext.Provider>
    </DispatchContext.Provider>
  )


export default Provider;

App.js

import React from 'react';
import  BrowserRouter, Route, Switch  from 'react-router-dom';
import Home from './Home';
import CustomRoute from './CustomRoute';
import  AppContext  from './Provider';

// const App = () => 
//   React.useEffect(() => 
//     console.log('App Mounted');
//   , []);

//   const  app  = React.useContext(AppContext);
//   return (
//     <Home app=app/>
//   )
// 

/*
  Why does context used in a custom route triggers rerender
  while when used on a regular component doesn't? ????????
*/

const App = () => (
    <BrowserRouter>
      <Switch>
        <CustomRoute path="/" exact component=Home />
        /* <Route path="/" exact component=Home /> */
      </Switch>
    </BrowserRouter>
);

export default App;


Child.js 用于测试子级是否也重新渲染。

import React from "react";

const Child = () => 
  React.useEffect(() => 
    console.log('Child MOUNTED');
  , []);

  const [text, setText] = React.useState('');
  
  const onTextChange = (e) => 
    setText(e.target.value);
  

  return (
    <div className="App">
      <h3>Child</h3>
      <input onChange=onTextChange value=text type="text" placeholder="Enter Text"/>
    </div>
  );



export default Child;

CustomRoute.js

import React from 'react';
import  Route  from 'react-router-dom';
import  AppContext  from './Provider';

const CustomRoute = ( component: Component, ...rest ) => 
  const  app  = React.useContext(AppContext);

  return (
  <Route 
      ...rest
      component=(props) => 
        // return <Component ...props  />  
        return <Component ...props app=app /> // pass down the cntext 
      
    />
)
 

export default CustomRoute;

我正在测试更改上下文的 isLoading 状态的值。如果您尝试在输入字段中输入内容并触发更改加载状态,则整个组件将重新渲染。

Home.js

import React from "react";
import "./styles.css";
import Child from './Child';
import  AppContext, DispatchContext from './Provider';

const Home = ( app ) => 
  React.useEffect(() => 
    console.log('HOME MOUNTED');
  , []);

  const [text, setText] = React.useState('');
  const dispatch = React.useContext(DispatchContext);
  // const  app  = React.useContext(AppContext);
  const onTextChange = (e) => 
    setText(e.target.value);
  

  return (
    <div className="App">
      <h3>Loading State: app.isLoading.toString()</h3>
      <input onChange=onTextChange value=text type="text" placeholder="Enter Text"/>
      <br/>
      <button onClick=() => dispatch( type: 'LOADING_ON' )>On</button>
      <button onClick=() => dispatch( type: 'LOADING_OFF' )>OFF</button>
      <br/>
      <Child />
    </div>
  );



export default Home;

【问题讨论】:

可能是因为您将对象文字 app: state 传递给 value 属性。每当Provider 重新渲染时,所有消耗AppContext 的组件也会重新渲染。 但是为什么当我在常规组件中使用它时,例如在我的codesandbox 中,如果您取消注释 App.js 中的第一个组件,它可以正常工作,但不能在自定义路由中使用? 【参考方案1】:

苦苦寻找答案,终于找到答案了。

在我的自定义路由中,我使用了component 道具而不是render 道具,这导致了UNMOUNTING 指定Route 上的组件并删除了孩子的状态。

当你使用组件(而不是下面的渲染或子组件)时,路由器使用 React.createElement 从给定组件创建一个新的 React 元素。这意味着如果您为 componentprop 提供内联函数,您将在每次渲染时创建一个新组件。这会导致卸载现有组件并安装新组件,而不仅仅是更新现有组件。

在我的旧自定义路线中

 // I used component prop before
<Route 
      ...rest
      component=(props) => 
        return <Component ...props app=app />
      
    />

为了解决这个问题,我使用了render prop 而不是component prop,如下所示:

<Route 
      ...rest
      render=(props) => 
        return <Component ...props app=app /> 
      
    />

【讨论】:

以上是关于为啥带有 useContext 触发器的自定义路由 HOC 会重新渲染?的主要内容,如果未能解决你的问题,请参考以下文章

React useEffect useContext - 上下文适用于某些组件,但不适用于动态路由

带有<router-link>的自定义Vue库组件,如何同步路由器状态?

为啥路由保护 canLoad 不会触发,但 canActivate 会

尝试使用 useContext() 获取对象时未定义对象

为啥在tailwindcss的自定义类中我不能定义2悬停:?

Laravel - 带有模板的自定义插件