I'm building a chat app, I have 3 components from parent to child in this hierarchical order: Chat
, ChatLine
, EditMessage
.
I'm looping through messages
state in Chat
to display multiple ChatLine
components as a list, and I pass some state to ChatLine
and then to EditMessage
.
I need the state :
const [editValue, setEditValue] = useState("");
const [editingId, setEditingId] = useState(null);
to remain in the parent component Chat
so I can have access to it later there.
Anyway, now when I click on the Edit
button, the EditMessage
component shows a textarea, and I'm setting state onChange in it, but everytime I click the Edit button or type a letter in the textarea all the components rerender as I see in React DevTool Profiler, even the children that didn't get affected, I only need the Chat
and affected ChatLine
to rerender at most.
The whole code is available in CodeSandbox, and deployed in Netlify.
And here it is here also :
(Chat.js)
import { useEffect, useState } from "react";
import ChatLine from "./ChatLine";
const Chat = () => {
const [messages, setMessages] = useState([]);
const [editValue, setEditValue] = useState("");
const [editingId, setEditingId] = useState(null);
useEffect(() => {
setMessages([
{ id: 1, message: "Hello" },
{ id: 2, message: "Hi" },
{ id: 3, message: "Bye" },
{ id: 4, message: "Wait" },
{ id: 5, message: "No" },
{ id: 6, message: "Ok" },
]);
}, []);
return (
<div>
<p>MESSAGES :</p>
{messages.map((line) => (
<ChatLine
key={line.id}
line={line}
editValue={editValue}
setEditValue={setEditValue}
editingId={editingId}
setEditingId={setEditingId}
/>
))}
</div>
);
};
export default Chat;
(ChatLine.js)
import EditMessage from "./EditMessage";
import { memo } from "react";
const ChatLine = ({
line,
editValue,
setEditValue,
editingId,
setEditingId,
}) => {
return (
<div>
{editingId !== line.id ? (
<>
<span>{line.id}: </span>
<span>{line.message}</span>
<button
onClick={() => {
setEditingId(line.id);
setEditValue(line.message);
}}
>
EDIT
</button>
</>
) : (
<EditMessage
editValue={editValue}
setEditValue={setEditValue}
setEditingId={setEditingId}
editingId={editingId}
/>
)}
</div>
);
};
export default memo(ChatLine);
(EditMessage.js)
import { memo } from "react";
const EditMessage = ({ editValue, setEditValue, editingId, setEditingId }) => {
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("");
}}
>
CANCEL
</button>
</div>
);
};
export default memo(EditMessage);
const updateMessage = (editValue, setEditValue, editingId, setEditingId) => {
const message = editValue;
const id = editingId;
// resetting state as soon as we press Enter
setEditValue("");
setEditingId(null);
// ajax call to update message in DB using `message` & `id` variables
console.log("updating..");
};
CodePudding user response:
The problem is that all of the child components see their props change any time any of them is in the process of being edited, because you're passing the current editing information to all of the children. Instead, only pass the current editing text (editValue
) to the component being edited, not to all the others.
ChatLine
doesn't use editValue
when it's not the instance being edited. So I'd do one of two things:
Use a different component for display (
ChatLine
) vs. edit (ChatLineEdit
). Almost the entire body ofChatLine
is different depending on whether that line is being edited or not anyway. Then only passeditValue
toChatLineEdit
.Pass
""
(or similar) aseditValue
to the one not being edited. In themap
inChat
:editValue={line.id === editingId ? editValue : ""}
.Pass an "are equal" function into
memo
forChatLine
that doesn't care what the value ofeditValue
is ifline.id !== editingId
. By default,memo
does a shallow check of all props, but you can take control of that process by providing a function as the second argument. For instance:export default memo(ChatLine, (prevProps, nextProps) => { // "Equal" for rendering purposes? return ( // Same chat line prevProps.line === nextProps.line && // Same edit value setter (you can leave this out, setters from `useState` never change) prevProps.setEditValue === prevProps.setEditValue && // *** // Same editingId prevProps.editingId === prevProps.editingId && // Same editingId setter (you can leave this out too) prevProps.setEditingId === prevProps.setEditingId && // *** ( // Same edit value... prevProps.editValue === prevProps.editValue || // OR, we don't care because we're not being edited nextProps.line.id !== nextProps.editingId ) ); });
This is fragile, because it's easy to get the check wrong, but it's another option.
I would go for #1. Not even passing props to components that they don't need is (IMHO) the cleanest approach.