loading...

One-way data flow: Why?

lukeshiru profile image ▲ LUKE知る ・3 min read

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 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 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 own state directly, which is really 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) trough props.
  • State should be updated by the parent itself, reacting to events of its children.

Basically your components should avoid having 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) trough 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!

Posted on by:

Discussion

pic
Editor guide