Performance implications of implementing 2-way data binding in React
Asked Answered
G

1

23

A common question of newcomers to React is why two-way data binding is not a built-in feature, and the usual response includes an explanation of unidirectional data flow along with the idea that two-way data binding is not always desirable for performance reasons. It's the second point that I'd like to understand in more detail.

I am currently working on a form library for apollo-link-state (a new client-side state management tool from Apollo). The concept is very similar to redux-form except using apollo-link-state instead of redux as the state manager. (Note that form state is stored separately from the state of domain entities, although an entity can optionally be used to populate the initial state of a form.)

When the user makes changes on the form, the library immediately updates the store via onChange handlers. I was thinking about allowing individual fields to opt-out of that behavior in case the programmer was concerned about performance, but then I started wondering when this would ever be a real performance issue. The browser is going to fire the oninput event no matter what, so the only performance consideration I can think of is whether or not the store is updated as the user types. Granted there is the additional overhead of executing a mutation rather than just calling setState(), but that essentially just amounts to a couple additional function calls. And let's suppose that I weren't using apollo but just calling a function that updates some global store directly - what would be the performance consideration then?

My thinking is that if a form is going to support immediately updating the form state as the user types in one field, it might as well do so for all the fields. The user can only type in one field at a time, and I don't see the benefit of making the page sometimes faster (probably negligible) with some fields and sometimes slower with others. Furthermore, my library allows consumers to use whatever input components they want, so if the programmer just wants fewer state updates, they could just write a component that debounces React's onChange event or uses the browser's own change or blur event instead.

Am I missing something here? Is there some other reason why a user of my library would want to ignore changes for particular fields until the user submits the form? Or maybe a more useful option would be to ignore changes to the entire form (until submit)?

Here's a basic (greatly simplified) illustration of the basic concept behind my current approach:

// defined in a globally-accessible module
const formState = {
    // This somehow causes any dependent form components to re-render
    // when state changes
    update(formName, updatedState) {
        ...
    }
}
export default formState

...
// UserForm.js:

export default class UserForm extends PureComponent {
    componentDidMount() {
        formState.userForm = {
            firstName: '',
            lastName: '',
        }
    }

    handleChange(e) {
        const { target } = e
        formState.update('userForm', { [target.name]: target.value })
    }

    //...

    render() {
        const { userForm } = formState
        return (
            <form onSubmit={this.handleSubmit}>
                <label for="name">Name</label>
                <input id="name" type="text" onChange={this.handleChange} value={userForm.name} />

                <label for="email">Email</label>
                <input id="email" type="email" onChange={this.handleChange} value={userForm.email} />
            </form>
        )
    }
}

Finally, for the sake of completeness, I should mention that there are some API design considerations involved in this as well. Individual input components could have a slightly simpler design if I did not provide an option to opt-out of the automatic 2-way binding. I can post details if anyone is interested.

Grath answered 22/2, 2018 at 16:5 Comment(0)
B
22

2 way data binding implications

Starting from the first part of your question, there are two primary reasons for react not going with two way data binding:

  1. A single source of truth for data changes in a React app, hence less chances of bugs and easier debugging
  2. Performance benefits

In React, we can share state among different child components by lifting the state up to a common parent component. When a shared piece of state is updated, all the child components can update themselves. Here is a good example from the docs related to forms.

Talking about the performance benefits, two way data binding in some other context (say AngularJS) works by means of watchers watching different elements. This sounds easier (and less code than React's one way data flow) for a small number of elements, however as the number of your UI components/elements grow, so does the number of watchers. A single change in this case causes a lot of watchers to fire up in order to keep things in sync. This makes the performance a bit sluggish. In case of React, since there is only one way data flow, it's easier to determine which components need to be updated.

Handling state updates

Coming to the second part of your question, your state library provides the data to your form components causing any dependent components to update on state change, sweet. Here are my thoughts:

I was thinking about allowing individual fields to opt-out of that behavior in case the programmer was concerned about performance, but then I started wondering when this would ever be a real performance issue.

The store update in itself will happen pretty quick. JavaScript runs very fast, it's the DOM updates which often times causes bottlenecks. So, unless there are hundreds of dependent form elements on the same page and all of them are getting updated, you'll be just fine.

And let's suppose that I weren't using apollo but just calling a function that updates some global store directly - what would be the performance consideration then?

I don't think it'll have significant difference.

My thinking is that if a form is going to support immediately updating the form state as the user types in one field, it might as well do so for all the fields. The user can only type in one field at a time, and I don't see the benefit of making the page sometimes faster (probably negligibly) with some fields and sometimes slower with others.

Agreed with this.

My library allows consumers to use whatever input components they want, so if the programmer just wants fewer state updates, they could just write a component that debounces React's onChange event or uses the browser's own change or blur event instead.

I think most of the use cases would be solved with a simple input. Again, I don't see a performance benefit with fewer state updates here. Debounce could be useful if for example I'm running an API call on the input (and want to wait before the user stops typing).

Is there some other reason why a user of my library would want to ignore changes for particular fields until the user submits the form? Or maybe a more useful option would be to ignore changes for the entire form (until submit)?

I don't see a benefit in ignoring changes for a particular field or waiting until submit. On the other hand, when using forms, a common use case I come across implementing things is data validation. For example,

  • provide feedback to the user as and when he is creating a password
  • check if an email is valid
  • perform API calls to see if a username is valid, etc.

These cases would need the state to be updated as the user is typing.

tl;dr

You should be fine with updating state as the user is typing. If you're still concerned about performance, I would suggest to profile your components to isolate bottlenecks if any :)

Buran answered 25/2, 2018 at 20:58 Comment(5)
Thank you very much for your thorough answer. One question about: "A single change in this case causes a lot of watchers to fire up in order to keep things in sync." That's only the case because of how Angular is implemented, right? My library technically has a lot of "watchers" too - every input component has its own onChange handler in order to notify the Form when it's updated (via a callback obtained from this.props) - but they are very simple and there's just one handleChange method on the Form that handles all the state updates.Grath
So these onChange handlers basically function as events that are passed up to the parent component, which then reacts to the events, just how React is intended to work (I think). So this should result in better performance than Angular's implementation, right?Grath
In any case, yes -- if it seems sluggish at all, I should definitely do some profiling before using this in production, as you recommended.Grath
You're welcome! AngularJS implemented two way binding via dirty checking which resulted in a cascade of events getting fired in order to determine parts of the UI which had changed. In react, you're only updating the parent state which updates the dependent children. Perhaps react docs can explain this in more detail reactjs.org/docs/reconciliation.html :)Buran
Thanks for the link. It sounds like my approach is sound and should be efficient, but always good to understand the details :)Grath

© 2022 - 2024 — McMap. All rights reserved.