Почему компонент React рендерится, если реквизиты не изменились? - PullRequest
1 голос
/ 19 апреля 2020

Я создал приложение на ReactJS 16.8.5 и React-Redux 3.7.2. Когда приложение загружает приложения, устанавливается начальное хранилище, и подписки базы данных настраиваются для базы данных Firebase Realtime. Приложение содержит боковую панель, заголовок и раздел контента. Профилируя приложение с помощью 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 Ответ

1 голос
/ 19 апреля 2020

Как прокомментировано; когда mapStateToProps возвращает новый объект, он будет визуализировать подключенный компонент, даже если соответствующие значения не изменятся.

Это потому, что {} !== {}, объект с такими же параметрами и значениями не равен другому объекту с такими же параметрами и значениями. потому что React сравнивает ссылку на объект, а не значения объекта. Вот почему вы не можете изменить состояние, изменяя его. Мутирование изменяет значения в объекте, но не ссылку на объект.

Ваш mapStateToProps должен возвращать новую ссылку на 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 (
    
      rendered:{rendered.current} times
      props:{JSON.stringify(props)}
); }; const selectVal = (state) => state.val; const selectMapStateToProps = createSelector (selectVal, // создаст этот объект заново только при изменении val (val) => console.log ('val updated) || {mem: {val}}); const memoizedMapStateToProps = selectMapStateToProps; const mapStateToProps = ({val}) => ({nonMem: {val}}); // каждый раз заново создаем props.nonMem const MemoizedConnected = connect (memoizedMapStateToProps) (Component); // этот mapStateToProps создаст реквизиты из {val: 1} // чистые компоненты (возвращаемые connect) будут сравнивать каждое свойство // объекта реквизита, а не реквизит в целом. Поскольку props.val // никогда не менялся между рендерами, он не рендерится const OneLevelConnect = connect (({val}) => ({val})) (Component); const Connected = connect (mapStateToProps) (Компонент); const Pure = memo (function Pure () {// реквизиты никогда не меняются, поэтому они будут отображаться только один раз: console.log («реквизиты никогда не меняются, поэтому не рендерится Pure»); return (
); }); const App = () => {const dispatch = useDispatch (); useEffect (// отправлять действие каждую секунду, это создаст новое // состояние ref, но state.val никогда не изменится () => {setInterval (() => dispatch ({type: 88}), 1000);}, [рассылка] // рассылка никогда не меняется, но инструменты для раскрашивания этого не знают); возвращение ; }; ReactDOM.render ( , 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 (
    
      rendered:{rendered.current} times
      props:{JSON.stringify(props)}
); }; // оптимизированный 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 (// отправлять действие каждую секунду, это создаст новое // состояние ref, но state.val никогда не изменится () => {setInterval (() => dispatch ({type: 88}), 1000);}, [рассылка] // рассылка никогда не меняется, но инструменты для раскрашивания этого не знают); return (

приложение отображено {rendered.current} раз

Подключенное лицо (будет отображаться повторно)

Оптимизированный Связанный человек (не будет визуализироваться)

); }; ReactDOM.render ( , 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>
...