React context with hooks prevent re render
Asked Answered
D

4

28

I use React context with hooks as a state manager for my React app. Every time the value changes in the store, all the components re-render.

Is there any way to prevent React component to re-render?

Store config:

import React, { useReducer } from "react";
import rootReducer from "./reducers/rootReducer";

export const ApiContext = React.createContext();

export const Provider = ({ children }) => {
  const [state, dispatch] = useReducer(rootReducer, {});

  return (
    <ApiContext.Provider value={{ ...state, dispatch }}>
      {children}
    </ApiContext.Provider>
  );
};

An example of a reducer:

import * as types from "./../actionTypes";

const initialState = {
  fetchedBooks: null
};

const bookReducer = (state = initialState, action) => {
  switch (action.type) {
    case types.GET_BOOKS:
      return { ...state, fetchedBooks: action.payload };

    default:
      return state;
  }
};

export default bookReducer;

Root reducer, that can combine as many reducers, as possible:

import userReducer from "./userReducer";
import bookReducer from "./bookReducer";

const rootReducer = ({ users, books }, action) => ({
  users: userReducer(users, action),
  books: bookReducer(books, action)
});

An example of an action:

import * as types from "../actionTypes";

export const getBooks = async dispatch => {
  const response = await fetch("https://jsonplaceholder.typicode.com/todos/1", {
    method: "GET"
  });

  const payload = await response.json();

  dispatch({
    type: types.GET_BOOKS,
    payload
  });
};
export default rootReducer;

And here's the book component:

import React, { useContext, useEffect } from "react";
import { ApiContext } from "../../store/StoreProvider";
import { getBooks } from "../../store/actions/bookActions";

const Books = () => {
  const { dispatch, books } = useContext(ApiContext);
  const contextValue = useContext(ApiContext);

  useEffect(() => {
    setTimeout(() => {
      getBooks(dispatch);
    }, 1000);
  }, [dispatch]);

  console.log(contextValue);

  return (
    <ApiContext.Consumer>
      {value =>
        value.books ? (
          <div>
            {value.books &&
              value.books.fetchedBooks &&
              value.books.fetchedBooks.title}
          </div>
        ) : (
          <div>Loading...</div>
        )
      }
    </ApiContext.Consumer>
  );
};

export default Books;

When the value changes in Books component, another my component Users re-renders:

import React, { useContext, useEffect } from "react";
import { ApiContext } from "../../store/StoreProvider";
import { getUsers } from "../../store/actions/userActions";

const Users = () => {
  const { dispatch, users } = useContext(ApiContext);
  const contextValue = useContext(ApiContext);

  useEffect(() => {
    getUsers(true, dispatch);
  }, [dispatch]);

  console.log(contextValue, "Value from store");

  return <div>Users</div>;
};

export default Users;

What's the best way to optimize context re-renders? Thanks in advance!

Dockhand answered 14/7, 2019 at 18:20 Comment(3)
Do you have a CodeSandbox that demonstrates this?Boilermaker
It seems that you created your own redux with hooks + context :)Benner
What makes you say other components are re-rendering? How can you tell? It looks like what is happening is normal - each time you change routes, the Nav Links get rerendered.. is that what you're referring to?Boilermaker
B
3

I believe what is happening here is expected behavior. The reason it renders twice is because you are automatically grabbing a new book/user when you visit the book or user page respectively.

This happens because the page loads, then useEffect kicks off and grabs a book or user, then the page needs to re-render in order to put the newly grabbed book or user into the DOM.

I have modified your CodePen in order to show that this is the case.. If you disable 'autoload' on the book or user page (I added a button for this), then browse off that page, then browse back to that page, you will see it only renders once.

I have also added a button which allows you to grab a new book or user on demand... this is to show how only the page which you are on gets re-rendered.

All in all, this is expected behavior, to my knowledge.

Edit react-state-manager-hooks-context

Boilermaker answered 14/7, 2019 at 23:26 Comment(0)
R
26

Books and Users currently re-render on every cycle - not only in case of store value changes.

1. Prop and state changes

React re-renders the whole sub component tree starting with the component as root, where a change in props or state has happened. You change parent state by getUsers, so Books and Users re-render.

const App = () => {
  const [state, dispatch] = React.useReducer(
    state => ({
      count: state.count + 1
    }),
    { count: 0 }
  );

  return (
    <div>
      <Child />
      <button onClick={dispatch}>Increment</button>
      <p>
        Click the button! Child will be re-rendered on every state change, while
        not receiving any props (see console.log).
      </p>
    </div>
  );
}

