Empresas
Empregos
  • Sobre nós
  • Soluções
    • Publicação de vagas
      Publique sua vaga e receba candidatos qualificados em 48h.
    • Avaliações de candidatos
      Mais de 500 testes técnicos e psicológicos, mais anti-fraude.
    • Headhunting
      Busca executiva personalizada do início ao fim.
    • Folha de Pagamento + EOR
      Dispersão de folha e EOR em mais de 15 países da LATAM.
  • Preços
  • Empregos

0

153
Visualizações
Avoid rerendering every component in list while updating only one in React

I have a simple chat app using Firebase v9, with these components from parent to child in this hierarchical order: ChatSection, Chat, ChatLine, EditMessage.

I have a custom hook named useChatService holding the list of messages in state, the hook is called in ChatSection, the hook returns the messages and I pass them from ChatSection in a prop to Chat, then I loop through messages and create a ChatLine component for every message.

I can click the Edit button in front of each message, it shows the EditMessage component so I can edit the text, then when I press "Enter", the function updateMessage gets executed and updates the message in the db, but then every single ChatLine gets rerendered again, which is a problem as the list gets bigger.

EDIT 2: I've completed the code to make a working example with Firebase v9 so you can visualize the rerenders I'm talking about after every (add, edit or delete) of a message. I'm using ReactDevTools Profiler to track rerenders.

  • Here is the full updated code: CodeSandbox
  • Also deployed on: Netlify

ChatSection.js:

import useChatService from "../hooks/useChatService";
import { useEffect } from "react";
import Chat from "./Chat";
import NoChat from "./NoChat";
import ChatInput from "./ChatInput";

const ChatSection = () => {
  let unsubscribe;
  const { getChatAndUnsub, messages } = useChatService();

  useEffect(() => {
    const getChat = async () => {
      unsubscribe = await getChatAndUnsub();
    };

    getChat();

    return () => {
      unsubscribe?.();
    };
  }, []);

  return (
    <div>
      {messages.length ? <Chat messages={messages} /> : <NoChat />}
      <p>ADD A MESSAGE</p>
      <ChatInput />
    </div>
  );
};

export default ChatSection;

Chat.js:

import { useState } from "react";
import ChatLine from "./ChatLine";
import useChatService from "../hooks/useChatService";

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

  const { updateMessage, deleteMessage } = useChatService();

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

export default Chat;

ChatInput:

import { useState } from "react";
import useChatService from "../hooks/useChatService";

const ChatInput = () => {
  const [inputValue, setInputValue] = useState("");
  const { addMessage } = useChatService();

  return (
    <textarea
      onKeyPress={(e) => {
        if (e.key === "Enter") {
          e.preventDefault();
          addMessage(inputValue);
          setInputValue("");
        }
      }}
      placeholder="new message..."
      onChange={(e) => {
        setInputValue(e.target.value);
      }}
      value={inputValue}
      autoFocus
    />
  );
};

export default ChatInput;

ChatLine.js:

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

const ChatLine = ({
  line,
  editValue,
  setEditValue,
  editingId,
  setEditingId,
  updateMessage,
  deleteMessage,
}) => {
  return (
    <div>
      {editingId !== line.id ? (
        <>
          <span style={{ marginRight: "20px" }}>{line.id}: </span>
          <span style={{ marginRight: "20px" }}>[{line.displayName}]</span>
          <span style={{ marginRight: "20px" }}>{line.message}</span>
          <button
            onClick={() => {
              setEditingId(line.id);
              setEditValue(line.message);
            }}
          >
            EDIT
          </button>
          <button
            onClick={() => {
              deleteMessage(line.id);
            }}
          >
            DELETE
          </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(null);
        }}
      >
        CANCEL
      </button>
    </div>
  );
};

export default memo(EditMessage);

useChatService.js:

import { useCallback, useState } from "react";
import {
  collection,
  onSnapshot,
  orderBy,
  query,
  serverTimestamp,
  updateDoc,
  doc,
  addDoc,
  deleteDoc,
} from "firebase/firestore";
import { db } from "../firebase/firebase-config";

