DEV Community

Cover image for One-way data flow: Why?
LUKESHIRU for Laser Reindeer

Posted on • Updated on

One-way data flow: Why?

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>
    );
};
Enter fullscreen mode Exit fullscreen mode

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>
    );
};
Enter fullscreen mode Exit fullscreen mode

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 inside InputUsername and InputPassword), which makes tracking state changes way harder and less predictable.
  • InputUsername and InputPassword now can only be used where the state has a string state for their values, if the state evolves to be more complex (let's say an object), then instead of just updating LoginPage, you have to update InputUsername and InputPassword as well.
  • InputUsername and InputPassword can't be reused in other places if the state is different, so because we changed them to be simpler to use in LoginPage, we made them harder to use anywhere else.
  • Because InputUsername and InputPassword 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 }) => {};
Enter fullscreen mode Exit fullscreen mode

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 }) => {};
Enter fullscreen mode Exit fullscreen mode

So the parent can react if it wants to those events.

That's it for this article,
thank you for reading!

Discussion (12)

Collapse
vladislavmurashchenko profile image
VladislavMurashchenko • Edited on

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

Collapse
lukeshiru profile image
LUKESHIRU Author

My first post in dev.to was about the importance of naming, but in this particular example updatePassword is not the same as onChange. The onChange in the first example is actually the onChange 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.

Collapse
vladislavmurashchenko profile image
VladislavMurashchenko

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

Thread Thread
lukeshiru profile image
LUKESHIRU Author

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.

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 the event.target, event.preventDefault, event.stopPropagation, and so on. Effectively you took away part of the parent control. Let's shorten the JSX:

<InputUsername
  onChange={/* Expects an event handler */}
  updateUsername={/* Expects a state setter */}
/>
Enter fullscreen mode Exit fullscreen mode

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 the updateUsername 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:

Parent -> render -> Child
Parent <- event  <- Child
Parent -> update -> Child
Enter fullscreen mode Exit fullscreen mode

And not something like this:

Parent -> render -> Child
Parent <- update <- Child
Enter fullscreen mode Exit fullscreen mode

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 being false. 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).

Thread Thread
vladislavmurashchenko profile image
VladislavMurashchenko

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

Thread Thread
lukeshiru profile image
LUKESHIRU Author • Edited on

If you're creating a component just to be used by yourself, I understand that...

I personally almost never use all that imperative event methods. They are really rarely needed.

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 type ChangeEvent<Input>, so devs using your component can use that event the same way they will use the onChange in a regular input. No surprises. If they have hooks/utils designed for the native input, they will also work with the UsernameInput component.

Take the Link component from react-router-dom. It only wraps an a tag, and adds an event listener to it, you can still prevent that event, or use all the properties you'll use in a regular a.
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:

But at also nothing bad to provide one more event up from component for value only, like onValueChange.

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:

const getValue => handler => event => handler(event.currentTarget.value);
Enter fullscreen mode Exit fullscreen mode

And then use it like this:

<UsernameInput onChange={getValue(/* State setter here */)} />
Enter fullscreen mode Exit fullscreen mode

Or even a custom hook:

import { useCallback, useState } from "react";

export const useEventValue = defaultValue => {
    const [value, setValue] = useState(defaultValue);
    const setValueFromEvent = useCallback(
        event => setValue(event.currentTarget.value),
        [setValue]
    );

    return [value, setValueFromEvent];
};
Enter fullscreen mode Exit fullscreen mode

And then use it like this:

const [value, onChange] = useEventValue();

return <UsernameInput {...{ value, onChange }} />
Enter fullscreen mode Exit fullscreen mode

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

Thread Thread
vladislavmurashchenko profile image
VladislavMurashchenko

Yes, I used such util but I like to call it forInput forCheckbox and so on, because
onChange={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...

Thread Thread
lukeshiru profile image
LUKESHIRU Author

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:

  1. Take a look at the Checkbox and Input components. Both are just wrapping input and setting some default properties on it, everything else is what you can expect from a regular input element.
  2. Both CheckboxField and InputField now have a simpler implementation making use of Checkbox and Input, but you don't loose control over the actual label (which you can access trough the labelProps prop). With TS 4.2 and up, you can use types like the one I created in the types directory called Uses, and instead of having labelProps, you'll had properties prepended with label. So stuff like labelClassName to change the className of the label.
  3. Take a look at the type JSXProperties. It takes a TagName and returns all the JSX properties of that element, useful to "extend" the native elements.
  4. Several properties were made optional, mainly because ideally you should make all your properties optional (just like in a native element), which makes you cover more edge cases.
  5. The wrap util used in both Checkbox and Input, basically takes the tagName you want to extend with React, and wraps it with a memo and a forwardRef so it behaves more like a preact 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 regular input[type=checkbox] element, just with some defaults set. CheckboxField is a little less usable, but still you have full control over it. Finally PersonFields 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.

Thread Thread
vladislavmurashchenko profile image
VladislavMurashchenko

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.

  1. Checkbox and Input components used only once inside CheckboxField and InputField. I prefer to extract additional abstractions only when I need them. Maybe you will never need your Input and Checkbox without label. So it extra code for future.
  2. wrap do optimization which don't needed and also never work because callbacks are not wrapped into useCallback. So currently it just do nothing at all, just extra code for future.
  3. UserForm now provide onSubmit which never used. Extra code for future.
  4. All those extra props povided with spreed and ts utils are also never used. Again 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.

Thread Thread
vladislavmurashchenko profile image
VladislavMurashchenko • Edited on

All this idea about make your components reusable from somewhere else probably violation of YAGNI principle.
Components like CheckboxField and InputField 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 yet

Thread Thread
lukeshiru profile image
LUKESHIRU Author

The 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 like type, or value 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

Thread Thread
vladislavmurashchenko profile image
VladislavMurashchenko

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