Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React memo keeps rendering when props have not changed

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 };
like image 771
CaribouCode Avatar asked Feb 19 '19 10:02

CaribouCode


People also ask

Does React re-render if props don't change?

⛔️ Re-renders reason: props changes (the big myth)It doesn't matter whether the component's props change or not when talking about re-renders of not memoized components. In order for props to change, they need to be updated by the parent component.

How do we prevent unnecessary re-renders even when props remain the same?

memo Using a Single Component as its Only Parameter. We saw earlier how a React component re-renders even when the props have not changed. For instance, when a parent component renders, it causes the child component to render as well. To avoid this behavior, implement React.

How do I stop component from re-render when props changes?

If you don't want a component to re-render when its parent renders, wrap it with memo. After that, the component indeed will only re-render when its props change.


1 Answers

The 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.

like image 199
Harley Alexander Avatar answered Oct 06 '22 00:10

Harley Alexander