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

Remove singletons in favor of React context (hooks-based implementation)(backward compatible with all versions of react) #221

Merged
merged 40 commits into from
Aug 26, 2019
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
aa24736
Remove singletons / Checkpoint
Aug 9, 2019
8989282
update version of jsdom to maintain support for node 6
Aug 9, 2019
19ab350
Add support for pureComponent as React.memo and fix direction
Aug 10, 2019
bc754c7
Add fallback to the ThemedStyleSheet API when no context was provided
Aug 13, 2019
ca6340d
Move contextual folder to root
Aug 13, 2019
7d4eed1
Add direction to proptypes
Aug 13, 2019
df6b7d5
Fix component name
Aug 13, 2019
efcd432
Copy statics to the memoized component as well
Aug 13, 2019
19d0f48
Use only one context type, WithStylesContext
Aug 15, 2019
00afde9
Use a cache to avoid re-creating and re-resolving styles
Aug 15, 2019
dc8894c
Do not cache resolved props since they should change if props change
Aug 15, 2019
f25e134
Add react-hooks eslint plugin
Aug 15, 2019
61a9726
eslint
Aug 15, 2019
5c1c475
Set the react peer dep to 16.8
Aug 15, 2019
2a65c8a
actually fix eslint warnings for hooks
Aug 16, 2019
ff18eda
Improve the API so that it's actually usable as a hook
Aug 19, 2019
e026784
Move enlint disables to the top of file
Aug 19, 2019
0a8b12b
Groom comments
Aug 19, 2019
67e1098
Listen to directional updates from context directly
Aug 19, 2019
94196e1
Simplify broadcast effect -- avoid unnecessary ref
Aug 20, 2019
297318f
Groom variable naming for clarity
Aug 20, 2019
62a1b2b
linting
Aug 20, 2019
8d3992b
Update linter and update linting rules
Aug 20, 2019
3a8e27d
Remove unnecessary direction prop types
Aug 20, 2019
d1576d7
Use hook feature detection to choose which version of withStyles to use
Aug 20, 2019
a179c1c
Protect hook against empty stylesFn
Aug 21, 2019
f2444bb
Move perf to utils
Aug 21, 2019
9b6e885
Protect hook against empty stylesFn
Aug 21, 2019
05c777d
Move perf to utils
Aug 21, 2019
77535fa
Feature detection for react context
Aug 22, 2019
8cbb1bd
Remove utilities we don't want to make public
Aug 22, 2019
5f0ca91
Change error messaging
Aug 22, 2019
5ac8a14
Feature detect inside WithStylesWithHooks
Aug 22, 2019
c13de70
Improve test error messages by using `to.have.property`
Aug 22, 2019
241de38
Fix tests so they run for the right versions of react
Aug 22, 2019
2c4c254
Merge branch 'nora--remove-singletons-backwards-compatible' into nora…
Aug 22, 2019
9b447c6
Add back css export for backward compatibility
Aug 23, 2019
58ce44b
Copy over all statics to avoid errors
Aug 23, 2019
d1ad51f
Update test/withStylesWithHooks_test.jsx
noratarano Aug 23, 2019
1c783f6
Apply suggestion: Update React version for createContext
Aug 23, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
{
"extends": "airbnb",
"extends": [
"airbnb",
"airbnb/hooks"
],

"rules": {
"react/jsx-props-no-spreading": "off",
"react/forbid-foreign-prop-types": "off",
"arrow-parens": "off"
},

"env": {
"browser": true,
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"clean": "rimraf lib",
"lint": "eslint --ext .js,.jsx .",
"mocha": "mocha test",
"mocha:debugger": "mocha --inspect-brk",
"postversion": "git commit package.json CHANGELOG.md -m \"Version $npm_package_version\" && npm run tag && git push && git push --tags && npm publish",
"prepublish": "in-publish && safe-publish-latest && npm run build || not-in-publish",
"pretest": "npm run --silent lint",
Expand Down Expand Up @@ -56,11 +57,14 @@
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.14.0",
Copy link
Collaborator

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

"eslint": "^5.16.0",
"eslint-config-airbnb": "^17.1.1",
"eslint-config-airbnb": "^18.0.1",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.14.3",
"eslint-plugin-react-hooks": "^1.7.0",
"in-publish": "^2.0.0",
"jsdom": "^14.1.0",
"jsdom-global": "3.0.2",
"mocha": "^5.2.0",
"react": "^16.8.6",
"react-dom": "^16.8.6",
Expand All @@ -71,10 +75,11 @@
"sinon-sandbox": "^2.0.5"
},
"peerDependencies": {
"react": ">=0.14",
"react": ">=16.8",
"react-with-direction": "^1.1.0"
},
"dependencies": {
"airbnb-prop-types": "^2.14.0",
ljharb marked this conversation as resolved.
Show resolved Hide resolved
"hoist-non-react-statics": "^3.2.1",
"object.assign": "^4.1.0",
"prop-types": "^15.7.2",
Expand Down
9 changes: 9 additions & 0 deletions src/ThemedStyleSheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ function flush() {
}
}

// Exported until we deprecate this API completely
// eslint-disable-next-line no-underscore-dangle
export function _getInterface() {
return styleInterface;
}

// Exported until we deprecate this API completely
export { get as _getTheme };

Copy link
Collaborator Author

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.

export default {
registerTheme,
registerInterface,
Expand Down
8 changes: 8 additions & 0 deletions src/WithStylesContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createContext } from 'react';

const WithStylesContext = createContext({
stylesInterface: null,
stylesTheme: null,
});

export default WithStylesContext;
218 changes: 218 additions & 0 deletions src/deprecated/withStyles.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import React from 'react';
Copy link
Member

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?

import PropTypes from 'prop-types';
import hoistNonReactStatics from 'hoist-non-react-statics';

import { CHANNEL, DIRECTIONS } from 'react-with-direction/dist/constants';
import brcastShape from 'react-with-direction/dist/proptypes/brcast';

import ThemedStyleSheet from '../ThemedStyleSheet';

// Add some named exports to assist in upgrading and for convenience
export const css = ThemedStyleSheet.resolveLTR;
export const withStylesPropTypes = {
styles: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
theme: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
css: PropTypes.func.isRequired,
};

const EMPTY_STYLES = {};
const EMPTY_STYLES_FN = () => EMPTY_STYLES;

const START_MARK = 'react-with-styles.createStyles.start';
const END_MARK = 'react-with-styles.createStyles.end';

function baseClass(pureComponent) {
if (pureComponent) {
if (!React.PureComponent) {
throw new ReferenceError('withStyles() pureComponent option requires React 15.3.0 or later');
}

return React.PureComponent;
}

return React.Component;
}

const contextTypes = {
[CHANNEL]: brcastShape,
};

const defaultDirection = DIRECTIONS.LTR;

export function withStyles(
styleFn,
{
stylesPropName = 'styles',
themePropName = 'theme',
cssPropName = 'css',
flushBefore = false,
pureComponent = false,
} = {},
) {
let styleDefLTR;
let styleDefRTL;
let currentThemeLTR;
let currentThemeRTL;
const BaseClass = baseClass(pureComponent);

function getResolveMethod(direction) {
return direction === DIRECTIONS.LTR
? ThemedStyleSheet.resolveLTR
: ThemedStyleSheet.resolveRTL;
}

function getCurrentTheme(direction) {
return direction === DIRECTIONS.LTR
? currentThemeLTR
: currentThemeRTL;
}

function getStyleDef(direction, wrappedComponentName) {
const currentTheme = getCurrentTheme(direction);
let styleDef = direction === DIRECTIONS.LTR
? styleDefLTR
: styleDefRTL;

const registeredTheme = ThemedStyleSheet.get();

// Return the existing styles if they've already been defined
// and if the theme used to create them corresponds to the theme
// registered with ThemedStyleSheet
if (styleDef && currentTheme === registeredTheme) {
return styleDef;
}

if (
process.env.NODE_ENV !== 'production'
&& typeof performance !== 'undefined'
&& performance.mark !== undefined && typeof performance.clearMarks === 'function'
) {
performance.clearMarks(START_MARK);
performance.mark(START_MARK);
}

const isRTL = direction === DIRECTIONS.RTL;

if (isRTL) {
styleDefRTL = styleFn
? ThemedStyleSheet.createRTL(styleFn)
: EMPTY_STYLES_FN;

currentThemeRTL = registeredTheme;
styleDef = styleDefRTL;
} else {
styleDefLTR = styleFn
? ThemedStyleSheet.createLTR(styleFn)
: EMPTY_STYLES_FN;

currentThemeLTR = registeredTheme;
styleDef = styleDefLTR;
}

if (
process.env.NODE_ENV !== 'production'
&& typeof performance !== 'undefined'
&& performance.mark !== undefined && typeof performance.clearMarks === 'function'
) {
performance.clearMarks(END_MARK);
performance.mark(END_MARK);

const measureName = `\ud83d\udc69\u200d\ud83c\udfa8 withStyles(${wrappedComponentName}) [create styles]`;

performance.measure(
measureName,
START_MARK,
END_MARK,
);
performance.clearMarks(measureName);
}

return styleDef;
}

function getState(direction, wrappedComponentName) {
return {
resolveMethod: getResolveMethod(direction),
styleDef: getStyleDef(direction, wrappedComponentName),
};
}

return function withStylesHOC(WrappedComponent) {
const wrappedComponentName = WrappedComponent.displayName
|| WrappedComponent.name
|| 'Component';

// NOTE: Use a class here so components are ref-able if need be:
// eslint-disable-next-line react/prefer-stateless-function
class WithStyles extends BaseClass {
constructor(props, context) {
super(props, context);

const direction = this.context[CHANNEL]
? this.context[CHANNEL].getState()
: defaultDirection;

this.state = getState(direction, wrappedComponentName);
}

componentDidMount() {
if (this.context[CHANNEL]) {
// subscribe to future direction changes
this.channelUnsubscribe = this.context[CHANNEL].subscribe((direction) => {
this.setState(getState(direction, wrappedComponentName));
});
}
}

componentWillUnmount() {
if (this.channelUnsubscribe) {
this.channelUnsubscribe();
}
}

render() {
// As some components will depend on previous styles in
// the component tree, we provide the option of flushing the
// buffered styles (i.e. to a style tag) **before** the rendering
// cycle begins.
//
// The interfaces provide the optional "flush" method which
// is run in turn by ThemedStyleSheet.flush.
if (flushBefore) {
ThemedStyleSheet.flush();
}

const {
resolveMethod,
styleDef,
} = this.state;

return (
<WrappedComponent
{...this.props}
{...{
[themePropName]: ThemedStyleSheet.get(),
[stylesPropName]: styleDef(),
[cssPropName]: resolveMethod,
}}
/>
);
}
}

WithStyles.WrappedComponent = WrappedComponent;
WithStyles.displayName = `withStyles(${wrappedComponentName})`;
WithStyles.contextTypes = contextTypes;
if (WrappedComponent.propTypes) {
WithStyles.propTypes = { ...WrappedComponent.propTypes };
delete WithStyles.propTypes[stylesPropName];
delete WithStyles.propTypes[themePropName];
delete WithStyles.propTypes[cssPropName];
}
if (WrappedComponent.defaultProps) {
WithStyles.defaultProps = { ...WrappedComponent.defaultProps };
}

return hoistNonReactStatics(WithStyles, WrappedComponent);
};
}
72 changes: 72 additions & 0 deletions src/useStyles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useRef } from 'react';
import { DIRECTIONS } from 'react-with-direction';

