Search code examples
reactjsreact-hooksreact-context

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 :

  • The main problem here is a full rerender with every letter typed in the input.
  • The second, is the RightBar toggle button which also causes a full rerender.

Solution

  • 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