EDITED
Well, maybe using Mediator is not a good idea :)
We can use event (or event-like) to send message from children to parent component.
Put the state in the parent component is not a bad idea.
See this comment please!
And thank you @lukeshiru, you are the best one!!!
Original Post
Hello everyone. When we try to code in React, we usually write some dialog component with some input component, some button component, and something else.
It is always hard to re-use those input component. Because its behavior is rely on other components.
As you see, in the normal way, the input component need to know how does the dialog work (to set the state of the dialog). And the submit button also need to know how to get the state and send a request to server. And the submit button also need to know how to show the error message (in here, the button need to set the state errorMsg
to "error"
.
We can pass the props in function type. To let those component can be called the 'callback' function. for example:
- Pass the input component a function prop to deal with the event
onChange
. Now when we change the input component, the state of dialog will be changed. - Pass the submit button component a function prop to set the
errorMsg
.
Now those sub-component does not rely on the dialog component. Those component now just rely on those callback function. It is really good. But can it be better?
- If the dialog component has many input components, it will hold many states. Can we just let sub-components hold the states? And can we let the sub-components have a method to get its value?
- In normal way, only the parent component can deal with the logic. What will happen if the parent need to deal with a lot of events? Can the sub-component have methods to be called?
We do not want to let the input components rely on the dialog component, because we want to re-use those in the future. So the best way is let the sub-components rely on the interface of the parent component. In here, the sub-component rely on a function notify
to call parent component. And the parent's job is just to coordinate sub-components.
For example, now I have a parent component. Its job is to send a request to server with an email address:
const Dialog = () => {
const inputRef = useRef(null);
const submitRef = useRef(null);
const notify = (eventName, data) => {
console.log(eventName, data);
if (eventName === 'submit') {
const email = inputRef.current.getContent();
submitRef.current.submit(email);
} else if (eventName === 'error') {
inputRef.current.setErrorMsg(data);
}
}
return (
<div className={styles.dialog}>
<Input ref={inputRef} />
<Submit notify={notify} ref={submitRef} />
</div>
)
}
As you see, the dialog component rely on two components: Input
and Submit
. But Input
and Submit
do not rely on Dialog
. They rely on an interface with a method notify
.
It is good. It means we can use those components Input
or Submit
in another component, which has a method notify
.
Then we need to define Input
:
const Input = forwardRef((_prop, ref) => {
const [content, setContent] = useState('');
const [errorMsg, setErrorMsg] = useState(null);
useImperativeHandle(ref, () => ({
getContent: () => {
return content;
},
setErrorMsg: (newErrorMsg) => {
setErrorMsg(newErrorMsg);
}
}), [content]);
return (
<>
<input
ref={ref}
className={styles.input}
onChange={(e) => setContent(e.target.value)}
/>
{ errorMsg ? <span>{errorMsg}</span> : null }
</>
)
})
It has two method: getContent
and setErrorMsg
. So the parent do not need to handle what is the content
of the component. Neither know the errorMsg
. It is just hold by the Input
component.
And here is another component Submit
:
const Submit = forwardRef(({ notify }, ref) => {
useImperativeHandle(ref, () => ({
submit: (email) => {
console.log('Try to fetch email:', email);
console.error(`Sorry it (${email}) is not the vaild email`);
notify('error', `Sorry it (${email}) is not the vaild email`);
}
}));
return (
<button
ref={ref}
className={styles.submit}
onClick={() => notify('submit')}
>
Submit
</button>
)
});
Cool right? The Submit
component will ask Dialog
to submit. And the Dialog
will call its method to submit to a server. It is helpful if you want to put the submit button into another component.
Top comments (11)
I have a suggestion. I'd completely remove the usage of refs to start with. They make code really hard to follow and cross-connect everything. It works fine without refs :)
Lets start with the input component. It gets everything feeded from the outside:
Nice, clean and simple. If an error gets passed to the component, it will display one. If the error is taken away, it vanishes. I'd add more things like an
id
,name
and other things but lets leave them out for the sake of a simple example.Lets go to the dialog next:
The dialog uses a hook named
useFormSystem
which takes the target URL for the form data and maybe a callback function to call so components further up in the tree might close the dialog after successful submission.Lets see how that custom hook is built:
This is a possible approach I would choose. Everything is written from the top of my head and has not been tested but I wrote it to give a basic example without refs. And something that is highly re-usable.
It can be easily extended so you can pass a TS interface to type your complete form data here. You might want to use constants instead of writing
"email"
repeatingly in the getters.You might want to use client-side validation to not being forced to send everything to the server to get some feedback.
OMG, you are right. I really like your solution. And I believe it is the really best solution! We can try to create our hook to re-use. The basic components are just renders and send event to their parent.
I will update my post! Thank you very much!
I'm glad I could help out. My solution is just a quick sketch of what can be done with hooks. Its certainly far from being the best solution.
But I am happy I could give you some new perspective :)
By the way, In my opinion, the function
notify
is not only ask parent-component to do what, and also ask parent-component for data. For example, in sub-componentSubmit
, we want to get the email address from parent-componet, we can use:We do not want to let the parent have the email adress, so we cannot get the email adress by props. But we can ask parent to get it.
If you want to get it, you can also use
getEmail={() => notify("getEmail")}
. But longer.If the code is using typescript, it will be better. Let us make the type of
notify
be a object, whose attrs are all function. Each sub-component can define what type ofnotify
they want, and the parent must finish the union of thosenofity
types.Hello, Luke Shiru. I like
onXxxxxx
prop. It is useful if you want to let sub-component change the parent-component's state. In here, you can use<Submit onSubmit={() => notify("submit")} onError={(errorMsg) => notify("error", errorMsg)} />
. But longer.Yes yes! You are right. I will update my post! Thank you very much! :)
You are so kindly! I will just use one thread next time. This is my first time to get comment.
Thanks. I agree!!! Using
on
is much easy.What do you think? Do you think there is some way better than this?
I'm not entirely sure what makes this better than just using plain JS events.
I am not sure too. Welcome to talk about it.
I meet some trouble when I want to re-use some sub-component. When I try to detach the sub-component from the dialog, I find I need to store the state in the parent component. Because other sub-components need it.
Well, but it works. The state is hold in parent-component. And pass some 'callback' functions as sub-components' props. But it looks not good.
I hope the sub-component can hold its state into itself. I do not think that put sub-components' state into parent-component is a good idea if there are a lot of sub-components.
For example, in normal way, there is a submit button. It want to send a message to a server with some information from other sub-components. There are two way to do it:
onSubmit
as the prop of the submit button. How we need write the code for submit in parent.