const useChatService = () => {
  const [messages, setMessages] = useState([]);

  /**
   * Get Messages
   *
   * @returns {Promise<Unsubscribe>}
   */
  const getChatAndUnsub = async () => {
    const q = query(collection(db, "messages"), orderBy("createdAt"));

    const unsubscribe = onSnapshot(q, (snapshot) => {
      const data = snapshot.docs.map((doc, index) => {
        const entry = doc.data();

        return {
          id: doc.id,
          message: entry.message,
          createdAt: entry.createdAt,
          updatedAt: entry.updatedAt,
          uid: entry.uid,
          displayName: entry.displayName,
          photoURL: entry.photoURL,
        };
      });

      setMessages(data);
    });

    return unsubscribe;
  };

  /**
   * Memoized using useCallback
   */
  const updateMessage = useCallback(
    async (editValue, setEditValue, editingId, setEditingId) => {
      const message = editValue;
      const id = editingId;

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

      try {
        await updateDoc(doc(db, "messages", id), {
          message,
          updatedAt: serverTimestamp(),
        });
      } catch (err) {
        console.log(err);
      }
    },
    []
  );

  const addMessage = async (inputValue) => {
    if (!inputValue) {
      return;
    }
    const message = inputValue;

    const messageData = {
      // hardcoded photoURL, uid, and displayName for demo purposes
      photoURL:
        "https://lh3.googleusercontent.com/a/AATXAJwNw_ECd4OhqV0bwAb7l4UqtPYeSrRMpVB7ayxY=s96-c",
      uid: keyGen(),
      message,
      displayName: "John Doe",
      createdAt: serverTimestamp(),
      updatedAt: null,
    };

    try {
      await addDoc(collection(db, "messages"), messageData);
    } catch (e) {
      console.log(e);
    }
  };

  /**
   * Memoized using useCallback
   */
  const deleteMessage = useCallback(async (idToDelete) => {
    if (!idToDelete) {
      return;
    }
    try {
      await deleteDoc(doc(db, "messages", idToDelete));
    } catch (err) {
      console.log(err);
    }
  }, []);

  const keyGen = () => {
    const s = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    return Array(20)
      .join()
      .split(",")
      .map(function () {
        return s.charAt(Math.floor(Math.random() * s.length));
      })
      .join("");
  };

  return {
    messages,
    getChatAndUnsub,
    updateMessage,
    addMessage,
    deleteMessage,
  };
};

export default useChatService;

When a message gets updated using updateMessage method, I only need the affected ChatLine to rerender (same for add & delete), not every single ChatLine in the list, while keeping the messages state passed from ChatSection to Chat, I understand that ChatSection & Chat should rerender, but not every ChatLine in the list. (Also ChatLine is memoized)

EDIT 1: I guess the problem is with setMessages(data) in useChatService.js, but I thought React will only rerender the edited line because I already provided the key={line.id} when looping through messages in Chat component, but I have no idea how to fix this.

over 4 years ago · Santiago Trujillo
2 Respostas
Responde à pergunta

0

This is what I think, You are passing Messages in ChatSection and that means that when Messages get updated ChatSection will rerender and all its children will rerender too.

So here is my idea remove Messages from ChatSection and only add it in Chat.

You already using useChatService in Chat so adding Messages there should be better.

Try this and gets back too us if it working.

If still not as you like it to be there is also other way we could fix it.

But you have to create a working example for us so we could have a look and make small changes.

over 4 years ago · Santiago Trujillo Relatório

0

Wrap ChatLine inside React.memo, it will stop multiple rereders.

NOTE: Update areEqual function according to your usecase.

import { useState } from "react";
import ChatLine from "./ChatLine";
import useChatService from "../hooks/useChatService";

function areEqual(prevProps, nextProps) {
  /*
  return true if passing nextProps to render would return
  the same result as passing prevProps to render,
  otherwise return false
  */
  return prevProps.line === nextProps.line;
}
const ChatLineMemo = React.memo(ChatLine, areEqual);

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

  const { updateMessage, deleteMessage } = useChatService();


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

export default Chat;
over 4 years ago · Santiago Trujillo Relatório
Responde à pergunta
Encontrar trabalhos remotos

Descubra a nova forma de encontrar um emprego!

melhores empregos
Principais categorias de trabalho
Empresas
Postar vaga Preços Comercial
Jurídico
Termos e Condições Política de privacidade
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomende algumas ofertas para mim
Preciso de ajuda