为什么 props 没有改变时 React 组件会重新渲染?

Why is React component rerendering when props has not changed?

我在 ReactJS 16.8.5 和 React-Redux 3.7.2 上构建了一个应用程序。当应用程序加载应用程序安装时,会设置初始存储并针对 Firebase 实时数据库设置数据库订阅。该应用程序包含侧边栏、header 和内容部分。通过使用 React Developer Tools 分析应用程序,我可以看到 Sidebar 被渲染了几次 - 触发了 child 组件的重新渲染。我已经实现了 React.memo 以避免在 props 更改时重新渲染。 据我所见,道具没有改变,但 Sidebar 仍然重新渲染,这让我很困惑。

app.js

//Imports etc...
const jsx = (
  <React.StrictMode>
    <Provider store={store}>
      <AppRouter />
    </Provider>
  </React.StrictMode>
)

let hasRendered = false
const renderApp = () => {
  if (!hasRendered) { //make sure app only renders one time
    ReactDOM.render(jsx, document.getElementById('app'))
    hasRendered = true
  }
}

firebase.auth().onAuthStateChanged((user) => {
  if (user) {
    // Set initial store and db subscriptions
    renderApp()
  }
})

AppRouter.js

//Imports etc...
const AppRouter = ({}) => {
  //...
  return (
    <React.Fragment>
      //uses Router instead of BrowserRouter to use our own history and not the built in one
      <Router history={history}>    
        <div className="myApp">
          <Route path="">
            <Sidebar />
          </Route>
          //More routes here...
        </div>
      </Router>
    </React.Fragment>
  )
}
//...
export default connect(mapStateToProps, mapDispatchToProps)(AppRouter)

Sidebar.js

//Imports etc...
export const Sidebar = (props) => {
  const onRender = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
    if (id !== 'Sidebar') { return }
    console.log('Profile', phase, actualDuration)
  }
  return (
    <Profiler id="Sidebar" onRender={onRender}>
      <React.Fragment>
        {/* Contents of Sidebar */}
      </React.Fragment>
    </Profiler>
}

const mapStateToProps = (state) => {
  console.log('Sidebar mapStateToProps')
  return {
    //...
  }
}
const areEqual = (prevProps, nextProps) => {
  const areStatesEqual = _.isEqual(prevProps, nextProps)
  console.log('Profile Sidebar isEqual', areStatesEqual)
  return areStatesEqual
}
export default React.memo(connect(mapStateToProps, mapDispatchToProps)(Sidebar),areEqual)

Console output

Sidebar mapStateToProps 2 
Profile Sidebar mount 225 
Sidebar mapStateToProps 
Profile Sidebar isEqual true 
Sidebar mapStateToProps 
Profile Sidebar update 123 
Sidebar mapStateToProps 2 
Profile Sidebar update 21 
Sidebar mapStateToProps 
Profile Sidebar update 126 
Sidebar mapStateToProps 
Profile Sidebar update 166 
Sidebar mapStateToProps 
Profile Sidebar update 99 
Sidebar mapStateToProps 
Sidebar mapStateToProps 
Sidebar mapStateToProps 
Sidebar mapStateToProps
Sidebar mapStateToProps 
Sidebar mapStateToProps 
Profile Sidebar update 110 
Sidebar mapStateToProps 
Sidebar mapStateToProps 
Sidebar mapStateToProps 
Profile Sidebar update 4

为什么 Sidebar 重渲染 八次 而 props 没有改变?预计会有一次重新渲染?

亲切的问候/K

如评论;当 mapStateToProps return 是一个新对象时,即使相关值没有变化,它也会重新渲染连接的组件。

这是因为 {} !== {},具有相同 props 和值的对象不等于另一个具有相同 props 和值的对象,因为 React 比较对象引用而不是对象的值。这就是为什么你不能通过改变它来改变状态。变异会更改对象中的值,但不会更改对对象的引用。

您的 mapStateToProps 必须 return 在第 2 层有一个新的引用才能使用相同的值重新渲染,因此 {val:1} 不会重新渲染,但 {something:{val:1}} 会。

下面的代码显示了不记忆 mapStateToProps 的结果是如何导致重新渲染的:

const { Provider, connect, useDispatch } = ReactRedux;
const { createStore } = Redux;
const { createSelector } = Reselect;
const { useRef, useEffect, memo } = React;

