Dwix
Dwix

Reputation: 1257

Memoize a function in parent before passing it to children to avoid rerender React

I'm building a chat app, I have 3 main components from parent to child in this hierarchical order: Chat.js, ChatLine.js, EditMessage.js. I have a function updateMessage in Chat component that I need to pass to the second child EditMessage, but it causes a rerender of every ChatLine when I click on Edit button and begin typing. I can't figure out how to memoize it so it only causes a rerender on the ChatLine I'm editing.

It only works if I pass it to ChatLine as :

updateMessage={line.id === editingId ? updateMessage : null}

instead of :

updateMessage={updateMessage}

But I want to avoid that and memoize it instead so it doesn't cause a rerender after each letter I type while editing a message.

This is the whole code: (also available in CodeSandbox & Netlify)

(Chat.js)

import { useEffect, useState } from "react";
import ChatLine from "./ChatLine";

const Chat = () => {
  const [messages, setMessages] = useState([]);
  const [editValue, setEditValue] = useState("");
  const [editingId, setEditingId] = useState(null);

  useEffect(() => {
    setMessages([
      { id: 1, message: "Hello" },
      { id: 2, message: "Hi" },
      { id: 3, message: "Bye" },
      { id: 4, message: "Wait" },
      { id: 5, message: "No" },
      { id: 6, message: "Ok" },
    ]);
  }, []);

  const updateMessage = (editValue, setEditValue, editingId, setEditingId) => {
    const message = editValue;
    const id = editingId;

    // resetting state as soon as we press Enter
    setEditValue("");
    setEditingId(null);

    // ajax call to update message in DB using `message` & `id` variables
    console.log("updating..");
  };

  return (
    <div>
      <p>MESSAGES :</p>
      {messages.map((line) => (
        <ChatLine
          key={line.id}
          line={line}
          editValue={line.id === editingId ? editValue : ""}
          setEditValue={setEditValue}
          editingId={editingId}
          setEditingId={setEditingId}
          updateMessage={updateMessage}
        />
      ))}
    </div>
  );
};

export default Chat;

(ChatLine.js)

import EditMessage from "./EditMessage";
import { memo } from "react";

const ChatLine = ({
  line,
  editValue,
  setEditValue,
  editingId,
  setEditingId,
  updateMessage,
}) => {
  return (
    <div>
      {editingId !== line.id ? (
        <>
          <span>{line.id}: </span>
          <span>{line.message}</span>
          <button
            onClick={() => {
              setEditingId(line.id);
              setEditValue(line.message);
            }}
          >
            EDIT
          </button>
        </>
      ) : (
        <EditMessage
          editValue={editValue}
          setEditValue={setEditValue}
          setEditingId={setEditingId}
          editingId={editingId}
          updateMessage={updateMessage}
        />
      )}
    </div>
  );
};

export default memo(ChatLine);

(EditMessage.js)

import { memo } from "react";

const EditMessage = ({
  editValue,
  setEditValue,
  editingId,
  setEditingId,
  updateMessage,
}) => {
  return (
    <div>
      <textarea
        onKeyPress={(e) => {
          if (e.key === "Enter") {
            // prevent textarea default behaviour (line break on Enter)
            e.preventDefault();
            // updating message in DB
            updateMessage(editValue, setEditValue, editingId, setEditingId);
          }
        }}
        onChange={(e) => setEditValue(e.target.value)}
        value={editValue}
        autoFocus
      />
      <button
        onClick={() => {
          setEditingId(null);
          setEditValue("");
        }}
      >
        CANCEL
      </button>
    </div>
  );
};

export default memo(EditMessage);

Upvotes: 1

Views: 386

Answers (1)

Drew Reese
Drew Reese

Reputation: 202836

Use the useCallback React hook to memoize the updateMessage callback so it can be passed as a stable reference. The issue is that each time Chat is rendered when editValue state updates it is redeclaring the updateMessage function so it's a new reference and triggers each child component it's passed to to rerender.

import { useCallback } from 'react';

...

const updateMessage = useCallback(
  (editValue, setEditValue, editingId, setEditingId) => {
    const message = editValue;
    const id = editingId;

    // resetting state as soon as we press Enter
    setEditValue("");
    setEditingId(null);

    // ajax call to update message in DB using `message` & `id` variables
    console.log("updating..");

    // If updating messages state use functional state update to
    // avoid external dependencies.
  },
  []
);

Edit memoize-a-function-in-parent-before-passing-it-to-children-to-avoid-rerender-rea

Upvotes: 2

Related Questions