Let's say you have a <Comments />
component. Inside it you have two children <CreateComment />
and <ListComments />
. <ListComments />
renders the list of comments passed to it. <CreateComment />
renders an input box. <ListComment />
also renders a reply button next to each comment. When the button is clicked we want to populate our input box in <CreateComment />
with the comment we click on enclosed within double quotes. So basically we need a way to communicate between <ListComments />
and <CreateComment />
You can see the demo of the required behavior in the below codesandbox:
export default function Comments() {
const allComments = [{ content: "comment one" }, { content: "comment two" }];
return (
<div>
<CreateComment />
<ListComments allComments={allComments} />
</div>
);
}
export default function CreateComment() {
const [comment, setComment] = useState("");
return (
<div className="create-comment">
<header>CreateComment</header>
<textarea value={comment} onChange={(e) => setComment(e.target.value)} />
</div>
);
}
export default function ListComments({ allComments }) {
function handleReply(comment) {
}
return (
<div className="list">
<header>List</header>
<main>
{
allComments.map((comment) => (
<div className="comment">
<div>{comment.content}</div>
<button onClick={() => handleReply(comment)}>Reply</button>
</div>
))
}
</main>
</div>
);
}
There are three ways to do this.
Most people are oblivious to the 3rd option and I really prefer the 3rd option in many cases.
i.e. Lifting the state comment
to <Comments />
Remove the useState()
line from CreateComment.jsx
and add it to Comments
and pass the state as props to it.
export default function Comments() {
const allComments = [{ content: "comment one" }, { content: "comment two" }];
const [comment, setComment] = useState("");
return (
<div>
<CreateComment comment={comment} setComment={setComment} />
<ListComments allComments={allComments} setComment={setComment} />
</div>
);
}
Now you can use setComment
to do the job in handleReply()
export default function ListComments({ allComments, setComment }) {
function handleReply(commentToAppend) {
setComment((text) => `${text}\n"${commentToAppend.content}"\n`);
}
}
The problem with this is that comment
is supposed to be a state that is local to <CreateComment />
. We placed it in the parent component for the sole reason to mutate it in response to an event in a sibling component.
You can do a similar thing with redux where instead of lifting the state to the common parent, you can store it in redux. From <ListComments />
you can issue an action to mutate it and in <CreateComment />
you can access it as you access any other state in redux.
The problem again with this approach is that comment
is supposed to be local to <CreateComment />
but we put it in the global state.
I am not presenting the code for this as this method is extensively covered by many articles out in the internet.
RxJS subjects allow you to create a stream to which you push events and also subscribe to those events. In our case we'll create a replyTo$
stream and use it for communication between the siblings.
import { Subject } from "rxjs";
export default function Comments() {
const allComments = [{ content: "comment one" }, { content: "comment two" }];
const replyTo$ = useRef(new Subject());
return (
<div>
<CreateComment replyTo$={replyTo$} />
<ListComments allComments={allComments} replyTo$={replyTo$} />
</div>
);
}
Send data to <CreateComment />
through the replyTo$
stream
export default function ListComments({ allComments, replyTo$ }) {
function handleReply(commentToAppend) {
replyTo$.current.next(commentToAppend);
}
return (
<div className="list">
<header>List</header>
<main>
{
allComments.map((comment) => (
<div className="comment">
<div>{comment.content}</div>
<button onClick={() => handleReply(comment)}>Reply</button>
</div>
))
}
</main>
</div>
);
}
Subscribe to the stream in <CreateComment />
export default function CreateComment({ replyTo$ }) {
const [comment, setComment] = useState("");
useEffect(() => {
const subscription = replyTo$.current.subscribe((replyTo) => {
setComment((text) => `${text}\n"${replyTo.content}"\n`)
});
return function teardown() {
subscription.unsubscribe();
};
}, []);
return (
<div className="create-comment">
<header>CreateComment</header>
<textarea value={comment} onChange={(e) => setComment(e.target.value)} />
</div>
);
}
The good thing about this approach is that comment
and setComment()
remains where they should be and you don't have to go to any other file other than CreateComment.jsx
to understand how comment
is being mutated.
We saw three ways to make sibling components commmunicate in React:
Did I miss to think about anything? Let me know in the comments. Thanks!
Comments