const state = { val: 1 };
//returning a new state every action but no values
//  have been changed
const reducer = () => ({ ...state });
const store = createStore(
  reducer,
  { ...state },
  window.__REDUX_DEVTOOLS_EXTENSION__ &&
    window.__REDUX_DEVTOOLS_EXTENSION__()
);
const Component = (props) => {
  const rendered = useRef(0);
  rendered.current++;
  return (
    <div>
      <div>rendered:{rendered.current} times</div>
      props:<pre>{JSON.stringify(props)}</pre>
    </div>
  );
};
const selectVal = (state) => state.val;
const selectMapStateToProps = createSelector(
  selectVal,
  //will only re create this object when val changes
  (val) => console.log('val changed') || { mem: { val } }
);
const memoizedMapStateToProps = selectMapStateToProps;
const mapStateToProps = ({ val }) =>
  ({ nonMem: { val } }); //re creates props.nonMem every time
const MemoizedConnected = connect(memoizedMapStateToProps)(
  Component
);
//this mapStateToProps will create a props of {val:1}
//  pure components (returned by connect) will compare each property
//  of the prop object and not the props as a whole. Since props.val
//  never changed between renders it won't re render
const OneLevelConnect = connect(({ val }) => ({ val }))(
  Component
);
const Connected = connect(mapStateToProps)(Component);
const Pure = memo(function Pure() {
  //props never change so this will only be rendered once
  console.log('props never change so wont re render Pure');
  return (
    <div>
      <Connected />
      <MemoizedConnected />
      <OneLevelConnect />
    </div>
  );
});
const App = () => {
  const dispatch = useDispatch();
  useEffect(
    //dispatch an action every second, this will create a new
    // state ref but state.val never changes
    () => {
      setInterval(() => dispatch({ type: 88 }), 1000);
    },
    [dispatch] //dispatch never changes but linting tools don't know that
  );
  return <Pure />;
};

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>


<div id="root"></div>

mapStateToProps 函数也可以通过传递一个 return 函数的函数来进一步优化。通过这种方式,您可以在组件安装时创建一个备忘的选择器。这可以用在列表项中(见下面的代码)。

const { useRef, useEffect } = React;
const {
  Provider,
  useDispatch,
  useSelector,
  connect,
} = ReactRedux;
const { createStore } = Redux;
const { createSelector } = Reselect;
const state = {
  data: [
    {
      id: 1,
      firstName: 'Ben',
      lastName: 'Token',
    },
    {
      id: 2,
      firstName: 'Susan',
      lastName: 'Smith',
    },
  ],
};
//returning a new state every action but no values
//  have been changed
const reducer = () => ({ ...state });
const store = createStore(
  reducer,
  { ...state },
  window.__REDUX_DEVTOOLS_EXTENSION__ &&
    window.__REDUX_DEVTOOLS_EXTENSION__()
);
//selectors
const selectData = (state) => state.data;
const selectPerson = createSelector(
  selectData,
  (_, id) => id, //pass second argument to select person by id
  (people, _id) => people.find(({ id }) => id === _id)
);
//function that will create props for person component
//  from person out of state
const asPersonProps = (person) => ({
  person: {
    fullName: person.firstName + ' ' + person.lastName,
  },
});
//in ConnectedPerson all components share this selector
const selectPersonProps = createSelector(
  (state, { id }) => selectPerson(state, id),
  asPersonProps
);
//in OptimizedConnectedPerson each component has it's own
//  selector
const createSelectPersonProps = () =>
  createSelector(
    (state, { id }) => selectPerson(state, id),
    asPersonProps
  );

const Person = (props) => {
  const rendered = useRef(0);
  rendered.current++;
  return (
    <li>
      <div>rendered:{rendered.current} times</div>
      props:<pre>{JSON.stringify(props)}</pre>
    </li>
  );
};
//optimized mapStateToProps
const mapPersonStateToProps = createSelectPersonProps;
const OptimizedConnectedPerson = connect(
  mapPersonStateToProps
)(Person);
const ConnectedPerson = connect(selectPersonProps)(Person);
const App = () => {
  const dispatch = useDispatch();
  const people = useSelector(selectData);
  const rendered = useRef(0);
  rendered.current++;
  useEffect(
    //dispatch an action every second, this will create a new
    // state ref but state.val never changes
    () => {
      setInterval(() => dispatch({ type: 88 }), 1000);
    },
    [dispatch] //dispatch never changes but linting tools don't know that
  );
  return (
    <div>
      <h2>app rendered {rendered.current} times</h2>
      <h3>Connected person (will re render)</h3>
      <ul>
        {people.map(({ id }) => (
          <ConnectedPerson key={id} id={id} />
        ))}
      </ul>
      <h3>
        Optimized Connected person (will not re render)
      </h3>
      <ul>
        {people.map(({ id }) => (
          <OptimizedConnectedPerson key={id} id={id} />
        ))}
      </ul>
    </div>
  );
};

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>


<div id="root"></div>