道具未更改时,为什么要重新渲染React组件?

时间:2020-04-19 09:10:11

标签: reactjs redux rerender

我已经在ReactJS 16.8.5和React-Redux 3.7.2上构建了一个应用程序。当应用程序加载应用程序挂载时,将设置初始存储,并针对Firebase实时数据库设置数据库订阅。该应用程序包含侧边栏,标题和内容部分。通过使用React Developer Tools对应用程序进行性能分析,我可以看到Sidebar被渲染了几次-触发了子组件的重新渲染。我已实施React.memo以避免在道具更改时重新渲染。 据我所知,道具没有改变,但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重新呈现 八次 ?会要求一次重新提交吗?

亲切的问候/ K

1 个答案:

答案 0 :(得分:1)

如所评论;当mapStateToProps返回一个新对象时,即使相关值没有变化,它也会重新呈现连接的组件。

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

您的mapStateToProps必须在第二级返回一个新引用,以便使用相同的值重新呈现,因此{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函数。这样,您可以在安装组件时创建一个记忆选择器。可以在列表项中使用它(请参见下面的代码)。

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>