Linked State

One area Preact takes a little further than React is in optimizing state changes. A common pattern in ES2015 React code is to use Arrow functions within a render() method in order to update state in response to events. Creating functions enclosed in a scope on every render is inefficient and forces the garbage collector to do more work than is necessary.



The Nicer Manual Way

One solution is to declare bound component methods using ES7 class properties (class instance fields):

class Foo extends Component {
    updateText = e => {
        this.setState({ text: e.target.value });
    };
    render({ }, { text }) {
        return <input value={text} onInput={this.updateText} />;
    }
}Run in REPL

While this achieves much better runtime performance, it's still a lot of unnecessary code to wire up state to UI.

Another solution is to bind component methods declaratively, using ES7 decorators, such as decko's @bind:

Linked State to the Rescue

Fortunately, there is a solution in the form of preact's linkState module.

Earlier versions of Preact had the linkState() function built-in; however, it has since been moved to a separate module. If you wish to restore the old behavior, see this page for information about using the polyfill.

Calling linkState(this, 'text') returns a handler function that, when passed an Event, uses its associated value to update the named property in your component's state. Multiple calls to linkState(component, name) with the same component and name are cached, so there is essentially no performance penalty.

Here is the previous example rewritten using Linked State:

import linkState from 'linkstate';

class Foo extends Component {
    render({ }, { text }) {
        return <input value={text} onInput={linkState(this, 'text')} />;
    }
}Run in REPL

This is concise, easy to comprehend, and effective. It handles linking state from any input type. An optional third argument 'path' can be used to explicitly provide a dot-notated keypath to the new state value for more custom bindings (such as binding to a third party component's value).

Custom Event Paths

By default, linkState() will try to derive the appropriate value from an event automatically. For example, an <input> element will set the given state property to event.target.value or event.target.checked depending on the input type. For custom event handlers, passing scalar values to the handler generated by linkState() will simply use the scalar value. Most of the time, this behavior is desirable.

There are cases where this is undesirable, however - custom events and grouped radio buttons are two such examples. In these cases, a third argument can be passed to linkState() to specify the dot-notated key path within the event where a value can be found.

To understand this feature, it can be useful to peek under the hood of linkState(). The following illustrates a manually created event handler that persists a value from within an Event object into state. It is functionally equivalent to the linkState() version, though does not include the memoization optimization that makes linkState() valuable.

// this handler returned from linkState:
handler = linkState(this, 'thing', 'foo.bar');

// ...is functionally equivalent to:
handler = event => {
  this.setState({
    thing: event.foo.bar
  });
}Run in REPL

Illustration: Grouped Radio Buttons

The following code does not work as expected. If the user clicks "no", noChecked becomes true but yesChecked remains true, as onChange is not fired on the other radio button:

import linkState from 'linkstate';

class Foo extends Component {
  render({ }, { yes, no }) {
    return (
      <div>
        <input type="radio" name="demo"
          value="yes" checked={yes}
          onChange={linkState(this, 'yes')}
        />
        <input type="radio" name="demo"
          value="no" checked={no}
          onChange={linkState(this, 'no')}
        />
      </div>
    );
  }
}Run in REPL

linkState's third argument helps here. It lets you provide a path on the event object to use as the linked value. Revisiting the previous example, let's explicitly tell linkState to get its new state value from the value property on event.target:

import linkState from 'linkstate';

class Foo extends Component {
  render({ }, { answer }) {
    return (
      <div>
        <input type="radio" name="demo"
          value="yes" checked={answer == 'yes'}
          onChange={linkState(this, 'answer', 'target.value')}
        />
        <input type="radio" name="demo"
          value="no" checked={answer == 'no'}
          onChange={linkState(this, 'answer', 'target.value')}
        />
      </div>
    );
  }
}Run in REPL

Now the example works as intended!

Built by a bunch of lovely people like @rpetrich.