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

Why does custom route HOC with useContext triggers re-render?

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

而且,我一路上学到了这一点,

A React context Provider will cause its consumers to re-render whenever the value provided changes.

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

这里是这个问题的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;

经过漫长的等待寻找答案,我终于找到了答案。

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

When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the componentprop, you would create a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component.

在我的旧自定义路线中

 // 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} /> 
      }}
    />