避免重新渲染列表中的每个组件,同时只更新 React 中的一个组件

dwi*_*wix 13 javascript reactjs react-hooks

我有一个使用 Firebase v9 的简单聊天应用程序,其中这些组件按以下分层顺序从父级到子级:ChatSectionChatChatLineEditMessage

我有一个名为useChatService保存 in 状态列表的自定义钩子messages,该钩子在 in 中调用ChatSection,钩子返回 ,messages我将它们从ChatSectionprop 中传递到Chat,然后循环messages并为每条消息创建一个ChatLine组件。

我可以单击Edit每条消息前面的按钮,它显示EditMessage组件,以便我可以编辑文本,然后当我按“Enter”时,该函数updateMessage将被执行并更新数据库中的消息,但随后每个消息ChatLine都会再次重新渲染,随着列表变大,这是一个问题。

编辑 2:我已经完成了使用 Firebase v9 制作一个工作示例的代码,以便您可以在每次(添加、编辑或删除)消息之后可视化我正在讨论的重新渲染。我正在使用 ReactDevTools Profiler 来跟踪重新渲染。

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;
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

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);
Run Code Online (Sandbox Code Playgroud)

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);
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

当使用方法更新消息时updateMessage,我只需要受影响的重新渲染(与添加和删除相同),而不是列表中的ChatLine每个,同时保持从to传递的状态,我知道&应该重新渲染,但不是列表中的每个列表。(也被记住了)ChatLinemessagesChatSectionChatChatSectionChatChatLineChatLine

编辑1:我猜问题出在setMessages(data)in useChatService.js,但我认为 React 只会重新渲染编辑后的行,因为我已经提供了在组件中key={line.id}循环的时间,但我不知道如何解决这个问题。messagesChat

Dre*_*ese 5

序幕

\n

看来你最近的几个问题都围绕着试图阻止 React 组件重新渲染。这很好,但不要花太多时间过早地优化。React 开箱即用,运行良好。

\n

关于memoHOC 和优化性能,甚至文档也直截了当地指出:

\n
\n

该方法仅作为性能优化而存在。不要依赖 \xe2\x80\x9cprevent\xe2\x80\x9d 进行渲染,因为这可能会导致错误。

\n
\n

这意味着 React 仍然可以在需要时重新渲染组件。我相信映射数组messages就是其中一种情况。当。。。的时候messages状态更新时,它是一个新数组,因此必须重新渲染。React 的协调需要重新渲染数组和数组的每个元素,但可能不需要更深入。

\n

您可以通过添加一个记忆的子组件来测试这一点ChatLine,并观察即使ChatLine包裹在memoHOC 中,它仍然会重新渲染,而记忆的子组件则不会。

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

...

