使用 react-redux、重新选择和 React.memo() 记忆功能组件
Memoize functional component using react-redux, reselect and React.memo()
我已经在 ReactJS 16.8.5 和 React-Redux 3.7.2 上构建了一个应用程序。当应用程序加载应用程序安装时,会设置初始存储并针对 Firebase 实时数据库设置数据库订阅。
该应用包含 header、Sidebar
和内容部分。
我已经实现了 reselect along with React.memo 以避免在 props 更改时重新渲染,但是 Sidebar
组件仍然是 re-rendering。
在 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
之前看起来不错 - 之后组件 re-renders 很多次 - 为什么!?
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
后续渲染不会影响映射到道具(位置)的商店的任何部分,但组件仍然是 re-rendering。
Console output - subsequent render
Profile Sidebar areEqual true
onRender Sidebar update 76
onRender Sidebar update 4
我希望 Sidebar
被记忆,并且在初始加载期间 mount/update 期间只有 render/re-render 几次。
为什么 Sidebar
组件被渲染了这么多次?
亲切的问候/K
不需要 React.memo 因为 react-redux connect 将 return 一个纯组件,只有当你改变传递的 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 returned 的对象每次都是一个新对象,但它的直接属性永远不会改变,因为选择器总是 return 记住结果。请参阅下面的示例
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>
我已经在 ReactJS 16.8.5 和 React-Redux 3.7.2 上构建了一个应用程序。当应用程序加载应用程序安装时,会设置初始存储并针对 Firebase 实时数据库设置数据库订阅。
该应用包含 header、Sidebar
和内容部分。
我已经实现了 reselect along with React.memo 以避免在 props 更改时重新渲染,但是 Sidebar
组件仍然是 re-rendering。
在 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
之前看起来不错 - 之后组件 re-renders 很多次 - 为什么!?
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
后续渲染不会影响映射到道具(位置)的商店的任何部分,但组件仍然是 re-rendering。
Console output - subsequent render
Profile Sidebar areEqual true
onRender Sidebar update 76
onRender Sidebar update 4
我希望 Sidebar
被记忆,并且在初始加载期间 mount/update 期间只有 render/re-render 几次。
为什么 Sidebar
组件被渲染了这么多次?
亲切的问候/K
不需要 React.memo 因为 react-redux connect 将 return 一个纯组件,只有当你改变传递的 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 returned 的对象每次都是一个新对象,但它的直接属性永远不会改变,因为选择器总是 return 记住结果。请参阅下面的示例
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>