dwi*_*wix 13 javascript reactjs react-hooks
我有一个使用 Firebase v9 的简单聊天应用程序,其中这些组件按以下分层顺序从父级到子级:ChatSection、Chat、ChatLine、EditMessage。
我有一个名为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
看来你最近的几个问题都围绕着试图阻止 React 组件重新渲染。这很好,但不要花太多时间过早地优化。React 开箱即用,运行良好。
\n关于memoHOC 和优化性能,甚至文档也直截了当地指出:
\n\n该方法仅作为性能优化而存在。不要依赖 \xe2\x80\x9cprevent\xe2\x80\x9d 进行渲染,因为这可能会导致错误。
\n
这意味着 React 仍然可以在需要时重新渲染组件。我相信映射数组messages就是其中一种情况。当。。。的时候messages状态更新时,它是一个新数组,因此必须重新渲染。React 的协调需要重新渲染数组和数组的每个元素,但可能不需要更深入。
您可以通过添加一个记忆的子组件来测试这一点ChatLine,并观察即使ChatLine包裹在memoHOC 中,它仍然会重新渲染,而记忆的子组件则不会。
const Child = memo(({ id }) => {\n useEffect(() => {\n console.log(\'Child rendered\', id); // <-- doesn\'t log when messages updates\n })\n return <>Child: {id}</>;\n});\nRun Code Online (Sandbox Code Playgroud)\n...
\nconst 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);\nRun Code Online (Sandbox Code Playgroud)\n这里的要点应该是您不应该过早地优化。仅当您找到实际的工具时才应考虑诸如记忆化和虚拟化之类的工具。
\n您也不应该“过度优化”。我为与我合作的客户开发的 React 应用程序,我们很早就这样做了,以为我们可以节省自己的时间,但最终随着时间的推移(随着我们对 React hooks 的熟悉),我们已经删除了大部分或几乎所有我们的“优化”,因为它们最终并没有真正为我们节省太多,并增加了更多的复杂性。我们最终发现性能瓶颈更多地与我们的架构和组件组成有关,而不是与列表中呈现的组件数量有关。
\n因此,您在多个组件中使用useChatService自定义挂钩,但正如所写,每个挂钩都是其自己的实例,并提供自己的状态副本messages和其他各种回调。这就是为什么你必须将messagesstate 作为 prop 从ChatSectionto传递Chat。在这里,我建议将messages状态和回调移动到 React 上下文中,以便每个useChatService钩子“实例”可以提供相同的上下文值。
(可能会被重命名,因为现在不仅仅是一个钩子)
\n创建上下文:
\nexport const ChatServiceContext = createContext({\n messages: [],\n updateMessage: () => {},\n addMessage: () => {},\n deleteMessage: () => {}\n});\nRun Code Online (Sandbox Code Playgroud)\n创建上下文提供者:
\ngetChatAndUnsub没有等待任何东西,所以没有理由声明它async。记住添加、更新和删除消息的所有回调。
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;\nRun Code Online (Sandbox Code Playgroud)\n创建useChatService钩子:
export const useChatService = () => useContext(ChatServiceContext);\nRun Code Online (Sandbox Code Playgroud)\n索引.js
\nimport ChatServiceProvider from "./hooks/useChatService";\n\nReactDOM.render(\n <React.StrictMode>\n <ChatServiceProvider>\n <App />\n </ChatServiceProvider>\n </React.StrictMode>,\n document.getElementById("root")\n);\nRun Code Online (Sandbox Code Playgroud)\n使用useChatService钩子来消耗messages状态。
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;\nRun Code Online (Sandbox Code Playgroud)\n删除编辑状态和设置器(稍后会详细介绍)。使用useChatService钩子来消耗messages状态。
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;\nRun Code Online (Sandbox Code Playgroud)\n将编辑状态移至此处。使用布尔切换来代替editingId状态来进行编辑模式。将编辑 ID 封装在updateMessage上下文的回调中。在本地管理此处的所有编辑状态,不要将状态值和设置器作为回调传递给另一个组件来调用。请注意,EditMessage组件 API 已更新。
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};\nRun Code Online (Sandbox Code Playgroud)\n在这里您可以使用memoHOC。你可以进一步暗示 React也许不应该重新渲染,但请记住,这并不能完全阻止组件重新渲染。这只是一个暗示,也许 React 可以放弃重新渲染。
export default memo(ChatLine, (prev, next) => {\n return prev.line.id === next.line.id;\n});\nRun Code Online (Sandbox Code Playgroud)\n只需将 props 代理到textarea和各自的 props 即可button。换句话说,让ChatLine保持它需要的状态。
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;\nRun Code Online (Sandbox Code Playgroud)\n消耗addMessage来自useChatService。我认为这里没有太大变化,但为了完整起见,还是包括在内。
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;\nRun Code Online (Sandbox Code Playgroud)\n\n