-
Notifications
You must be signed in to change notification settings - Fork 270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow callback style setState #1
base: master
Are you sure you want to change the base?
Allow callback style setState #1
Conversation
Want me to write some basic tests? |
src/unstated.js
Outdated
setState(state: $Shape<State> | (State => $Shape<State>)) { | ||
this.state = | ||
typeof state === 'function' | ||
? state(this.state) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this still be merged back into the existing state?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. In the example it would just allocate an unnecessary empty object and key-iteration round (with Object.assign), as lodash/fp
/ ramda
take care of immutability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello, I'd prefer it to stay that way so it reflects React's default behaviour.
Thanks again @jamiebuilds for this excellent project.
EDIT: Today I learned that the callback setState is merged back to the state! 💡
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about this?
- Run updater
- Check for null / undefined or identity equal to previous state
- If not, assign over defensive copy
step 2 would mean that if you bail early in your updater that decision is propagated to your components. @jamiebuilds?
Sure, sorry there aren't any already. I wanted to publish it before I stayed up to 2am |
Haha fair enough. Will add tests + when we iron out the above comment appropriate documentation. |
* and leaving old state in place. | ||
*/ | ||
if (newState == null || newState === prevState) { | ||
this.state = prevState; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this even necessary? Or should I just return?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assignment indicates that it changes something, I think return
would be more appropriate here
* propagate correctly through component tree by not calling _listeners | ||
* and leaving old state in place. | ||
*/ | ||
if (newState == null || newState === prevState) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think returning early if a shallow comparison fails would be a breaking change, and incompatible with React's setState
behavior. React docs discourage returning mutated state from the setState
updater function (or passing a mutated state object to setState
), but neither is prohibited. From the React docs:
setState()
will always lead to a re-render unlessshouldComponentUpdate()
returns false.
An example demonstrating a successful state-mutation update is here.
My second concern is that React emits its own development-time console warnings for passing null
to setState
, and by returning silently we might prevent a developer from realizing they have made a mistake.
For both of those reasons, I think it might be best to eliminate the conditional early return altogether, and let React do its thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So in other words, I would think about rolling back ff77c96.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setState() will always lead to a re-render unless shouldComponentUpdate() returns false.
I think this is not true with react@16. I don't use this feature but returning null
is supposed to prevent scheduling a re-render.
What we might need to introduce here is 2 distinct paths for objects & function styles. Because of the null
issue we cannot combine both and use newState
in following lines, it should be restructured to something like:
setState(state: $Shape<State> | (State => $Shape<State>)): void {
const prevState = this.state;
let newState
if (typeof state === 'function') {
newState = state(prevState)
if (newState === null) {
return;
}
} else {
newState = state
if (process.env.NODE_ENV !== 'production' && newState === null) {
console.warn('write a nice dev warning here')
}
}
if (newState === prevState) {
return
}
this.state = Object.assign({}, prevState, newState);
this._listeners.forEach(fn => fn());
}
Does it make sense to u?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I understand how to get there code-wise, I'd just like to have @jamiebuilds's input on what the desired behavior is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, the API should mirror React's API as closely as it can. So I'd say we should just follow that path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah since this is already only conceptually similar to setState, I'm not convinced we should be following React to the letter vs doing the principally right thing. Their API tradeoffs aren't necessarily the same as those of a pure state manager. I'll update tomorrow, and hopefully get some time in to write those tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want it to match the React behaviour as closely as possible. Refactoring component state into container state should be near seamless
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow my mistake! I was operating under the impression the Subscribe
component's onUpdate()
subscription callback was receiving the updated state and passing it to setState()
. I should have read more closely. 🙂
@StevenLangbroek Given the above discussion I concede it makes sense to have custom error reporting and perhaps disregard what React does or does not report as a mistake.
However, the documentation for Unstated is currently written without any specification for setState()
. The implication is that setState
should behave the same way you would expect already, having worked with React. That means:
null
prevents a re-render- But equal object identity does not prevent a re-render.
If there's an intent to change that, it should be documented well, and also be considered a breaking change which would merit a new major version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jamiebuilds I missed your reply, but I guess I should have just let your comment sit. 😄 I appreciate that sentiment - it's my favorite thing about this library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (process.env.NODE_ENV !== 'production' && newState === null) { console.warn('write a nice dev warning here') }
Hi @StevenLangbroek , can we make one constant file and manage this kind of string production
? because in code we are using this kind of thing repeatedly and so instead of doing repetitively we can handle it in one file use.
Any progress on a fix for this? |
* propagate correctly through component tree by not calling _listeners | ||
* and leaving old state in place. | ||
*/ | ||
if (newState == null || newState === prevState) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (process.env.NODE_ENV !== 'production' && newState === null) { console.warn('write a nice dev warning here') }
Hi @StevenLangbroek , can we make one constant file and manage this kind of string production
? because in code we are using this kind of thing repeatedly and so instead of doing repetitively we can handle it in one file use.
this.state = Object.assign({}, this.state, state); | ||
setState(state: $Shape<State> | (State => $Shape<State>)): void { | ||
const prevState = this.state; | ||
const newState = typeof state === 'function' ? state(prevState) : state; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of "function", can we make it in a constant file and export it @StevenLangbroek
This PR allows the "callback" style
setState
, which React also allows. Where React allows it for asynchronicity, the change here would allow advanced users who are aware of (im)mutability concerns (which you take care of with Object.assign, to a certain extent) to make all state changes with a single pass oflodash.flow
orR.evolve
:"Advanced" (imagine airquotes, thanks :) ).
Todo