Take into consideration that this article is a few years old at this point, so you might want to look for a more recent version with modern practices. At the time of writing of this message the oficial React Docs are finally being updated, so maybe when you read this they are out of beta already. Remember to always look for up-to-date tutorials and documentation.
One possible question that can arise from the use of libraries like React is: Why is "one-way data flow" always listed in the "best practices" guides?
To understand the reasoning behind that, we need to see it in practice and then we will learn the theory behind it. Let's start with a ...
One-way data flow Login
Let's say we have this LoginPage
component, which uses Form
, InputUsername
, InputPassword
and ButtonSubmit
:
// These are just wrapping html with some default props
const Form = props => <form {...props} />;
const InputUsername = props => <input type="text" {...props} />;
const InputPassword = props => <input type="password" {...props} />;
const ButtonSubmit = props => <button type="submit" {...props} />;
// The juicy part:
const LoginPage = () => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const login = event => {
event.preventDefault();
// Hit endpoint with username and password
};
return (
<Form onSubmit={login}>
<InputUsername
value={username}
onChange={event => setUsername(event.currentTarget.value)}
/>
<InputPassword
value={password}
onChange={event => setPassword(event.currentTarget.value)}
/>
<ButtonSubmit>Login</ButtonSubmit>
</Form>
);
};
The approach is pretty standard one-way data flow, LoginPage
has a state for username
and password
, and when InputUsername
or InputPassword
change, the state is updated in LoginPage
. So let's "optimize" this to use two-way data flow instead.
Two-way data flow Login
This is the same LoginPage
, but now InputUsername
and InputPassword
do more than just informing about their state:
const Form = props => <form {...props} />;
// InputUsername now takes an updateUsername callback which sets
// the state of the parent directly
const InputUsername = ({ updateUsername, ...props }) => (
<input
type="text"
onChange={event => updateUsername(event.currentTarget.value)}
{...props}
/>
);
// InputPassword does the same thing
const InputPassword = ({ updatePassword, ...props }) => (
<input
type="password"
onChange={event => updatePassword(event.currentTarget.value)}
{...props}
/>
);
const ButtonSubmit = props => <button type="submit" {...props} />;
const LoginPage = () => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const login = event => {
event.preventDefault();
// Hit endpoint with username and password
};
// But hey! look! Now this is simpler! So this is ok, right?
// Wrong! This is just the beginning of a mess.
return (
<Form onSubmit={login}>
<InputUsername value={username} updateUsername={setUsername} />
<InputPassword value={password} updatePassword={setPassword} />
<ButtonSubmit>Login</ButtonSubmit>
</Form>
);
};
If you run both examples you get the same behavior, so it can give the impression that both are the same. Based on that, the developer might think the second being simpler when being used is better, but that's not the case.
Why not two-way data flow?
The short answer is that the cost of maintenance increases a lot.
While the two-way example seems to have a simpler usage for InputUsername
and InputPassword
than the one-way, the reality is that the two-way approach introduced the following issues in exchange for that "simplicity":
- The state of the LoginPage now is updated in several places (inside
LoginPage
and insideInputUsername
andInputPassword
), which makes tracking state changes way harder and less predictable. -
InputUsername
andInputPassword
now can only be used where the state has astring
state for their values, if the state evolves to be more complex (let's say an object), then instead of just updatingLoginPage
, you have to updateInputUsername
andInputPassword
as well. -
InputUsername
andInputPassword
can't be reused in other places if the state is different, so because we changed them to be simpler to use inLoginPage
, we made them harder to use anywhere else. - Because
InputUsername
andInputPassword
update the state directly, they are effectively updating their state directly, which is bad if you want to do something with that state besides updating it (let's say for example running some validation, blocking some characters, and so on).
So then, why one-way is better then?
Let's start with the short answer again: Because is easier to maintain, understand/read/review, and so on. Basically because is in line with KISS.
One-way encourages the developers to keep their components simple, by following certain rules about state management and props:
- State should travel downwards (from parent component to children) through props.
- State should be updated by the parent itself, reacting to events of its children.
Your components should avoid having a state or altering the state of the parent, they have to set all internal values with props and should inform of anything happening in them (clicks, inputs, and son on) through events (onClick
, onInput
, and so on).
How to spot bad practices
Generally, the names of the props being used in a component are a red flag. If a component looks like this:
const AComponent = ({ updateFoo, setBar, applyFoobar }) => {};
You have callbacks with prepends like update
, set
, apply
, which usually means that those are expecting to update/set/apply values, and they shouldn't. Instead, that should look more like this:
const AComponent = ({ onFoo, onBar, onFoobar }) => {};
So the parent can react if it wants to those events.
That's it for this article,
thank you for reading!
Top comments (12)
It seems that in your particular example of bad practice you can just rename "updateUsername" and "updatePassword" to "onChange", so it already not a setter but event like prop. Because, probably you usually don't need event object itself like output of this event prop. If you need, it will be possible to add "onEventChange" later
My first post in dev.to was about the importance of naming, but in this particular example
updatePassword
is not the same asonChange
. TheonChange
in the first example is actually theonChange
of input, so it has all the event data.updatePassword
on the other side only has the input value and is expecting to receive a state setter instead, which is not good if you want to use it in other scenarios.The main problem with the double binding approach is that you're leaking logic of the parent into the child components, so those components will not work with other parents (no reuse).
You can see all the problems in the Why not two-way data flow? section of the article.
But I mean, that it is not important to make it parent responsibility getting value from event. It is quite ok, to keep this responsibility for Input component. Input doesn't handle any parent logic in your example, it just take value from event and throw it up to parent component, so parent component can do what he want.
For example you can have SameDataManipulationComponent which takes data and provide onDataChange event to parent.
So this component responsible of how to change data. And it is still one way data binding
Not really. You decided that the parent doesn't need the event from the child component, but if you only receive the
event.currentTarget.value
of the event, you loose all other event information and methods, such as theevent.target
,event.preventDefault
,event.stopPropagation
, and so on. Effectively you took away part of the parent control. Let's shorten the JSX:onChange
being just an event, is there to let the parent know that something happened, so the parent is the one that will actually "update the username" if it wants (maybe there is some logic that doesn't allow to use some characters, or something like that, easy to prevent with an actual event handler).updateUsername
expects to receive the state setter and make the change itself, so the responsibility to update the state is in the child component. If that component changes and adds more logic to theupdateUsername
callback, then that will affect the state of the parent effectively making it double binding.A component shouldn't expect a state setter as a property, because basically is expecting to do the update itself. Components should only "let the paren know" that something happened, and is up to the parent to do something about it. So something like this:
And not something like this:
One other useful thing, is that you can have preventable behaviors in the child component. So if you want to add extra logic to the event at the child level, you can do it after calling the parent event, and then wrap everything in a condition that depends on
event.defaultPrevented
beingfalse
. That way the parent can even prevent behaviors with the regular event API instead of having to add yet more properties for just that in the child that doesn't care about that logic (again, making it more reusable and predictable).I personally almost never use all that imparative event methods. They are really rarely needed.
maybe use onChange name can be for DOM event itself.
But at also nothing bad to provide one more event up from component for value only, like onValueChange.
Sure you should never have do parent logic inside child, but you can provide convenient interface for you events. And it is not violation of one way data flow. It is just:
Parent <- prepared event <- Child
You think about it like child control the state of parent, but really the only thing child do is transform event before provide it to parent
e => e.target.value
Is just event transformation, not a part of parent logic until you do something else with event
If you're creating a component just to be used by yourself, I understand that...
But the ideas presented here are for a more general/broad use of a component.
The best components are those that just extend the native ones, because all their properties are predictable for anyone using your component. The
onChange
event will contain an event of typeChangeEvent<Input>
, so devs using your component can use that event the same way they will use theonChange
in a regular input. No surprises. If they have hooks/utils designed for the nativeinput
, they will also work with theUsernameInput
component.Take the
Link
component fromreact-router-dom
. It only wraps ana
tag, and adds an event listener to it, you can still prevent that event, or use all the properties you'll use in a regulara
.You can design your internal components to not send the entire event up, adapted to your particular needs, but if you make them as suggested by this post, you can reuse those components all over that project, or in other projects even. You are able to even put components in a separate package and import them as such from several projects.
I agree a bit when you say:
Maybe it makes sense to create a new event exposing only the value and nothing else, but you could also have a generic util for that, like:
And then use it like this:
Or even a custom hook:
And then use it like this:
And the best thing about taking the route of hooks/utils? They can be reused by any input (even the native ones), and you don't have to pollute the props of your component :D
Yes, I used such util but I like to call it
forInput
forCheckbox
and so on, becauseonChange={forInput(setSomeState)}
more convenient to read.
I understand your point about keeping onChange for event if we talking about input. But at the same time I prefer write
onValueChange
once when I create component then wrap setter to util each time when using.Also this approach is good in general for cases like:
Parent own some peace if data
Child have to getting this peace and responsible for manipulations with it.
Sure Child here is container component which know some business and not going to be reused.
Such an approach is useful for big nested forms. I even have an example. Maybe it will help you to understand my point.
You can write your version if you like, it would be interesting for me
codesandbox.io/s/github/VladislavM...
Loved that you used TypeScript so nicely :D ... I did some changes here and there: codesandbox.io/s/priceless-dream-l...
Some things to take from my changes are:
Checkbox
andInput
components. Both are just wrappinginput
and setting some default properties on it, everything else is what you can expect from a regularinput
element.CheckboxField
andInputField
now have a simpler implementation making use ofCheckbox
andInput
, but you don't loose control over the actuallabel
(which you can access trough thelabelProps
prop). With TS 4.2 and up, you can use types like the one I created in thetypes
directory calledUses
, and instead of havinglabelProps
, you'll had properties prepended withlabel
. So stuff likelabelClassName
to change theclassName
of thelabel
.JSXProperties
. It takes aTagName
and returns all the JSX properties of that element, useful to "extend" the native elements.wrap
util used in bothCheckbox
andInput
, basically takes the tagName you want to extend with React, and wraps it with amemo
and aforwardRef
so it behaves more like apreact
element, and also so it infers all the properties of the wrapped element automatically.What I want to highlight is what I mentioned before of trying to make your components reusable from somewhere else.
Checkbox
for example can be now reused anywhere in this app (or any other), and the API is the same of a regularinput[type=checkbox]
element, just with some defaults set.CheckboxField
is a little less usable, but still you have full control over it. FinallyPersonFields
is the component that is more dependent of the current app. Ideally this last one should be turned into a more generic component that takes an array of "fields" and render them, but for this particular example and being just for a Dev.to comment, is fine enough.Thanks for you time on that :)
You solution is smart and interesting, especially this mind-blowing ts utils, probably I need to spent some hours to understand them :)
But I honestly think none of you do here is needed for such a small example.
Checkbox
andInput
components used only once insideCheckboxField
andInputField
. I prefer to extract additional abstractions only when I need them. Maybe you will never need yourInput
andCheckbox
without label. So it extra code for future.wrap
do optimization which don't needed and also never work because callbacks are not wrapped intouseCallback
. So currently it just do nothing at all, just extra code for future.UserForm
now provideonSubmit
which never used. Extra code for future.There is YAGNI principle which say that we don't need to implement those things which we don't have in requirements yet (but yes, we should our code extensible enough to do it later).
Ron Jeffries:
Always implement things when you actually need them, never when you just foresee that you need them.
All this idea about
make your components reusable from somewhere else
probably violation of YAGNI principle.Components like
CheckboxField
andInputField
were reusable, maybe you will need to extend them to fit new cases but at least, they don't cover usecases which don't exists yetThe idea is that if you think about it, I spend a little more time creating utils like
wrap
(I actually copied them from a private component repo I maintain, which is used in a bunch of projects to construct the UI of some webapps), and then less time wrapping components, because I don't need to create the types for stuff liketype
, orvalue
like you had to do, those are inferred because I'm wrapping an input.I'm not covering "all the cases", I'm just covering the extra things that I need (like the
label
prop, and setting the default classnames), and then all the rest are already covered by standard html.In short, every time you need to control a new property from that
Input
(let's say you want to add aria labels), you need to update the Prop types, then update the component to use those props, and so on. Mine in practice is an input with extra stuff, so I only need to go back to it if I need to add something extra to the native input.One thing you might need to consider is that my point of view is based on libraries of components indented to be reused by more than 1 project, so the scope is quite wider. I agree that for components only used in one app it might make sense to make them simpler, but my point in this article is that, at least in my experience, having this reusable approach in components for any app is quite powerful.
At work we were able to move the components directory to its own repo, and reuse those from other apps with no problem, because they weren't made to be used just in one place in a certain way. Those kind of migrations are kinda harder when you have tighter coupling to your app.
Still, thanks for engaging in such an interesting debate. I might update the article later adding details like "is fine to be sloppy in smaller scope projects", so it doesn't give the idea that you have to do it this way everywhere :D
Yep, I agreed that all that stuff have sense if you are going to make UI library out of it.
But when we just building components for our projects - better to keep them simpler and focused on your current use case and at the same time extendable and decomposable