\n
const ChatLine = (props) => {\n  ...\n\n  useEffect(() => {\n    console.log("Chatline rendered", line.id); // <-- logs when messages updates\n  });\n\n  return (\n    <div>\n      ...\n          <Child id={line.id} />\n      ...\n    </div>\n  );\n};\n\nexport default memo(ChatLine);\n
Run Code Online (Sandbox Code Playgroud)\n

这里的要点应该是您不应该过早地优化。仅当您找到实际的工具时才应考虑诸如记忆化和虚拟化之类的工具

\n

您也不应该“过度优化”。我为与我合作的客户开发的 React 应用程序,我们很早就这样做了,以为我们可以节省自己的时间,但最终随着时间的推移(随着我们对 React hooks 的熟悉),我们已经删除了大部分或几乎所有我们的“优化”,因为它们最终并没有真​​正为我们节省太多,并增加了更多的复杂性。我们最终发现性能瓶颈更多地与我们的架构和组件组成有关,而不是与列表中呈现的组件数量有关。

\n

建议的解决方案

\n

因此,您在多个组件中使用useChatService自定义挂钩,但正如所写,每个挂钩都是其自己的实例,并提供自己的状态副本messages和其他各种回调。这就是为什么你必须将messagesstate 作为 prop 从ChatSectionto传递Chat。在这里,我建议将messages状态和回调移动到 React 上下文中,以便每个useChatService钩子“实例”可以提供相同的上下文值。

\n

使用聊天服务

\n

可能会被重命名,因为现在不仅仅是一个钩子

\n

创建上下文:

\n
export const ChatServiceContext = createContext({\n  messages: [],\n  updateMessage: () => {},\n  addMessage: () => {},\n  deleteMessage: () => {}\n});\n
Run Code Online (Sandbox Code Playgroud)\n

创建上下文提供者:

\n

getChatAndUnsub没有等待任何东西,所以没有理由声明它async。记住添加、更新和删除消息的所有回调。

\n
const ChatServiceProvider = ({ children }) => {\n  const [messages, setMessages] = useState([]);\n\n  const getChatAndUnsub = () => {\n    const q = query(collection(db, "messages"), orderBy("createdAt"));\n\n    const unsubscribe = onSnapshot(q, (snapshot) => {\n      const data = snapshot.docs.map((doc, index) => {\n        const entry = doc.data();\n\n        return { .... };\n      });\n\n      setMessages(data);\n    });\n\n    return unsubscribe;\n  };\n\n  useEffect(() => {\n    const unsubscribe = getChatAndUnsub();\n\n    return () => {\n      unsubscribe();\n    };\n  }, []);\n\n  const updateMessage = useCallback(async (message, id) => {\n    try {\n      await updateDoc(doc(db, "messages", id), {\n        message,\n        updatedAt: serverTimestamp()\n      });\n    } catch (err) {\n      console.log(err);\n    }\n  }, []);\n\n  const addMessage = useCallback(async (message) => {\n    if (!message) {\n      return;\n    }\n\n    const messageData = { .... };\n\n    try {\n      await addDoc(collection(db, "messages"), messageData);\n    } catch (e) {\n      console.log(e);\n    }\n  }, []);\n\n  const deleteMessage = useCallback(async (idToDelete) => {\n    if (!idToDelete) {\n      return;\n    }\n    try {\n      await deleteDoc(doc(db, "messages", idToDelete));\n    } catch (err) {\n      console.log(err);\n    }\n  }, []);\n\n  const keyGen = () => { .... };\n\n  return (\n    <ChatServiceContext.Provider\n      value={{\n        messages,\n        updateMessage,\n        addMessage,\n        deleteMessage\n      }}\n    >\n      {children}\n    </ChatServiceContext.Provider>\n  );\n};\n\nexport default ChatServiceProvider;\n
Run Code Online (Sandbox Code Playgroud)\n

创建useChatService钩子:

\n
export const useChatService = () => useContext(ChatServiceContext);\n
Run Code Online (Sandbox Code Playgroud)\n

为应用程序提供聊天服务

\n

索引.js

\n
import ChatServiceProvider from "./hooks/useChatService";\n\nReactDOM.render(\n  <React.StrictMode>\n    <ChatServiceProvider>\n      <App />\n    </ChatServiceProvider>\n  </React.StrictMode>,\n  document.getElementById("root")\n);\n
Run Code Online (Sandbox Code Playgroud)\n

聊天区

\n

使用useChatService钩子来消耗messages状态。

\n
const ChatSection = () => {\n  const { messages } = useChatService();\n\n  return (\n    <div>\n      {messages.length ? <Chat /> : <NoChat />}\n      <p>ADD A MESSAGE</p>\n      <ChatInput />\n    </div>\n  );\n};\n\nexport default ChatSection;\n
Run Code Online (Sandbox Code Playgroud)\n

聊天

\n

删除编辑状态和设置器(稍后会详细介绍)。使用useChatService钩子来消耗messages状态。

\n
const Chat = () => {\n  const { messages } = useChatService();\n\n  return (\n    <div>\n      <p>MESSAGES :</p>\n      {messages.map((line) => (\n        <ChatLine key={line.id} line={line} />\n      ))}\n    </div>\n  );\n};\n\nexport default Chat;\n
Run Code Online (Sandbox Code Playgroud)\n

聊天热线

\n

将编辑状态移至此处。使用布尔切换来代替editingId状态来进行编辑模式。将编辑 ID 封装在updateMessage上下文的回调中。在本地管理此处的所有编辑状态,不要将状态值和设置器作为回调传递给另一个组件来调用。请注意,EditMessage组件 API 已更新。

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

在这里您可以使用memoHOC。你可以进一步暗示 React也许不应该重新渲染,但请记住,这并不能完全阻止组件重新渲染。这只是一个暗示,也许 React 可以放弃重新渲染。

\n
export default memo(ChatLine, (prev, next) => {\n  return prev.line.id === next.line.id;\n});\n
Run Code Online (Sandbox Code Playgroud)\n

编辑留言

\n

只需将 props 代理到textarea和各自的 props 即可button。换句话说,让ChatLine保持它需要的状态。

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

聊天输入

\n

消耗addMessage来自useChatService。我认为这里没有太大变化,但为了完整起见,还是包括在内。

\n
const ChatInput = () => {\n  const [inputValue, setInputValue] = useState("");\n  const { addMessage } = useChatService();\n\n  return (\n    <textarea\n      onKeyPress={(e) => {\n        if (e.key === "Enter") {\n          e.preventDefault();\n          addMessage(inputValue);\n          setInputValue("");\n        }\n      }}\n      placeholder="new message..."\n      onChange={(e) => {\n        setInputValue(e.target.value);\n      }}\n      value={inputValue}\n      autoFocus\n    />\n  );\n};\n\nexport default ChatInput;\n
Run Code Online (Sandbox Code Playgroud)\n

编辑在反应中仅更新一个时避免重新渲染列表中的每个组件

\n