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} />;
}
}
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')} />;
}
}
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
});
}
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>
);
}
}
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>
);
}
}
Now the example works as intended!