Skip to content
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

fix for #754 prevent ColorWrap to reset own state #791

Closed
wants to merge 1 commit into from

Conversation

adamborowski
Copy link

@adamborowski adamborowski commented Dec 2, 2020

The issue is that ColorWrap HOC manages color state but at the same time accepts "color" prop which affects the state in getDerivedStateFromProps.
Because getDerivedStateFromProps is called every render, it always replaces state without actual checking if the color property was changed.
In the demo https://codesandbox.io/s/custom-picker-new-react-ur2z4 mentioned in #754 the component has initial value 'orange' - but it will overwrite component internal state every render.

I added a step to check if property was changed to prevent resetting internal state.

But to be honest I suggest to change the API of the component to avoid confusion.
In the example

<MyPicker color="orange" onChangeComplete={handleColorChange} />

Color prop is always passed so it should never change it by its own.
If the intention is to provide initial value for the component, I think we should add 'defaultValue' property to indicate that it will be used only once.

The issue is that ColorWrap HOC manages color state but at the same time accepts "color" prop which affects the state in getDerivedStateFromProps.
Because getDerivedStateFromProps is called every render, it always replaces state without acutal checking if the color property was changed.
In the demo https://codesandbox.io/s/custom-picker-new-react-ur2z4 mentioned in casesandberg#754 the component has initial value 'orange' - it will derive to state every render.

I added a step to check if property was changed to prevent resetting internal state.

But to be honest I suggest to change the API of the component to avoid confusion.
In the example
```tsx
<MyPicker color="orange" onChangeComplete={handleColorChange} />
```
Color prop is always passed so it should never change it by its own.
If the intention is to provide initial value for the component, I think we should add 'defaultValue' property to indicate that it will be used only once.
@adamborowski
Copy link
Author

@casesandberg what is blocking us from finalising this?

@hzhu
Copy link

hzhu commented Dec 10, 2020

ColorWrap HOC manages color state but at the same time accepts "color" prop

This component API design is fine - a component should accept both a controlled prop (in this case color) and be able to manage it's own internal color state for uncontrolled usages.

I think we should add 'defaultValue' property

This is a bit unrelated, but I agree. A defaultValue prop should be available for uncontrolled usages.


@adamborowski what do you think about completely removing getDerivedStateFromProps as the only change in this pull request? I don't think the other changes are necessary. Why is getDerivedStateFromProps necessary here to begin with? In what scenario does the state need to sync up with props? What are the consequences of removing getDerivedStateFromProps?

The only reason I can think of is if the color prop was an invalid HEX, then we may want to intercept the state, and update it's color to the previous, valid color. @casesandberg, what are your thoughts?

P.S. Thanks for putting up a pull request for the issue 🙏🏼 .

@adamborowski
Copy link
Author

Actually I don't use this component but needed to help someone using it but after analyzing the code I think I understand the purpose of getDerivedStateFromProps.

So, we have color prop which is used to control the value, onChange prop which is called all the time user drags the mouse, and onChangeComplete which is called when user finishes dragging and releases mouse.

We have two ways how user might want use component in a controlled mode.

First, when users want to control current position on the "slider" and update state even when user drags the mouse. To do that, color and onChange props can be used.

Second, when users want the control only final state of the "slider" and update state only when user finishes dragging the mouse and releases the mouse. To do that, color and onChangeComplete props can be used.

So for the second case - user wants to control the state only if user isn't dragging the mouse. So for the time user is dragging mouse - picker has to manage the state internally until mouse is released.

This means that the second case isn't fully controlled mode - it's a mixed mode. This is why we need to manage derived state.

And I think it makes sense. For example, when we have a combo box - we might want to control the value but we expect onChange callback to by called not during typing text but only after user finishes typing and selects his preferred option.

@hzhu
Copy link

hzhu commented Dec 10, 2020

After taking another look at ColorWrap, it looks like the component has a default prop for color — I take it that this is the "uncontrolled" case since the developer isn't controlling the color prop. The API design of this component is a bit unique. More commonly, if a color prop isn't passed (uncontrolled usage), the component uses a default internal color state or uses a defaultValue prop (as mentioned in the PR description 💯 ).

Still — there is no clear reason as to why the getDerivedStateFromProps usage is necessary. Deleting it completely fixes this bug, and another bug #794 too. At the same time, there seem to be no regressions. As it stands, I still think this PR should only remove the getDerivedStateFromProps static method as the only change.

@adamborowski
Copy link
Author

The problem @hzhu is that I can't find defaultValue in the code. It means that there is no other way to control the state from outside than by setting 'color' prop. If we remove internal state at all, we won't be able to use color and onChangeComplete pair of props. And on the other side - some of existing bugs may be caused because setState() merges objects, not replaces. So having it inside a nested field in internal state would probably solve few of them

@hzhu
Copy link

hzhu commented Dec 11, 2020

The problem @hzhu is that I can't find defaultValue in the code. It means that there is no other way to control the state from outside than by setting 'color' prop.

I agree that this is a problem and I think the API design of this component is a bit unconventional. I agree that it should have a defaultValue prop, but that's a separate topic.

If we remove internal state at all, we won't be able to use color and onChangeComplete pair of props.

This isn't what I'm suggesting — I'm not suggesting removing the internal state. My suggestion is to remove the static method deriving state from props.

@adamborowski
Copy link
Author

adamborowski commented Dec 11, 2020

This isn't what I'm suggesting — I'm not suggesting removing the internal state. My suggestion is to remove the static method deriving state from props.

If you do so, you won't be able to use the component in a controlled mode at all. Picker's color prop, if provided, will be used only initially

@hzhu
Copy link

hzhu commented Dec 11, 2020

The getDerivedStateFromProps static method is the root cause of bugs #754 and #794. I haven't found any reasons to use derived state from props. It's probably not needed and should be removed. That should be the only change in this PR.

Here's a fork demonstrating the removal of getDerivedStateFromProps. All examples still work. There doesn't seem to be any regressions. Feel free to pull it down and run it locally to confirm this.

Fork: https://github.com/hzhu/react-color
Storybook build: https://delete-getderivedstatefromprops.netlify.app

@dcantatore
Copy link

@casesandberg - Do you think you could take a look at this please?

@adamborowski
Copy link
Author

@hzhu can you provide a code snippets of how you imagine using that ColorWrap component?
I become feeling we are talking about totally different use cases

@amir0ff
Copy link

amir0ff commented Aug 5, 2021

The getDerivedStateFromProps static method is the root cause of bugs #754 and #794. I haven't found any reasons to use derived state from props. It's probably not needed and should be removed. That should be the only change in this PR.

Here's a fork demonstrating the removal of getDerivedStateFromProps. All examples still work. There doesn't seem to be any regressions. Feel free to pull it down and run it locally to confirm this.

Fork: https://github.com/hzhu/react-color
Storybook build: https://delete-getderivedstatefromprops.netlify.app

Thank you for this. This is the only way I was able to run the storybook script. Their original repo is buggy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants