Home > OS >  Only rerender the affected child in list of components while state resides in parent React
Only rerender the affected child in list of components while state resides in parent React

Time:12-11

I'm building a chat app, I have 3 components from parent to child in this hierarchical order: Chat, ChatLine, EditMessage. I'm looping through messages state in Chat to display multiple ChatLine components as a list, and I pass some state to ChatLine and then to EditMessage.

I need the state :

const [editValue, setEditValue] = useState("");
const [editingId, setEditingId] = useState(null);

to remain in the parent component Chat so I can have access to it later there.

Anyway, now when I click on the Edit button, the EditMessage component shows a textarea, and I'm setting state onChange in it, but everytime I click the Edit button or type a letter in the textarea all the components rerender as I see in React DevTool Profiler, even the children that didn't get affected, I only need the Chat and affected ChatLine to rerender at most.

The whole code is available in CodeSandbox, and deployed in Netlify.

And here it is here also :

(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" },
    ]);
  }, []);

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

export default Chat;

(ChatLine.js)

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

const ChatLine = ({
  line,
  editValue,
  setEditValue,
  editingId,
  setEditingId,
}) => {
  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}
        />
      )}
    </div>
  );
};

export default memo(ChatLine);

(EditMessage.js)

import { memo } from "react";

const EditMessage = ({ editValue, setEditValue, editingId, setEditingId }) => {
  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);

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..");
};

CodePudding user response:

The problem is that all of the child components see their props change any time any of them is in the process of being edited, because you're passing the current editing information to all of the children. Instead, only pass the current editing text (editValue) to the component being edited, not to all the others.

ChatLine doesn't use editValue when it's not the instance being edited. So I'd do one of two things:

  1. Use a different component for display (ChatLine) vs. edit (ChatLineEdit). Almost the entire body of ChatLine is different depending on whether that line is being edited or not anyway. Then only pass editValue to ChatLineEdit.

  2. Pass "" (or similar) as editValue to the one not being edited. In the map in Chat: editValue={line.id === editingId ? editValue : ""}.

  3. Pass an "are equal" function into memo for ChatLine that doesn't care what the value of editValue is if line.id !== editingId. By default, memo does a shallow check of all props, but you can take control of that process by providing a function as the second argument. For instance:

    export default memo(ChatLine, (prevProps, nextProps) => {
        // "Equal" for rendering purposes?
        return (
            // Same chat line
            prevProps.line === nextProps.line &&
            // Same edit value setter (you can leave this out, setters from `useState` never change)
            prevProps.setEditValue === prevProps.setEditValue && // ***
            // Same editingId
            prevProps.editingId === prevProps.editingId &&
            // Same editingId setter (you can leave this out too)
            prevProps.setEditingId === prevProps.setEditingId && // ***
            (
                // Same edit value...
                prevProps.editValue === prevProps.editValue ||
                // OR, we don't care because we're not being edited
                nextProps.line.id !== nextProps.editingId
            )
        );
    });
    

    This is fragile, because it's easy to get the check wrong, but it's another option.

I would go for #1. Not even passing props to components that they don't need is (IMHO) the cleanest approach.

  • Related