我有一个使用 Firebase v9 的简单聊天应用程序,这些组件按此层次顺序从父级到子级: ChatSection
、 Chat
、 ChatLine
、 EditMessage
。
我有一个名为 useChatService
的自定义钩子(Hook),它在状态下保存 messages
列表,钩子(Hook)在 ChatSection
中调用,钩子(Hook)返回 messages
,我将它们从 ChatSection
在 Prop 中传递给 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;
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;
当使用 updateMessage
方法更新消息时,我只需要重新渲染受影响的 ChatLine
(添加和删除相同),而不是列表中的每个 ChatLine
,同时保持 messages
状态从 ChatSection
传递到 Chat
,我了解 ChatSection
和 Chat
应该重新渲染,但不是列表中的每个 ChatLine
。 (也 ChatLine
被内存)编辑 1:我猜问题出在
setMessages(data)
中的 useChatService.js
上,但我认为 React 只会重新渲染已编辑的行,因为在 key={line.id}
组件中循环遍历 messages
时我已经提供了 Chat
,但我不知道如何解决这个问题。
最佳答案
序幕
似乎您最近的几个问题都围绕着试图阻止 React 组件重新呈现。这很好,但不要花太多时间过早地优化。 React 开箱即用运行良好。
关于memo
HOC 和优化性能,甚至是 docs直接声明:
This method only exists as a performance optimization. Do not rely on it to “prevent” a render, as this can lead to bugs.
这意味着如果需要,React 仍然可以重新渲染组件。我相信映射
messages
数组就是其中一种情况。当messages
state 更新它是一个新数组,因此必须重新渲染。 React 的和解需要重新渲染数组和数组的每个元素,但可能不需要更深入。您可以通过向
ChatLine
添加一个内存子组件来测试它。并观看,即使 ChatLine
包裹在 memo
中HOC 它仍然被重新渲染,而内存的 child 没有。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);
这里的要点应该是你不应该过早地优化。仅当您发现实际性能问题并具有适当的基准/审计性能时,才应查看诸如 memoization 和虚拟化之类的工具。你也不应该“过度优化”。我为与我一起工作的客户开发的 React 应用程序我们很早就这样做了,因为我们认为我们可以节省自己的时间,但最终随着时间的推移(并且随着我们对 React 钩子(Hook)的熟悉)我们已经删除了大部分或几乎所有我们的“优化”,因为它们最终并没有真正为我们节省太多并增加了更多的复杂性。我们最终发现我们的性能瓶颈更多地与我们的架构和组件组合有关,而不是与列表中呈现的组件数量有关。
建议的解决方案
所以您使用的是
useChatService
几个组件中的自定义钩子(Hook),但正如所写的,每个钩子(Hook)都是它自己的实例,并提供它自己的 messages
的副本state 和其他各种回调。这就是为什么你必须通过 messages
声明为来自 ChatSection
的 Prop 至Chat
.这里我建议移动messages
状态和回调到 React 上下文中,所以每个 useChatService
钩子(Hook)“实例”可以提供相同上下文值。使用聊天服务
(可能会被重命名,因为现在不仅仅是一个钩子(Hook))
创建上下文:
export const ChatServiceContext = createContext({
messages: [],
updateMessage: () => {},
addMessage: () => {},
deleteMessage: () => {}
});
创建上下文提供者:getChatAndUnsub
没有等待任何东西,所以没有理由宣布它async
.记住所有回调以添加、更新和删除消息。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;
创建 useChatService
钩:export const useChatService = () => useContext(ChatServiceContext);
为应用提供聊天服务index.js
import ChatServiceProvider from "./hooks/useChatService";
ReactDOM.render(
<React.StrictMode>
<ChatServiceProvider>
<App />
</ChatServiceProvider>
</React.StrictMode>,
document.getElementById("root")
);
聊天区使用
useChatService
使用 messages
的钩子(Hook)状态。const ChatSection = () => {
const { messages } = useChatService();
return (
<div>
{messages.length ? <Chat /> : <NoChat />}
<p>ADD A MESSAGE</p>
<ChatInput />
</div>
);
};
export default ChatSection;
聊天删除编辑状态和 setter (稍后会详细介绍)。使用
useChatService
使用 messages
的钩子(Hook)状态。const Chat = () => {
const { messages } = useChatService();
return (
<div>
<p>MESSAGES :</p>
{messages.map((line) => (
<ChatLine key={line.id} line={line} />
))}
</div>
);
};
export default Chat;
聊天热线将编辑状态移至此处。而不是
editingId
state 对编辑模式使用 bool 切换。将编辑id封装在updateMessage
中从上下文回调。管理 全部 在本地编辑状态,不要将状态值和 setter 作为回调传递给另一个组件调用。请注意 EditMessage
组件 API 已更新。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>
);
};
在这里您可以使用memo
HOC。您可以进一步向 React 提示,如果 line id 保持相等,则该组件可能不应该重新渲染,但请记住,这并不能完全阻止组件被重新渲染。这只是一个暗示,也许 React 可以放弃重新渲染。export default memo(ChatLine, (prev, next) => {
return prev.line.id === next.line.id;
});
编辑消息只需将 Prop 代理到
textarea
的各自 Prop 和 button
.换句话说,让 ChatLine
保持它需要的状态。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;
聊天输入消费
addMessage
来自 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;
关于javascript - 避免在 React 中只更新一个组件时重新渲染列表中的每个组件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70319227/