const Child = () => {
  console.log("render Child");
  return "Hello Child ";
};


ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>

Optimization technique

Use React.memo to prevent a re-render of a comp, if its own props haven't actually changed.

// prevents Child re-render, when the button in above snippet is clicked
const Child = React.memo(() => {
  return "Hello Child ";
});
// equivalent to `PureComponent` or custom `shouldComponentUpdate` of class comps

Important: React.memo only checks prop changes (useContext value changes trigger re-render)!


2. Context changes

All context consumers (useContext) are automatically re-rendered, when the context value changes.

// here object reference is always a new object literal = re-render every cycle
<ApiContext.Provider value={{ ...state, dispatch }}>
  {children}
</ApiContext.Provider>

Optimization technique

Make sure to have stable object references for the context value, e.g. by useMemo Hook.

const [state, dispatch] = useReducer(rootReducer, {});
const store = React.useMemo(() => ({ state, dispatch }), [state])

<ApiContext.Provider value={store}>
  {children}
</ApiContext.Provider>

Other

Not sure, why you put all these constructs together in Books, just use one useContext:

const { dispatch, books } = useContext(ApiContext);
// drop these
const contextValue = useContext(ApiContext); 
<ApiContext.Consumer> /* ... */ </ApiContext.Consumer>; 

You also can have a look at this code example using both React.memo and useContext.

Rudnick answered 14/4, 2020 at 14:44 Comment(0)
B
3

I believe what is happening here is expected behavior. The reason it renders twice is because you are automatically grabbing a new book/user when you visit the book or user page respectively.

This happens because the page loads, then useEffect kicks off and grabs a book or user, then the page needs to re-render in order to put the newly grabbed book or user into the DOM.

I have modified your CodePen in order to show that this is the case.. If you disable 'autoload' on the book or user page (I added a button for this), then browse off that page, then browse back to that page, you will see it only renders once.

I have also added a button which allows you to grab a new book or user on demand... this is to show how only the page which you are on gets re-rendered.

All in all, this is expected behavior, to my knowledge.

Edit react-state-manager-hooks-context

Boilermaker answered 14/7, 2019 at 23:26 Comment(0)
C
2

I tried to explain with different example hope that will help.

Because context uses reference identity to determine when to re-render, that could trigger unintentional renders in consumers when a provider’s parent re-renders.

for example: code below will re-render all consumers every time the Provider re-renders because a new object is always created for value

class App extends React.Component {
  render() {
   return (
      <Provider value={{something: 'something'}}>
        <Toolbar />
      </Provider>
    );
 }
}

To get around this, lift the value into the parent’s state

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'something'},
    };
  }

  render() {
    return (
      <Provider value={this.state.value}>
        <Toolbar />
      </Provider>
    );
  }
}
Catherin answered 14/7, 2019 at 19:2 Comment(3)
So, I need to create a child component to display books, for example. I get data from back-end in Books component and pass it to BooksItem and it will prevent re-render, right?Dockhand
But since state changed in the parent, wouldnt the child components re render anyway?Melitamelitopol
you deserve a bounty, in my case it was solved using, useMemo to create the value objectMaryleemarylin
H
0

This solution is used to prevent a component from rendering in React is called shouldComponentUpdate. It is a lifecycle method which is available on React class components. Instead of having Square as a functional stateless component as before:

const Square = ({ number }) => <Item>{number * number}</Item>;

You can use a class component with a componentShouldUpdate method:

class Square extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    ...
  }

  render() {
    return <Item>{this.props.number * this.props.number}</Item>;
  }
}

As you can see, the shouldComponentUpdate class method has access to the next props and state before running the re-rendering of a component. That’s where you can decide to prevent the re-render by returning false from this method. If you return true, the component re-renders.

class Square extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.number === nextProps.number) {
      return false;
    } else {
      return true;
    }
  }

  render() {
    return <Item>{this.props.number * this.props.number}</Item>;
  }
}

In this case, if the incoming number prop didn’t change, the component should not update. Try it yourself by adding console logs again to your components. The Square component shouldn’t rerender when the perspective changes. That’s a huge performance boost for your React application because all your child components don’t rerender with every rerender of their parent component. Finally, it’s up to you to prevent a rerender of a component.

Understanding this componentShouldUpdate method will surely help you out!

Hett answered 14/7, 2019 at 19:40 Comment(1)
Thanks! But I use hooks, so I'll try memoDockhand

© 2022 - 2024 — McMap. All rights reserved.