为什么在道具未更改的情况下重新渲染React组件?

问题描述 投票:0回答:1

我已经在ReactJS 16.8.5和React-Redux 3.7.2上构建了一个应用程序。当应用程序加载应用程序挂载时,将设置初始存储,并针对Firebase实时数据库设置数据库订阅。该应用程序包含侧边栏,标题和内容部分。通过使用React Developer Tools对应用程序进行性能分析,我可以看到Sidebar被渲染了几次-触发了子组件的重新渲染。我已实施React.memo以避免在道具更改时重新渲染。从我可以看到的道具没有改变,但Sidebar仍然rerenders,使我感到困惑。

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

reactjs redux rerender
1个回答
0
投票

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

这是因为{} !== {},一个具有相同属性和值的对象不等于另一个具有相同属性和值的对象,因为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 }) =>
  console.log('every time') || { nonMem: { val } }; //re creates object every time
const MemoizedConnected = connect(memoizedMapStateToProps)(
  Component
);
//this mapStateToProps will create a props of {val:1} every time
//  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();
  //useSelector will cause app to re render because selecting
  //  all state
  const allState = useSelector((state) => state);
  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>
© www.soinside.com 2019 - 2024. All rights reserved.