Empresas
Empleos
  • Sobre nosotros
  • Soluciones
    • Publicación de vacantes
      Publica tu vacante y recibe candidatos calificados en 48h.
    • Evaluación de candidatos
      500+ pruebas técnicas y psicológicas, más anti-fraude.
    • Headhunting
      Búsqueda ejecutiva a la medida de principio a fin.
    • Nómina + EOR
      Dispersión de nómina y EOR en más de 15 países de LATAM.
  • Precios
  • Empleos

0

150
Vistas
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 Respuestas
Responde la pregunta

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 Denunciar

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 Denunciar
Responde la pregunta
Encuentra empleos remotos

¡Descubre la nueva forma de encontrar empleo!

Top de empleos
Top categorías de empleo
Empresas
Publicar vacante Precios Comercial
Legal
Términos y condiciones Política de privacidad
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomiéndame algunas ofertas
Necesito ayuda