问题
I have built an app on ReactJS 16.8.5 and React-Redux 3.7.2. When the app loads the app mounts, initial store is set and database subscriptions are set up against a Firebase Realtime Database.
The app contains a header, Sidebar
and content section.
I have implemented reselect along with React.memo to avoid rerendring when props change, but the Sidebar
component is still re-rendering.
Using React profiler API and a areEqual
comparison function in React.memo I can see that the Sidebar
is being rendered several times although props are equal.
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)
Initial render looks ok up until Sidebar navigation val changed
- after that the component re-renders a whole bunch of times - why!?
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
The subsequent render does not affect any part of the store that is mapped to props (location), but component is still re-rendering.
Console output - subsequent render
Profile Sidebar areEqual true
onRender Sidebar update 76
onRender Sidebar update 4
I expect Sidebar
to be memoized and only render/re-render a few times during mount/update of store during initial load.
Why is the Sidebar
component being rendered so many times?
Kind regards /K
回答1:
The React.memo is not needed because react-redux connect will return a pure component that will only re render if you change the props passed or after a dispatched action caused any changes in the state.
Your mapStateToPropsMemoized
should work (see update) but probalby better to write it this way:
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);
UPDATE
Your getState should work.
I cannot reproduce component re rendering with your code. The object returned from mapState is a new object every time but it's direct properties never change because the selectors always return memoized result. See example below
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>
来源:https://stackoverflow.com/questions/61405482/memoize-functional-component-using-react-redux-reselect-and-react-memo