-
Notifications
You must be signed in to change notification settings - Fork 262
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
feat: use react hooks to manage styles injection #720
Conversation
38bc460
to
9ab4a77
Compare
Is this caused by strict effects or is it something else? Could you link more references in the description? Would appreciate it!
|
8a93b81
to
1ded933
Compare
If you end up dropping an old react version, make sure you add the following as a new line to the commit message when squash & merged:
Adding that line to the commit message will ensure this releases as a new major! |
629c0b9
to
e47f2c1
Compare
|
||
render() { | ||
// This is a workaround to make the side effect async safe in the "render" phase. | ||
// See https://github.com/zeit/styled-jsx/pull/484 |
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.
Please read the comment below and see if you can verify that everything works fine i.e. triggering layout in a parent component doesn't run transitions. Before this was due to the fact that styles are appended after the DOM is updated.
@sebmarkbage explained what's going on here #484 (comment) This is by the way the reason why I was adding and removing styles during rendering - I needed them to be in the page before the related component was committed/updated
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.
useLayoutEffect
is safer than useEffect
here since useLayoutEffect
inject the styles to DOM synchronously after DOM mutations which kind avoid the repaint happening very soon by browser.
We tried the jsfiddle example provided in that PR to test with react 17. If style injection happens in useEffect
then it repaint but it doesn't repaint with useLayoutEffects.
https://codesandbox.io/s/react-17-repaint-demo-j21eq
Also found that emotion and styled-components both use useLayoutEffects
to manage style injection. So I think this so be a fine solution so far? Before react releases any new hook for this case.
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.
Transitions are still running when you trigger layout https://codesandbox.io/s/react-17-repaint-demo-forked-c716t?file=/src/App.js
I'll leave it to you to decide whether this is ok but if I remember correctly I had to come up with that hack (inject while rendering) because of a bug report from Vercel folks.
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.
As Sebastian said in that comment, it's missing a good solution and it feels OK for styled-jsx at the moment, as it's more like an uncovered case of React. We should either look forward to an official React API for this, or avoid immediate layout triggering in the application code.
I'll approve this PR since there will be a much bigger issue with the current implementation when enabling concurrent rendering in React 18.
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.
Sounds good!
if (typeof window === 'undefined') { | ||
styleSheetRegistry.add(props) | ||
return null |
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 we SSR the component, and somehow it's rendered multiple times (e.g. Suspense), will there be a problem?
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.
The first time it is called add
injects the styles into an in memory stylesheet. Future calls with the same props will just increment a counter
styled-jsx/src/stylesheet-registry.js
Lines 55 to 58 in 2741879
if (styleId in this._instancesCounts) { | |
this._instancesCounts[styleId] += 1 | |
return | |
} |
styled-jsx won't work with streaming rendering at the moment and perhaps suspense. I think it'd need a way to yield styles for the page while preserving a global state (registry) to avoid duplicates. I am open to collaborations if you'll ever need help with this.
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.
Tested with suspense in react 18, looks good so far. As the anncoucement said effects hooks are reliable with suspense.
I use the streaming API with next.js to test it locally. But maybe the case is too simple.
@giuseppeg Would you mind make it more clear why you think it wouldn't work with streaming so far? Is it not safe to manage styles in memory even with hooks?
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 we SSR the component, and somehow it's rendered multiple times (e.g. Suspense), will there be a problem?
To add more context to this, normally when we SSR all components will only be rendered once and there's no re-render. But with Suspense in the server side, it could be possible for a component to be re-rendered multiple times due to Suspense fallback and retry. In that case the counter will be wrongly counted. However since we won't have unmount in SSR and there's no style removal, so I think it's OK but I can't be 100% sure.
With CSR Suspense it won't be an issue though since we have every side effect inside the useLayoutEffect
hook.
We will figure this out. 👍
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 use the streaming API with next.js to test it locally
@huozhi interesting, I'd be curious to see how this works but in general I was thinking that as you stream HTML you'd need to flush styles (render style tags) but keep an in memory registry on the server so that you don't accidentally render the same styles in future chunks (imagine you have 4 instances of a Button component).
Probably this could be hacked with the current utils but styled-jsx doesn't do this out of the box at the moment.
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.
Note: streaming means multiple renders can happen concurrently, so a global registry on the server is unworkable with Concurrent Mode.
I wonder why is that unworkable. When doing SSR, the registry doesn't need to track the order of renders or concurrency, it just need to work like a boolean (only add the style when the counter turns from 0 to 1) since there's no removal process.
Or does it mean multiple requests, when you say "multiple renders" here?
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.
Yes, multiple requests can be being processed concurrently, so a singleton that tracks styles used wouldn’t work on the server, as other requests would read/write the same singleton.
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.
This works in synchronous React because the render starts and ends synchronously. But in React 18 a tree could suspend while waiting for data.
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 it possible for the following race condition to happen?
- big subtree starts rendering a Button which registers the styles
- smaller subtree renders Button and is ready before 1.
- the Button from 2. is unstyled
By the way a while ago I created a gist / PoC for a Style component that should work with streaming/suspense - I'll see if I can polish it and share it in the following days. As @devknoll mentioned it uses Context
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... if that "smaller subtree renders Button and is ready before 1." is inside another Suspense boundary, it’s gonna be unstyled until it’s hydrated.
74c1e9a
to
e31dbb6
Compare
BREAKING CHANGE: This change drops support for React 15 and 16
11aa92b
to
30d4d40
Compare
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.
Check the peerDependencies comment and then feel free to merge. Remember to add the comment below when squashing
BREAKING CHANGE: This change drops support for React 15.
@huozhi I am thinking that since there will be more breaking changes in the other PRs maybe we can create a canary release and once we merge all the changes necessary to support React18 we can cut a major release |
@giuseppeg yeah totally, @shuding and I were thinking the same thing. I'll update the |
0b28b97
to
20396d2
Compare
🎉 This PR is included in version 4.0.0-alpha.2 🎉 The release is available on: Your semantic-release bot 📦🚀 |
* fix: safely remove style * remove props.children dep * BREAKING CHANGE: This change drops support for React version < 16.8.0
* fix: safely remove style * remove props.children dep * BREAKING CHANGE: This change drops support for React version < 16.8.0
StyleSheetRegistry is an external state outside of react, to keep sync with react internal state, we'd better use reliable lifecycle to count the change instead of use cached class properties and
render
function to determine changes.Lifecycle
first render -> add style
update -> remove then add
unmount -> remove
Problem
the issue is the react internal state might be different with the cached class properties
this.prevProps
, react might drop some render due to prioritise change, leading to tearing.Notice
This PR only solved the effects issue when using the react under concurrent mode.