-
Notifications
You must be signed in to change notification settings - Fork 96
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
Remove singletons in favor of React context (hooks-based implementation)(backward compatible with all versions of react) #221
Conversation
|
||
// Exported until we deprecate this API completely | ||
export { get as _getTheme }; | ||
|
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 is so we can fall back to these singletons from the contextual implementation.
src/deprecated/withStyles.jsx
Outdated
@@ -0,0 +1,220 @@ | |||
/* eslint react/forbid-foreign-prop-types: off */ |
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 is the old withStyles
moved to deprecated/
|
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.
Using hooks means it's a breaking change, and we'll need to bump the peer dep to react 16.8 or higher.
Is there a reason we can't use traditional context?
src/StylesThemeContext.js
Outdated
|
||
const StylesThemeContext = createContext(); | ||
|
||
export default StylesThemeContext; |
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.
Make into a single context.
src/useThemedStyleSheet.js
Outdated
resolve, | ||
flush, | ||
}), | ||
[create, resolve, flush], |
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.
Avoid diffing so many things. Only diff the values that matter once, so no need to diff create/resolve/flush separately.
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 values returned by useCallback
are memoized already, and I'm not sure we need to re-memo them again. In fact, useCallback
could be implemented with useMemo
.
Should be able to:
return { create, resolve, flush }
Or
return [create, resolve, flush];
@ljharb This is intended to be a breaking change. This API is just much easier to maintain and read through. There's no particular reason other than maintainability for this, and Airbnb is using React 16.8, and I believe we're comfortable with this breaking change. Can you share why it's not recommended to introduce this restriction? |
@noratarano the X is just for updating the peer dep; the rest is just my opinion. Public open source projects aren't just for Airbnb, and while it's great that Airbnb is now on 16.8 (congrats!), making it a requirement excludes some portion of the internet from being able to use it - as of a month or two ago, it'd have excluded Airbnb too. In general, it's best to aim for maximal backwards compatibility, so that this project can belong anywhere. If the API is identically achievable and maintainable using legacy context - even if it's slightly less fun to work with - it seems to me like a moral imperative to maximize how many people can use the project, not to optimize for the experience of the < 5 developers who will touch this repo. All that said, if the peer dep is bumped, i'll remove my X, and I'll trust yall to make the decisions you feel best. |
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.
Looks good so far! Left a couple comments regarding hooks, and will leave some more comments
src/useStylesInterface.js
Outdated
// Fallback | ||
const registeredInterface = _getInterface(); | ||
if (!fallbackInterface) { | ||
setFallbackInterface(registeredInterface); |
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 _getInterface
is a simple getter, would there be any issues with executing it each time the hooks run?
const stylesInterface = useContext(StylesInterfaceContext);
if (stylesInterface) {
return stylesInterface;
}
// Fallback
return _getInterface();
Or, if we don't want to run it every time the hook is called, we could avoid the setState (and its re-render) with a ref:
const stylesInterface = useContext(StylesInterfaceContext);
const fallbackRef = useRef();
if (stylesInterface) {
return stylesInterface;
}
if (!fallbackRef.current) {
fallbackRef.current = _getInterface();
}
return fallbackRef.current;
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.
OOoh! Interesting. 👍
src/useThemedStyleSheet.js
Outdated
resolve, | ||
flush, | ||
}), | ||
[create, resolve, flush], |
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 values returned by useCallback
are memoized already, and I'm not sure we need to re-memo them again. In fact, useCallback
could be implemented with useMemo
.
Should be able to:
return { create, resolve, flush }
Or
return [create, resolve, flush];
src/withStyles.jsx
Outdated
// Calculate and cache the styles definition for this combination of global state values. This | ||
// value will only be recalculated if the create function changes, which in turn will only | ||
// change if any of the global state we depend on changes. | ||
const styles = useMemo(() => create(stylesFn), [create]); |
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.
👍
6ef2ed4
to
f25e134
Compare
src/deprecated/withStyles.jsx
Outdated
@@ -0,0 +1,218 @@ | |||
import React from 'react'; |
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 file completely untouched? I.e., you just moved what was previous at src/withStyles.jsx
into this deprecated folder? Or did you change other things around as well?
test/deprecated/withStyles_test.jsx
Outdated
@@ -0,0 +1,555 @@ | |||
import React from 'react'; |
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.
Same question here: is this file untouched other than the move from test/withStyles_test.jsx → test/deprecated/withStyles_test.jsx
?
test/withStyles_test.jsx
Outdated
@@ -1,555 +1,619 @@ | |||
/* eslint-disable no-unused-vars */ | |||
|
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.
Why this tho?
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.
Without disabling this I can't do:
describe('foo', () => {
let myUsedVar;
beforeEach(() => {
myUsedVar = imUsingIt();
});
it('uses this var', () => {
console.log(myUsedVar);
});
});
This fails. 😢
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.
Ah. Well that's a silly failure, but also Just Another Reason™ to avoid setup functions like beforeEact
et al that share state, especially given that people can nest these types of things and make it super hard in a long test suite to figure out what is actually going on (I experienced this often on the Host product code with tests with nested blocks and setup helpers). I don't believe tests are were we should be keeping things DRY, and would much rather see each test verbosely express everything it needs.
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.
Good point. I'll definitely keep that in mind going forward ^_^
d733f77
to
9b447c6
Compare
713e189
to
58ce44b
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.
Awesome, this is looking great!
@@ -55,12 +56,16 @@ | |||
"chai": "^4.2.0", | |||
"enzyme": "^3.10.0", | |||
"enzyme-adapter-react-16": "^1.14.0", |
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 should be removed, since the helper is being used now
src/WithStylesContext.js
Outdated
|
||
return { | ||
Provider: () => { | ||
throw new ReferenceError('WithStylesContext requires React 16.6 or later'); |
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.
createContext works in 16.3+, i believe
let { stylesInterface, stylesTheme: theme } = useContext(WithStylesContext); | ||
|
||
// Fallback to the singleton implementation | ||
stylesInterface = stylesInterface || _getInterface(); |
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.
does _getInterface()
make it available for future useContext calls, or should this somehow be persisted?
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.
No it doesn't... I see what you mean. 🤔
import ifReact from 'enzyme-adapter-react-helper/build/ifReact'; | ||
|
||
export function describeIfReact(version, ...rest) { | ||
return ifReact(version, describe, describe.skip)(...rest); |
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.
nice
Co-Authored-By: Jordan Harband <ljharb@gmail.com>
f65ff92
to
1c783f6
Compare
Merging this :) Will update README or make any additional changes in separate PRs before publishing the next version. |
Summary
Replace the
withStyles
implementation with one that usesReact.Context
, not singletons. We fall back to the singletons if noreact-with-styles
themes or interfaces are provided through context. TheThemedStyleSheet
API is still supported as we now detect Hooks/Context and conditionally use the old hooks-based implementation and the new one.Usage:
This change also introduces a few notable differences in the way that
withStyles
works:withStyles
call that the other component was doing before.Updates to the README are captured here: #223
Details
Singletons don't scale, and we're feeling that at Airbnb. In an effort to modernize our repos and scale our infrastructure, we're moving away from this singleton implementation and relying on the latest React context API to register themes and interfaces. Because we have so many systems sharing this library (react-dates, rheostat, and internal libs), including ones that require the singleton implementation (ahem hypernova) this implementation falls back to the singleton implementation (through
ThemedStyleSheet
) to ensure that bundles that don't set theming can continue to work until the process is complete.Lastly, while this is a breaking change, consumers of this lib can still access the old version of
withStyles
depending on their version of React. Those with React <= 16.8 will get the old implementation. Those with the new React will get the new one based on hooks.Deprecation process that I'm envisioning:
ThemedStyleSheet
(this PR)WithStylesContext.Provider
. If some aren't captured, we fall back to the previous implementation.ThemedStyleSheet
and thedeprecated/withStyles
HOC.Before making useStyles available:
stylesFn
is static (doesn't change)stylesFn
doesn't depend on props (cache created results?)useDirection
orDirectionContext
so thatuseStyles
can be used aloneFuture performance improvements:
Testing
I wrote a lot of tests for this. Please take a look at all the assertions.
I also tested this implementation on the internal design system storybook, rheostat and react-dates (with both the Aphrodite and CSS interfaces). 👍
Reviewers
@lencioni @ljharb @goatslacker @brucewpaul @TaeKimJR @majapw @indiesquidge