Dwix
Dwix

Reputation: 1257

Excessive rerendering when interacting with global state in React Context

I'm building a Chat app, I'm using ContextAPI to hold the state that I'll be needing to access from different unrelated components.

A lot of rerendering is happening because of the context, everytime I type a letter in the input all the components rerender, same when I toggle the RightBar which its state also resides in the context because I need to toggle it from a button in Navbar.

I tried to use memo on every components, still all the components rerender everytime I interact with state in context from any component.

I added my whole code simplified to this sandbox link : https://codesandbox.io/s/interesting-sky-fzmc6

And this is a deployed Netlify link : https://csb-fzmc6.netlify.app/

I tried to separate my code into some custom hooks like useChatSerice, useUsersService to simplify the code and make the actual components clean, I'll also appreciate any insight about how to better structure those hooks and where to put CRUD functions while avoiding the excessive rerendering.

I found some "solutions" indicating that using multiple contexts should help, but I can't figure out how to do this in my specific case, been stuck with this problem for a week.

EDIT :

Upvotes: 0

Views: 1913

Answers (3)

Drew Reese
Drew Reese

Reputation: 202874

Splitting the navbar and chat state into two separate React contexts is actually the recommended method from React. By nesting all the state into a new object reference anytime any single state updated it necessarily triggers a rerender of all consumers.

<ChatContext.Provider
  value={{ // <-- new object reference each render
    rightBarValue: [rightBarIsOpen, setRightBarIsOpen],
    chatState: {
      editValue,
      setEditValue,
      editingId,
      setEditingId,
      inputValue,
      setInputValue,
    },
  }}
>
  {children}
</ChatContext.Provider>

I suggest carving rightBarValue and state setter into its own context.

NavBar context

const NavBarContext = createContext([false, () => {}]);

const NavBarProvider = ({ children }) => {
  const [rightBarIsOpen, setRightBarIsOpen] = useState(true);
  return (
    <NavBarContext.Provider value={[rightBarIsOpen, setRightBarIsOpen]}>
      {children}
    </NavBarContext.Provider>
  );
};

const useNavBar = () => useContext(NavBarContext);

Chat context

const ChatContext = createContext({
  editValue: "",
  setEditValue: () => {},
  editingId: null,
  setEditingId: () => {},
  inputValue: "",
  setInputValue: () => {}
});

const ChatProvider = ({ children }) => {
  const [inputValue, setInputValue] = useState("");
  const [editValue, setEditValue] = useState("");
  const [editingId, setEditingId] = useState(null);

  const chatState = useMemo(
    () => ({
      editValue,
      setEditValue,
      editingId,
      setEditingId,
      inputValue,
      setInputValue
    }),
    [editValue, inputValue, editingId]
  );

  return (
    <ChatContext.Provider value={chatState}>{children}</ChatContext.Provider>
  );
};

const useChat = () => {
  return useContext(ChatContext);
};

MainContainer

const MainContainer = () => {
  return (
    <ChatProvider>
      <NavBarProvider>
        <Container>
          <NavBar />
          <ChatSection />
        </Container>
      </NavBarProvider>
    </ChatProvider>
  );
};

NavBar - use the useNavBar hook

const NavBar = () => {
  const [rightBarIsOpen, setRightBarIsOpen] = useNavBar();

  useEffect(() => {
    console.log("NavBar rendered"); // <-- log when rendered
  });

  return (
    <NavBarContainer>
      <span>MY NAVBAR</span>
      <button onClick={() => setRightBarIsOpen(!rightBarIsOpen)}>
        TOGGLE RIGHT-BAR
      </button>
    </NavBarContainer>
  );
};

Chat

const Chat = ({ chatLines }) => {
  const { addMessage, updateMessage, deleteMessage } = useChatService();
  const {
    editValue,
    setEditValue,
    editingId,
    setEditingId,
    inputValue,
    setInputValue
  } = useChat();

  useEffect(() => {
    console.log("Chat rendered"); // <-- log when rendered
  });

  return (
    ...
  );
};

When running the app notice now that "NavBar rendered" only logs when toggling the navbar, and "Chat rendered" only logs when typing in the chat text area.

Edit excessive-rerendering-when-interacting-with-global-state-in-react-context

Upvotes: 2

hammelion
hammelion

Reputation: 351

It looks like you are changing a global context on input field data change. If your global context is defined on a level of parent components (in relation to your input component), then the parent and all children will have to re-render. You have several options to avoid this behavior:

  1. Use context on a lower level, e.g. by extracting your input field to an external component and using useContext hook there
  2. Save the input to local state of a component and only sync it to the global context on blur or submit

Upvotes: 1

fixiabis
fixiabis

Reputation: 373

I recommend use jotai or other state management libraries.
Context is not suitable for high-frequency changes.
And, the RightBar's state looks can separate to other hook/context.

There is tricky one solution solve some render problems: https://codesandbox.io/s/stoic-mclaren-x6yfv?file=/src/context/ChatContext.js

Your code needs to be refactored, and useChatService in ChatSection also depends on your useChat, so ChatSection will re-render when the text changes.

Upvotes: 1

Related Questions