I have a stateless functional component which has no props and populates content from React context. For reference, my app uses NextJS and is an Isomorphic App. I'm trying to use React.memo() for the first time on this component but it keeps re-rendering on client side page change, despite the props and context not changing. I know this due to my placement of a console log.
A brief example of my component is:
const Footer = React.memo(() => {
const globalSettings = useContext(GlobalSettingsContext);
console.log('Should only see this once');
return (
<div>
{globalSettings.footerTitle}
</div>
);
});
I've even tried passing the second parameter with no luck:
const Footer = React.memo(() => {
...
}, () => true);
Any ideas what's going wrong here?
EDIT:
Usage of the context provider in _app.js
looks like this:
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
...
return { globalSettings };
}
render() {
return (
<Container>
<GlobalSettingsProvider settings={this.props.globalSettings}>
...
</GlobalSettingsProvider>
</Container>
);
}
}
The actual GlobalSettingsContext file looks like this:
class GlobalSettingsProvider extends Component {
constructor(props) {
super(props);
const { settings } = this.props;
this.state = { value: settings };
}
render() {
return (
<Provider value={this.state.value}>
{this.props.children}
</Provider>
);
}
}
export default GlobalSettingsContext;
export { GlobalSettingsConsumer, GlobalSettingsProvider };
I have a stateless functional component which has no props and populates content from React context. For reference, my app uses NextJS and is an Isomorphic App. I'm trying to use React.memo() for the first time on this component but it keeps re-rendering on client side page change, despite the props and context not changing. I know this due to my placement of a console log.
A brief example of my component is:
const Footer = React.memo(() => {
const globalSettings = useContext(GlobalSettingsContext);
console.log('Should only see this once');
return (
<div>
{globalSettings.footerTitle}
</div>
);
});
I've even tried passing the second parameter with no luck:
const Footer = React.memo(() => {
...
}, () => true);
Any ideas what's going wrong here?
EDIT:
Usage of the context provider in _app.js
looks like this:
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
...
return { globalSettings };
}
render() {
return (
<Container>
<GlobalSettingsProvider settings={this.props.globalSettings}>
...
</GlobalSettingsProvider>
</Container>
);
}
}
The actual GlobalSettingsContext file looks like this:
class GlobalSettingsProvider extends Component {
constructor(props) {
super(props);
const { settings } = this.props;
this.state = { value: settings };
}
render() {
return (
<Provider value={this.state.value}>
{this.props.children}
</Provider>
);
}
}
export default GlobalSettingsContext;
export { GlobalSettingsConsumer, GlobalSettingsProvider };
Share
Improve this question
edited Feb 19, 2019 at 10:30
CaribouCode
asked Feb 19, 2019 at 10:13
CaribouCodeCaribouCode
14.4k33 gold badges111 silver badges183 bronze badges
7
|
Show 2 more comments
1 Answer
Reset to default 15The problem is coming from useContext
. Whenever any value changes in your context, the component will re-render regardless of whether the value you're using has changed.
The solution is to create a HOC (i.e. withMyContext()
) like so;
// MyContext.jsx
// exported for when you really want to use useContext();
export const MyContext = React.createContext();
// Provides values to the consumer
export function MyContextProvider(props){
const [state, setState] = React.useState();
const [otherValue, setOtherValue] = React.useState();
return <MyContext.Provider value={{state, setState, otherValue, setOtherValue}} {...props} />
}
// HOC that provides the value to the component passed.
export function withMyContext(Component){
<MyContext.Consumer>{(value) => <Component {...value} />}</MyContext.Consumer>
}
// MyComponent.jsx
const MyComponent = ({state}) => {
// do something with state
}
// compares stringified state to determine whether to render or not. This is
// specific to this component because we only care about when state changes,
// not otherValue
const areEqual = ({state:prev}, {state:next}) =>
JSON.stringify(prev) !== JSON.stringify(next)
// wraps the context and memo and will prevent unnecessary
// re-renders when otherValue changes in MyContext.
export default React.memo(withMyContext(MyComponent), areEqual)
Passing context as props instead of using it within render allows us to isolate the changing values we actually care about using areEqual. There's no way to make this comparison during render within useContext
.
I would be a huge advocate for having a selector as a second argument similar to react-redux's new hooks useSelector. This would allow us to do something like
const state = useContext(MyContext, ({state}) => state);
Who's return value would only change when state changes, not the entire context.
But I'm just a dreamer.
This is probably the biggest argument I have right now for using react-redux over hooks for simple apps.
useContext
. reactjs.org/docs/hooks-reference.html#usecontext – bamtheboozle Commented Feb 19, 2019 at 10:17footerTitle
directly as a prop instead of context? – bamtheboozle Commented Feb 19, 2019 at 10:22