Функциональный компонент Memoize с помощью react-redux, Reselect и React.memo ()

Я создал приложение на ReactJS 16.8.5 и React-Redux 3.7.2. Когда приложение загружает приложение, монтируется, устанавливается начальное хранилище и настраиваются подписки на базу данных для базы данных Firebase Realtime. Приложение содержит заголовок, Sidebar и раздел содержимого.
Я реализовал повторный выбор вместе с React.memo, чтобы избежать повторного отображения при изменении свойств, но Sidebar компонент все еще перерисовывается. Используя API профилировщика React и функцию сравнения areEqual в 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 ...props />
          </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('onRender', phase, actualDuration)
  }
  return (
    <Profiler id="Sidebar" onRender={onRender}>
      <React.Fragment>
        {/* Contents of Sidebar */}
      </React.Fragment>
    </Profiler>
}

const getLang = state => (state.usersettings) ? state.usersettings.language : 'en'
const getMediaSize = state => (state.route) ? state.route.mediaSize : 'large'
const getNavigation = state => state.navigation
const getMyLang = createSelector(
  [getLang], (lang) => console.log('Sidebar lang val changed') || lang
)
const getMyMediaSize = createSelector(
  [getMediaSize], (mediaSize) => console.log('Sidebar mediaSize val changed') || mediaSize
)
const getMyNavigation = createSelector(
  [getNavigation], (navigation) => console.log('Sidebar navigation val changed') || navigation
)
const mapStateToPropsMemoized = (state) => {
  return {
    lang: getMyLang(state),
    mediaSize: getMyMediaSize(state),
    navigation: getMyNavigation(state)
  }
}

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

Первоначальный рендеринг выглядит нормально до Sidebar navigation val changed - после этого компонент перерисовывает много раз - почему !?

Console output - initial render

onRender Sidebar mount 572 
Sidebar mediaSize val changed 
Profile Sidebar areEqual true 
Sidebar navigation val changed 
onRender Sidebar update 153 
Sidebar navigation val changed 
onRender Sidebar update 142 
onRender Sidebar update 103 
onRender Sidebar update 49 
onRender Sidebar update 5 
onRender Sidebar update 2 
onRender Sidebar update 12 
onRender Sidebar update 3 
onRender Sidebar update 2 
onRender Sidebar update 58 
onRender Sidebar update 2 
onRender Sidebar update 4 
onRender Sidebar update 5 
onRender Sidebar update 4

Последующий рендеринг не влияет на какую-либо часть хранилища, сопоставленную с реквизитами (местоположением), но компонент по-прежнему выполняет повторный рендеринг.

Console output - subsequent render

Profile Sidebar areEqual true
onRender Sidebar update 76
onRender Sidebar update 4

Я ожидаю, что Sidebar будет мемоизирован и будет отображать / повторно отображать только несколько раз во время монтирования / обновления хранилища во время начальной загрузки.

Почему компонент Sidebar визуализируется так много раз?

С уважением / K


person Kermit    schedule 24.04.2020    source источник


Ответы (1)


React.memo не нужен, потому что response-redux connect вернет чистый компонент, который будет повторно отрисовываться только в том случае, если вы измените переданные реквизиты или после того, как отправленное действие вызвало какие-либо изменения в состоянии.

Ваш mapStateToPropsMemoized должен работать (см. Обновление), но, возможно, лучше написать его так:

const mapStateToPropsMemoized = createSelector(
  getMyLang,
  getMyMediaSize,
  getMyNavigation,
  (lang, mediaSize, navigation) => ({
    lang,
    mediaSize,
    navigation,
  })
);
//using react.redux connect will return a pure component and passing that
//  to React.memo should cause an error because connect does not return a
//  functional component.
export default connect(
  mapStateToPropsMemoized,
  mapDispatchToProps
)(Sidebar);

ОБНОВЛЕНИЕ

Ваш getState должен работать.

Я не могу воспроизвести повторный рендеринг компонента с помощью вашего кода. Объект, возвращаемый из mapState, является новым объектом каждый раз, но его прямые свойства никогда не меняются, потому что селекторы всегда возвращают мемоизированный результат. См. Пример ниже

const { useRef, useEffect } = React;
const {
  Provider,
  useDispatch,
  connect,
  useSelector,
} = ReactRedux;
const { createStore } = Redux;
const { createSelector } = Reselect;
const state = { someValue: 2, unrelatedCounter: 0 };
//returning a new state every action someValue
//  never changes, only counter
const reducer = (state) => ({
  ...state,
  unrelatedCounter: state.unrelatedCounter + 1,
});
const store = createStore(
  reducer,
  { ...state },
  window.__REDUX_DEVTOOLS_EXTENSION__ &&
    window.__REDUX_DEVTOOLS_EXTENSION__()
);
//selectors
const selectSomeValue = (state) => state.someValue;
//selectors only return a new object if someValue changes
const selectA = createSelector(
  [selectSomeValue],
  () => ({ value: 'A' }) //returns new object if some value changes
);
const selectB = createSelector(
  [selectSomeValue],
  () => ({ vale: 'B' }) //returns new object if some value changes
);
const selectC = createSelector(
  [selectSomeValue],
  () => ({ vale: 'C' }) //returns new object if some value changes
);
const Counter = () => {
  const counter = useSelector(
    (state) => state.unrelatedCounter
  );
  return <h4>Counter: {counter}</h4>;
};
const AppComponent = (props) => {
  const dispatch = useDispatch();
  const r = useRef(0);
  //because state.someValue never changes this component
  //  never gets re rendered
  r.current++;
  useEffect(
    //dispatch an action every second, this will create a new
    //  state but state.someValue never changes
    () => {
      setInterval(() => dispatch({ type: 88 }), 1000);
    },
    [dispatch] //dispatch never changes but linting tools don't know that
  );
  return (
    <div>
      <h1>Rendered {r.current} times</h1>
      <Counter />
      <pre>{JSON.stringify(props, undefined, 2)}</pre>
    </div>
  );
};
const mapStateToProps = (state) => {
  return {
    A: selectA(state),
    B: selectB(state),
    C: selectC(state),
  };
};

const App = connect(mapStateToProps)(AppComponent);
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>

person HMR    schedule 01.06.2020
comment
Большое спасибо HMR! Я несколько озадачен, в чем функциональная разница между нашим соответствующим mapStateToPropsMemoized. Не могли бы вы уточнить? Большое спасибо! / К - person Kermit; 02.06.2020
comment
@Kermit обновил ansewr. когда вы говорите, что реквизиты никогда не меняются, компонент не должен повторно отображаться, см. мой обновленный ответ с примером кода, показывающим, что когда значение состояния, которое селектор из повторного выбора не изменяется, селектор возвращает мемоизированный результат. - person HMR; 02.06.2020
comment
Большое спасибо @HMR за разъяснения! (^ __ ^) / / К - person Kermit; 02.06.2020