Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

568
Views
Evite volver a renderizar todos los componentes de la lista mientras actualiza solo uno en React

Tengo una aplicación de chat simple que usa Firebase v9, con estos componentes de padre a hijo en este orden jerárquico: ChatSection , Chat , ChatLine , EditMessage .

Tengo un gancho personalizado llamado useChatService que contiene la lista de messages en estado, el gancho se llama en ChatSection , el gancho devuelve los messages y los paso de ChatSection en un accesorio a Chat , luego recorro los messages y creo un componente ChatLine para cada mensaje

Puedo hacer clic en el botón Edit frente a cada mensaje, muestra el componente EditMessage para que pueda editar el texto, luego, cuando presiono "Enter", la función updateMessage se ejecuta y actualiza el mensaje en la base de datos, pero luego cada ChatLine se vuelve a renderizar, lo cual es un problema a medida que la lista crece.

EDICIÓN 2: Completé el código para hacer un ejemplo de trabajo con Firebase v9 para que pueda visualizar las reproducciones de las que estoy hablando después de cada (agregar, editar o eliminar) de un mensaje. Estoy usando ReactDevTools Profiler para realizar un seguimiento de los renderizados.

  • Aquí está el código actualizado completo: CodeSandbox
  • También implementado en: 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;

Cuando un mensaje se actualiza usando el método updateMessage , solo necesito que se ChatLine a procesar la línea de chat afectada (lo mismo para agregar y eliminar), no todas las líneas de ChatLine en la lista, mientras mantengo el estado de los messages pasados de ChatSection a Chat , entiendo que ChatSection & Chat debería volver a mostrarse, pero no todas las ChatLine de la lista. (También se ChatLine )

EDIT 1: Supongo que el problema es con setMessages(data) en useChatService.js , pero pensé que React solo volvería a mostrar la línea editada porque ya proporcioné la key={line.id} al recorrer los messages en el componente Chat , pero yo no tengo idea de cómo arreglar esto.

over 4 years ago · Santiago Trujillo
3 answers
Answer question

0

Esto es lo que creo: está pasando Messages en ChatSection y eso significa que cuando los Messages se actualicen, ChatSection se volverá a procesar y todos sus elementos secundarios también se volverán a procesar.

Así que aquí está mi idea de eliminar Messages de ChatSection y solo agregarlo en Chat .

Ya está usando useChatService en Chat, por lo que agregar Messages allí debería ser mejor.

Pruebe esto y nos devuelva también si funciona.

Si todavía no es como le gustaría que fuera, también hay otra manera de arreglarlo.

Pero debe crear un ejemplo de trabajo para nosotros para que podamos echar un vistazo y hacer pequeños cambios.

over 4 years ago · Santiago Trujillo Report

0

Envuelva ChatLine dentro de React.memo, detendrá múltiples rereders.

NOTA: Actualice la función areEqual según su caso de uso.

 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 Report

0

Preludio

Parece que varias de sus preguntas últimamente han girado en torno a tratar de evitar que los componentes de React vuelvan a renderizarse. Esto está muy bien, pero no pierda demasiado tiempo optimizando prematuramente. React funciona bastante bien desde el primer momento.

Con respecto a memo HOC y la optimización del rendimiento, incluso los documentos afirman rotundamente:

Este método solo existe como una optimización del rendimiento. No confíe en él para "prevenir" un renderizado, ya que esto puede generar errores.

Esto significa que React aún puede volver a renderizar un componente si es necesario. Creo que mapear la matriz de messages es uno de estos casos. Cuando el estado de los messages se actualiza, es una nueva matriz, por lo que debe volver a procesarse. La reconciliación de React necesita volver a representar la matriz y cada elemento de la matriz, pero es posible que no necesite profundizar más.