import withPerf from './utils/perf';
import useStylesInterface from './utils/useStylesInterface';

Choose a reason for hiding this comment

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

Should these be at the same level? (within src)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't intend for these to available publicly, though I think they sitll are.

import useStylesTheme from './utils/useStylesTheme';

/**
* Hook used to derive the react-with-styles props from the provided react-with-styles
* theme, interface, direction, and styles function.
*
* @export
* @param {*} [{ direction, stylesFn, flushBefore }={}]
* @returns {*} { css, styles, theme }
*/
export default function useStyles({ direction, stylesFn, flushBefore } = {}) {
// Get the styles interface and styles theme from context
const stylesInterface = useStylesInterface();
const theme = useStylesTheme();

// Flush if specified
if (flushBefore && stylesInterface.flush) {
stylesInterface.flush();
}

// Use a cache to store the interface methods and created styles by direction.
// This way, if the theme and the interface don't change, we do not recalculate
// styles or any other interface derivations. They are effectively only calculated
// once per direction, until the theme or interface change.
const cacheRefLTR = useRef();
const cacheRefRTL = useRef();
const cacheRef = direction === DIRECTIONS.RTL ? cacheRefRTL : cacheRefLTR;

// If the interface and theme haven't changed for this direction,
// we return all the cached values immediately.
if (
cacheRef.current
&& stylesInterface
&& cacheRef.current.stylesInterface === stylesInterface
&& cacheRef.current.theme === theme
) {
return cacheRef.current;
}

// (Re)Create the styles props for this direction
const directionSelector = direction === DIRECTIONS.RTL ? 'RTL' : 'LTR';

// Create the themed styles from the interface's create functions
// with the theme and styles function provided
let create = stylesInterface[`create${directionSelector}`] || stylesInterface.create;
if (process.env.NODE_ENV !== 'production') {
create = withPerf('create')(create);
}
const styles = create(stylesFn ? stylesFn(theme) : {});

// Create the css function from the interface's resolve functions
let resolve = stylesInterface[`resolve${directionSelector}`] || stylesInterface.resolve;
if (process.env.NODE_ENV !== 'production') {
resolve = withPerf('resolve')(resolve);
}
const css = (...args) => resolve(args);

Choose a reason for hiding this comment

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

Does resolve only take 1 argument that is an array? (I'm not sure, just felt odd)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes. :) This is why we have to do this.

Copy link
Collaborator Author

@noratarano noratarano Aug 21, 2019

Choose a reason for hiding this comment

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

function resolveLTR(...styles) {
if (styleInterface.resolveLTR) {
return styleInterface.resolveLTR(styles);
}
return resolve(styles);
}


// Cache the withStyles values for this direction
cacheRef.current = {
stylesInterface,
theme,
styles,
css,
};

return cacheRef.current;
}
Loading