Closed dzearing closed 3 years ago
So far, the mutable draft state has been very useful and performant. However, there is still concern that this isn't intuitive and contradicts the expectations of hooks. (Clear immutable ins and outs.)
We may want to use a naming convention to separate hooks which operate on draft state. The integration works very similarly to "mixins" of the past. Another common term is "behaviors".
How about a "Mixin" suffix?
const Button = React.forwardRef((props, ref) => {
const { render, state } = useButton();
// The "Mixin" suffix indicates that classes will be mixed into the state.
useButtonClassesMixin(state);
return render(state);
});
👍 Clearly indicates the hook "mixes stuff into state". 👎 Wordy, makes for a long function name.
Because this issue has not had activity for over 150 days, we're automatically closing it for house-keeping purposes.
Still require assistance? Please, create a new issue with up-to date details.
This relates to PR #14268 (compose replacement updates, introducing
mergeProps
,getSlots
, andresolveShorthandProps
.)Proposal
When creating component hooks, merge state by creating "draft state", allowing hooks to mutate that, in an effort to reduce perf overhead in constructing the final state for rendering.
Problem
Trying to keep hooks immutable forces multiple, unnecessary objects to be created between each hook, holding a slight deviation of state, forcing awkward merging between calls to ensure state is properly piped through them. This creates perf overhead and awkward code. We should evaluate patterns which avoid any extraneous overhead.
Solution
Prior art: https://immerjs.github.io/immer/docs/introduction
In immer, the
produce
helper is used to consume input, create draft state, allow changes to be applied, and the final result is returned to be consumed.Likewise, we'd like to evaluate a similar solution for weaving state through some of our core hooks:
This has the following benefits:
Details
Many hooks take in state, read some values, and adjust those values. Small behavior hooks almost always do this.
Often the state calculations must be chained through multiple hooks which may manipulate parts of the state.
There were numerous error-prone places which could have easily made an error using the wrong value:
useVariant
referred toprops.className
, orstate.className
, it would not have used the computed value from the previous hook.Some of these problems can be simplified by simplifying the inputs and ouputs to take in state and return new state:
Now each hook can be responsible for correctly updating the state without accidents. However, it requires each hook to properly clone as neeeded, and the consumer must weave the state through each hook as shown.
We can simplify this further by simply creating a clone up front and letting the hooks mutate the state:
Benefits:
Drawbacks:
draftState
)One size does not fit all
In some cases, this is overkill. Simple components which wrap a primitive and attach styling may be a case where a deep clone seems unneeded;
Details in
mergeProps
deep clone exceptionsWhen creating draft state, deep merging would be used, with these exceptions being that they would be treated as literals and replace the previous value rather than be walked.
className
props are always joined properlyThis means that if you merged 2 functions:
Last one wins. (
// { onClick: () => console.log('B') }
) Same with JSX, arrays, ref objects, and class instances.These could be manually patched if needed to be merged:
Refs would be good to auto-merge, but we can't easily distinguish a ref from a callback function or object.