Puede probar esto agregando un componente secundario memorizado a ChatLine y ver como, aunque ChatLine está envuelto en memo HOC, todavía se vuelve a procesar mientras que el componente secundario memorizado no lo está.

 const Child = memo(({ id }) => { useEffect(() => { console.log('Child rendered', id); // <-- doesn't log when messages updates }) return <>Child: {id}</>; });

...

 const ChatLine = (props) => { ... useEffect(() => { console.log("Chatline rendered", line.id); // <-- logs when messages updates }); return ( <div> ... <Child id={line.id} /> ... </div> ); }; export default memo(ChatLine);

La conclusión aquí debería ser que no debes optimizar prematuramente. Las herramientas como la memoización y la virtualización solo deben analizarse si encuentra un problema de rendimiento real y tiene un rendimiento auditado/comparado correctamente.

Tampoco debe "optimizar en exceso". La aplicación React que desarrollo para un cliente con el que trabajo hicimos esto al principio pensando que nos estábamos ahorrando tiempo, pero eventualmente con el tiempo (y a medida que nos familiarizamos con los ganchos de React) eliminamos la mayoría o casi todos nuestros " optimizaciones", ya que en realidad no nos ahorraron mucho y agregaron más complejidad. Eventualmente encontramos nuestros cuellos de botella de rendimiento que tenían más que ver con nuestra arquitectura y composición de componentes que con el número de componentes representados en las listas.

Solución sugerida

Por lo tanto, estaba utilizando el enlace personalizado useChatService en varios componentes, pero tal como está escrito, cada enlace era su propia instancia y proporcionaba su propia copia del estado de los messages y otras devoluciones de llamada. Esta es la razón por la que tuvo que pasar el estado de los messages como accesorio de ChatSection a Chat . Aquí sugiero mover el estado de los messages y las devoluciones de llamada a un contexto de React para que cada "instancia" useChatService pueda proporcionar el mismo valor de contexto.

usarChatService

( probablemente podría ser renombrado ya que es más que un gancho ahora )

Crea un contexto:

 export const ChatServiceContext = createContext({ messages: [], updateMessage: () => {}, addMessage: () => {}, deleteMessage: () => {} });

Cree un proveedor de contexto:

getChatAndUnsub no estaba esperando nada, por lo que no había motivo para declararlo async . Memoice todas las devoluciones de llamada para agregar, actualizar y eliminar mensajes.

 const ChatServiceProvider = ({ children }) => { const [messages, setMessages] = useState([]); const getChatAndUnsub = () => { 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 { .... }; }); setMessages(data); }); return unsubscribe; }; useEffect(() => { const unsubscribe = getChatAndUnsub(); return () => { unsubscribe(); }; }, []); const updateMessage = useCallback(async (message, id) => { try { await updateDoc(doc(db, "messages", id), { message, updatedAt: serverTimestamp() }); } catch (err) { console.log(err); } }, []); const addMessage = useCallback(async (message) => { if (!message) { return; } const messageData = { .... }; try { await addDoc(collection(db, "messages"), messageData); } catch (e) { console.log(e); } }, []); const deleteMessage = useCallback(async (idToDelete) => { if (!idToDelete) { return; } try { await deleteDoc(doc(db, "messages", idToDelete)); } catch (err) { console.log(err); } }, []); const keyGen = () => { .... }; return ( <ChatServiceContext.Provider value={{ messages, updateMessage, addMessage, deleteMessage }} > {children} </ChatServiceContext.Provider> ); }; export default ChatServiceProvider;

Cree el useChatService :

 export const useChatService = () => useContext(ChatServiceContext);

Proporcionar el servicio de chat a la aplicación.

índice.js

 import ChatServiceProvider from "./hooks/useChatService"; ReactDOM.render( <React.StrictMode> <ChatServiceProvider> <App /> </ChatServiceProvider> </React.StrictMode>, document.getElementById("root") );

ChatSección

Use el gancho useChatService para consumir el estado de los messages .

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

Chat

Elimine el estado de edición y los configuradores ( más sobre esto más adelante ). Use el gancho useChatService para consumir el estado de los messages .

 const Chat = () => { const { messages } = useChatService(); return ( <div> <p>MESSAGES :</p> {messages.map((line) => ( <ChatLine key={line.id} line={line} /> ))} </div> ); }; export default Chat;

Línea de chat

Mueva el estado de edición aquí. En lugar de un estado de editingId de edición, use un conmutador booleano para un modo de edición. Encapsule el ID de edición en la devolución de llamada updateMessage del contexto. Administre todo el estado de edición aquí localmente, no pase los valores de estado y setter como devoluciones de llamada para que otro componente llame. Tenga en cuenta que se actualizó la API del componente EditMessage .

 const ChatLine = ({ line }) => { const [editValue, setEditValue] = useState(""); const [isEditing, setIsEditing] = useState(false); const { updateMessage, deleteMessage } = useChatService(); return ( <div> {!isEditing ? ( <> <span style={{ marginRight: "20px" }}>{line.id}: </span> <span style={{ marginRight: "20px" }}>[{line.displayName}]</span> <span style={{ marginRight: "20px" }}>{line.message}</span> <button onClick={() => { setIsEditing(true); setEditValue(line.message); }} > EDIT </button> <button onClick={() => { deleteMessage(line.id); }} > DELETE </button> </> ) : ( <EditMessage value={editValue} onChange={setEditValue} onSave={() => { // updating message in DB updateMessage(editValue, line.id); setEditValue(""); setIsEditing(false); }} onCancel={() => setIsEditing(false)} /> )} </div> ); };

Aquí puedes usar el memo HOC. Puede sugerirle a React que tal vez este componente no debería volver a renderizarse si la identificación de la línea permanece igual, pero recuerde que esto no evita por completo que el componente se vuelva a renderizar. Es solo una pista de que tal vez React pueda rescatar las reproducciones.

 export default memo(ChatLine, (prev, next) => { return prev.line.id === next.line.id; });

Editar mensaje

Simplemente envíe los accesorios a sus respectivos accesorios del área de textarea y el button . En otras palabras, deja que ChatLine mantenga el estado que necesita.

 const EditMessage = ({ value, onChange, onSave, onCancel }) => { return ( <div> <textarea onKeyPress={(e) => { if (e.key === "Enter") { // prevent textarea default behaviour (line break on Enter) e.preventDefault(); onSave(); } }} onChange={(e) => onChange(e.target.value)} value={value} autoFocus /> <button type="button" onClick={onCancel}> CANCEL </button> </div> ); }; export default EditMessage;

Entrada de chat

Consuma addMessage desde el useChatService . No creo que haya cambiado mucho aquí, pero se incluye de todos modos por el bien de la integridad.

 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;

Editar evitar volver a renderizar todos los componentes de la lista mientras se actualiza solo uno en reacción

over 4 years ago · Santiago Trujillo Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!