使用 react-redux、reselect 和 React.memo() 来记忆功能组件

Posted

技术标签:

【中文标题】使用 react-redux、reselect 和 React.memo() 来记忆功能组件【英文标题】:Memoize functional component using react-redux, reselect and React.memo() 【发布时间】:2020-08-07 20:32:46 【问题描述】:

我在 ReactJS 16.8.5 和 React-Redux 3.7.2 上构建了一个应用程序。当应用加载应用装载时,会设置初始存储并针对 Firebase 实时数据库设置数据库订阅。 该应用程序包含标题 Sidebar 和内容部分。 我已经实现了reselect 和React.memo 以避免在道具更改时重新渲染,但是Sidebar 组件仍在重新渲染。 在 React.memo 中使用 React profiler API 和 areEqual 比较函数,我可以看到 Sidebar 被渲染了多次,尽管 props 是相等的。

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

后续渲染不会影响 store 中映射到 props(位置)的任何部分,但组件仍在重新渲染。

Console output - subsequent render

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

我希望 Sidebar 被记忆,并且在初始加载期间挂载/更新存储期间仅渲染/重新渲染几次。

为什么Sidebar 组件被渲染了这么多次?

亲切的问候 /K

【问题讨论】:

【参考方案1】:

不需要 React.memo,因为 react-redux connect 将返回一个纯组件,仅当您更改传递的 props 或在调度的操作导致状态发生任何变化后才会重新渲染。

您的 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>

【讨论】:

非常感谢 HMR!我有点困惑我们各自的“mapStateToPropsMemoized”之间的功能差异。你能详细说明一下吗?非常感谢! /K @Kermit 更新了答案。当您说道具永远不会改变时,组件不应该重新渲染,请参阅我更新的答案,其中包含示例代码,显示当来自重新选择的选择器的状态值没有改变时,选择器返回记忆的结果。 非常感谢@HMR 的澄清! (^__^)/ /K

以上是关于使用 react-redux、reselect 和 React.memo() 来记忆功能组件的主要内容,如果未能解决你的问题,请参考以下文章

MR小区搜索cell reselection

typescript RX-reselect.ts

typescript RX-reselect3.ts

typescript RX-reselect2.ts

使用 React-Redux Hooks 和 React-Redux Connect() 的主要区别是啥?

react 中的 redux 和react-redux的区别分析