Skip to content

Fix React Strictmode for sliders #4012

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

Closed
wants to merge 13 commits into from
Closed

Conversation

snowystinger
Copy link
Member

@snowystinger snowystinger commented Feb 4, 2023

Closes

Note on tests, failure count may be inaccurate due to cascading failures
Before
Test Suites: 27 failed, 1 skipped, 144 passed, 171 of 172 total
Tests: 657 failed, 49 skipped, 3534 passed, 4240 total
Snapshots: 1 passed, 1 total

After
Test Suites: 24 failed, 1 skipped, 147 passed, 171 of 172 total
Tests: 654 failed, 49 skipped, 3537 passed, 4240 total
Snapshots: 1 passed, 1 total

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices

📝 Test Instructions:

🧢 Your Project:

@rspbot
Copy link

rspbot commented Feb 4, 2023

@rspbot
Copy link

rspbot commented Feb 9, 2023

const isDraggingsRef = useRef<boolean[]>(null);
isDraggingsRef.current = isDraggings;
useEffect(() => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't change this to a layouteffect since it's in stately and we can't use our SSR safe version there since it's in aria's utils

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turned out that we don't even need to assign these refs during render. We only need them so that we can fire onChangeEnd immediately after onChange without waiting for a render in between. If we always assign them at the same time we update the state it seems to work. Done in #4564.

@rspbot
Copy link

rspbot commented Feb 15, 2023

@rspbot
Copy link

rspbot commented Feb 16, 2023

@rspbot
Copy link

rspbot commented Mar 2, 2023

if (isControlled) {
stateRef.current = value;
}
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think this is too late. Imagine we have a controlled component. We fire onChange, this triggers a re-render of the parent component. Then another onChange fires before the useEffect is called, but we would be comparing against the previous value in stateRef.current. This is definitely a problem with useEffect because it runs after an animation frame, but I think it's even a problem with useLayoutEffect.

For example, say you have two components: a parent and a child. The parent component has a useControlledState in it, and passes its onChange function to the child. The child has a useLayoutEffect in it which triggers an onChange in response to something. This is fairly common, for example combobox reacts to prop changes in an effect and might emit events based on that. Same with auto selecting the first item in tabs on mount. The problem is that child effects run before parent effects. If the child component calls onChange in its effect, the value in the ref within the parent component won't have updated yet, meaning the comparison we do would not be correct. You can see an example of that here - the stateRef is always behind by one render.

Honestly, I'm not really sure how to solve this. For React 18, we might be able to use useInsertionEffect which runs before layout effects. But you'd still have the same problem if any other component used useInsertionEffect (rare), and for older Reacts.

Updating during render is actually the most close to correct. It's only incorrect when a render gets aborted (e.g. during suspense). For strict mode, the value coming from props should be the same between the two renders so it shouldn't matter. Did you see test failures due to this, or was this just following the linter?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went back and checked the tests passing before and after. It's the same. So changing useControlledState was just following the linter.

@rspbot
Copy link

rspbot commented Mar 3, 2023

@rspbot
Copy link

rspbot commented Mar 30, 2023

@rspbot
Copy link

rspbot commented Apr 14, 2023

@rspbot
Copy link

rspbot commented Apr 14, 2023

@snowystinger snowystinger changed the title Fix React Strictmode for sliders and useControlledState Fix React Strictmode for sliders Apr 14, 2023
@rspbot
Copy link

rspbot commented Apr 14, 2023

@rspbot
Copy link

rspbot commented Apr 14, 2023

## API Changes

unknown top level export { type: 'identifier', name: 'Column' }
unknown top level export { type: 'identifier', name: 'Column' }
unknown type { type: 'link' }
unknown type { type: 'link' }
unknown type { type: 'link' }
unknown type { type: 'link' }
unknown type { type: 'link' }
unknown type { type: 'link' }

devongovett added a commit that referenced this pull request May 22, 2023
Fixes sliders and color components (alternative to #4012).
state.setThumbDragging(realTimeTrackDraggingIndex.current, false);
realTimeTrackDraggingIndex.current = null;
}
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these useEffectEvents should really be inside useMove so that other consumers benefit as well. I made this change in #4564.

stateRef.current = state;
useLayoutEffect(() => {
stateRef.current = state;
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can get rid of stateRef completely if we make useMove use useEffectEvent. Done in #4564.

const isDraggingsRef = useRef<boolean[]>(null);
isDraggingsRef.current = isDraggings;
useEffect(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turned out that we don't even need to assign these refs during render. We only need them so that we can fire onChangeEnd immediately after onChange without waiting for a render in between. If we always assign them at the same time we update the state it seems to work. Done in #4564.

@snowystinger
Copy link
Member Author

Closing in favor of #4564

@snowystinger snowystinger deleted the react-strictmode-sliders branch May 24, 2023 16:44
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.

3 participants