From c72c1ad21ed1bcd64651844d2fbb01e0c845e504 Mon Sep 17 00:00:00 2001 From: ITenthusiasm <47364027+ITenthusiasm@users.noreply.github.com> Date: Mon, 22 Mar 2021 11:54:04 -0400 Subject: [PATCH] Add Documentation for a Migration HOC * Added documentation for people to create a custom "lazy migration HOC" since the old HOC-based API is deprecated. - Includes information on JS, TS, and decorators. * Added tests to verify that the custom `withStyles` HOC behaves correctly. * Updated TypeScript to make its type checking more accurate. - Includes fixing/updating tests for withStyles.tsx * Loosed ESLint's rules for markdown files. --- .eslintrc.js | 3 +- docs/react-jss-hoc-migration.md | 134 +++++++++++ docs/react-jss.md | 2 +- package.json | 2 +- packages/react-jss/tests/types/withStyles.tsx | 16 +- .../tests/types/withStylesMigration.tsx | 223 ++++++++++++++++++ yarn.lock | 8 +- 7 files changed, 374 insertions(+), 14 deletions(-) create mode 100644 docs/react-jss-hoc-migration.md create mode 100644 packages/react-jss/tests/types/withStylesMigration.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 38b648b14..796911be7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -14,7 +14,8 @@ module.exports = { { files: ['docs/*.md', 'docs/**/*.md'], rules: { - 'no-console': 'off' + 'no-console': 'off', + 'func-names': 'off' } }, { diff --git a/docs/react-jss-hoc-migration.md b/docs/react-jss-hoc-migration.md new file mode 100644 index 000000000..cc4c35567 --- /dev/null +++ b/docs/react-jss-hoc-migration.md @@ -0,0 +1,134 @@ +# Migrating the `withStyles` HOC + +Although we recommend using the new hooks, it's possible that you have class components that cannot be migrated easily at this time. In that case, you can create your own higher order component (HOC) from the provided hooks. This way, you'll have a HOC that stays up-to-date with the latest features, and you'll still have the option of fully migrating to hooks at your own convenience. + +A simple solution may look something like this: + +```jsx +import React from 'react' +import {createUseStyles, useTheme} from 'react-jss' + +/** + * Creates a Higher Order Component that injects the CSS specified in `styles`. + * @param styles + */ +function withStyles(styles) { + return function(WrappedComponent) { + const useStyles = createUseStyles(styles) + + const StyledComponent = props => { + const {classes, ...passThroughProps} = props + const theme = useTheme() + const reactJssClasses = useStyles({...passThroughProps, theme}) + + return + } + + StyledComponent.displayName = `withStyles(${WrappedComponent.name})` + + return StyledComponent + } +} + +export default withStyles +``` + +Note that `useTheme` can be excluded if your application is not using a theme. + +To learn more about HOCs, see [react's documentation](https://reactjs.org/docs/higher-order-components.html). Since our HOC uses the `createUseStyles` hook under the hood, you can use the regular [hooks documentation](react-jss.md) for help with defining your `styles` objects. + +**Warning**: Because this HOC makes use of hooks, it cannot be used as a decorator. + +## Adding TypeScript to Your HOC + +If you're using TypeScript, you'll likely want to add types for your custom `withStyles` HOC, like so: + +```tsx +import React from 'react' +import {createUseStyles, useTheme, Styles} from 'react-jss' + +type ReactJSSProps = {classes?: ReturnType>} + +/** + * Creates a Higher Order Component that injects the CSS specified in `styles`. + * @param styles + */ +function withStyles( + styles: Styles | ((theme: T) => Styles) +) { + return function

(WrappedComponent: React.ComponentClass): React.FC

{ + const useStyles = createUseStyles(styles) + + const StyledComponent: React.FC

= (props: P) => { + const {classes, ...passThroughProps} = props + const theme = useTheme() + const reactJssClasses = useStyles({...(passThroughProps as P), theme}) + + return + } + + StyledComponent.displayName = `withStyles(${WrappedComponent.name})` + + return StyledComponent + } +} + +export default withStyles +``` + +This typed HOC enforces consistency with your `RuleNames` and `Theme`. It also enforces consistency between the `Props` you give to `Styles` and the ones you give to your component. + +You'll notice that here, we've typed the HOC to accept only class components as arguments. This is because you should be using the provided hooks for your functional components; not only do hooks provide a simpler interface, but they also help clarify which props actually belong to your component. + +## Migrating from Decorators + +Because this custom HOC makes use of hooks (which are [unusable in class components](https://reactjs.org/docs/hooks-faq.html#:~:text=You%20can't%20use%20Hooks,implementation%20detail%20of%20that%20component.)), you won't be able to use this HOC as a decorator. If you are using decorators in your project, you'll likely have to migrate your code from this: + +```javascript +import React from 'react' +import decorator1 from 'some-hoc-library' +import decorator2 from 'another-hoc-library' +// ... +import withStyles from 'path/to/custom-hoc' + +const styles = { + /* ... */ +} + +@decorator1 +@decorator2 +// ... +@withStyles(styles) +class MyComponent extends React.Component { + // ... +} + +export default MyComponent +``` + +to this: + +```javascript +import React from 'react' +import decorator1 from 'some-hoc-library' +import decorator2 from 'another-hoc-library' +// ... +import withStyles from 'path/to/custom-hoc' + +const styles = { + /* ... */ +} + +@decorator1 +@decorator2 +// ... +class MyComponent extends React.Component { + // ... +} + +export default withStyles(styles)(MyComponent) +``` + +If you find yourself using many decorators for your class components, consider migrating away from chained decorators to [composed function calls](https://reactjs.org/docs/higher-order-components.html#convention-maximizing-composability). This is a safer play in the long run since decorators still have not stabilized in the JS standard. + +If you don't use decorators or aren't familiar with them, then this won't be a concern for you. diff --git a/docs/react-jss.md b/docs/react-jss.md index 9d01c3841..00c81cd5a 100644 --- a/docs/react-jss.md +++ b/docs/react-jss.md @@ -4,7 +4,7 @@ React-JSS integrates [JSS](https://github.com/cssinjs/jss) with React using the Try it out in the [playground](https://codesandbox.io/s/j3l06yyqpw). -**The HOC based API is deprecated as of v10 and may be removed in a future version. You can still perform a lazy migration as described [here](https://reacttraining.com/blog/using-hooks-in-classes/). HOC specific docs are available [here](./react-jss-hoc.md).** +**The HOC-based API is deprecated as of v10 and may be removed in a future version. You can still perform a lazy migration as described [here](react-jss-hoc-migration.md). Documentation for the deprecated HOC-based API is available [here](react-jss-hoc.md).** ### Benefits compared to using the core JSS package directly: diff --git a/package.json b/package.json index 759666218..f6876e2a3 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "rollup-plugin-terser": "^7.0.2", "shelljs": "^0.8.2", "sinon": "4.5.0", - "typescript": "^3.7.0", + "typescript": "^4.2.3", "webpack": "^4.28.3", "zen-observable": "^0.6.0" } diff --git a/packages/react-jss/tests/types/withStyles.tsx b/packages/react-jss/tests/types/withStyles.tsx index b7dfa9aa1..dfb8e4419 100644 --- a/packages/react-jss/tests/types/withStyles.tsx +++ b/packages/react-jss/tests/types/withStyles.tsx @@ -15,8 +15,10 @@ interface MyTheme { color: 'red' } -function SimpleComponent(props: MyProps) { - return

{props.property}
+class SimpleComponent extends React.Component { + render() { + return
{this.props.property}
+ } } // Intended to test the output of withStyles to make sure the props are still valid @@ -137,7 +139,7 @@ ComponentTest = () => /* -------------------- Failing Cases -------------------- */ // A function argument cannot provide another defined theme type conflicting with `undefined` -function failingFunctionRedefineTheme(theme: MyTheme): Styles { +function failingFunctionNullTheme(theme: MyTheme): Styles { return { someClassName: '', anotherClassName: { @@ -146,7 +148,7 @@ function failingFunctionRedefineTheme(theme: MyTheme): Styles { +function passingFunctionAnyTheme(theme: MyTheme): Styles { return { someClassName: '', anotherClassName: { @@ -155,7 +157,7 @@ function passingFunctionUnknownTheme(theme: MyTheme): Styles { +function passingFunctionUnknownTheme(theme: MyTheme): Styles { return { someClassName: '', anotherClassName: { @@ -165,6 +167,6 @@ function passingFunctionNullTheme(theme: MyTheme): Styles } // @ts-expect-error -withStyles(failingFunctionRedefineTheme)(SimpleComponent) +withStyles(failingFunctionNullTheme)(SimpleComponent) +withStyles(passingFunctionAnyTheme)(SimpleComponent) withStyles(passingFunctionUnknownTheme)(SimpleComponent) -withStyles(passingFunctionNullTheme)(SimpleComponent) diff --git a/packages/react-jss/tests/types/withStylesMigration.tsx b/packages/react-jss/tests/types/withStylesMigration.tsx new file mode 100644 index 000000000..e0178c36e --- /dev/null +++ b/packages/react-jss/tests/types/withStylesMigration.tsx @@ -0,0 +1,223 @@ +import React from 'react' +import {createUseStyles, useTheme, Styles} from 'react-jss' + +/* -------------------- Defined HOC for Docs -------------------- */ +type ReactJSSProps = {classes?: ReturnType>} + +/** + * Creates a Higher Order Component that injects the CSS specified in `styles`. + * @param styles + */ +function withStyles( + styles: Styles | ((theme: T) => Styles) +) { + return function

(WrappedComponent: React.ComponentClass): React.FC

{ + const useStyles = createUseStyles(styles) + + const StyledComponent: React.FC

= (props: P) => { + const {classes, ...passThroughProps} = props + const theme = useTheme() + const reactJssClasses = useStyles({...(passThroughProps as P), theme}) + + return + } + + StyledComponent.displayName = `withStyles(${WrappedComponent.name})` + + return StyledComponent + } +} + +export default withStyles + +/* ------------------------------ Tests for HOC (should mimic withStyles.tsx tests) ------------------------------ */ + +// Note: Styles type is thoroughly tested in `jss/tests/types/Styles` and `react-jss/tests/types/createUseStyles`. +// This is simply a test to make sure `withStyles` accepts and rejects the correct arguments. + +// Note: Testing default theme vs. custom theme is unnecessary here since the user will +// always have to specify the theme anyway. + +interface MyProps { + classes?: Record + property: string +} + +interface MyTheme { + color: 'red' +} + +class SimpleComponent extends React.Component { + render() { + return

{this.props.property}
+ } +} + +// Intended to test the output of withStyles to make sure the props are still valid +let ResultingComponent: React.ComponentType +let ComponentTest: React.FC + +/* -------------------- Function Argument Passing Cases -------------------- */ +// Plain Object (no type supplied) +function functionPlainObject(theme: MyTheme) { + return { + someClassName: '', + anotherClassName: { + fontWeight: 'bold' + } + } +} +ResultingComponent = withStyles(functionPlainObject)(SimpleComponent) +ComponentTest = () => + +// Plain Styles +function functionPlainStyles(theme: MyTheme): Styles { + return { + someClassName: '', + anotherClassName: { + fontWeight: 'bold' + } + } +} +ResultingComponent = withStyles(functionPlainStyles)(SimpleComponent) +ComponentTest = () => + +// With Props +function functionProps(theme: MyTheme): Styles { + return { + someClassName: ({property}) => '', + anotherClassName: { + fontWeight: 'bold' + } + } +} +ResultingComponent = withStyles(functionProps)(SimpleComponent) +ComponentTest = () => + +// With Props and ClassName rules +function functionPropsAndName(theme: MyTheme): Styles { + return { + [1]: ({property}) => '', + [2]: { + fontWeight: 'bold' + } + } +} +ResultingComponent = withStyles(functionPropsAndName)(SimpleComponent) +ComponentTest = () => + +/* -------------------- Regular Object Passing Cases -------------------- */ + +// Plain Object (no type supplied) +const plainObject = { + someClassName: '', + anotherClassName: { + fontWeight: 'bold' + } +} +ResultingComponent = withStyles(plainObject)(SimpleComponent) +ComponentTest = () => + +// Plain Styles +const stylesPlain: Styles = { + someClassName: '', + anotherClassName: { + fontWeight: 'bold' + } +} +ResultingComponent = withStyles(stylesPlain)(SimpleComponent) +ComponentTest = () => + +// With Props +const stylesProps: Styles = { + someClassName: ({property}) => '', + anotherClassName: { + fontWeight: 'bold' + } +} +ResultingComponent = withStyles(stylesProps)(SimpleComponent) +ComponentTest = () => + +// With Theme +const stylesTheme: Styles = { + someClassName: ({theme}) => '', + anotherClassName: { + fontWeight: 'bold' + } +} +ResultingComponent = withStyles(stylesTheme)(SimpleComponent) +ComponentTest = () => + +// With Props and Theme +const stylesPropsAndTheme: Styles = { + someClassName: ({property, theme}) => '', + anotherClassName: { + fontWeight: 'bold' + } +} +ResultingComponent = withStyles(stylesPropsAndTheme)(SimpleComponent) +ComponentTest = () => + +// With Props and Theme and ClassName rules +const stylesPropsAndThemeAndName: Styles = { + [1]: ({property, theme}) => '', + [2]: { + fontWeight: 'bold' + } +} +ResultingComponent = withStyles(stylesPropsAndThemeAndName)(SimpleComponent) +ComponentTest = () => + +/* -------------------- Failing Cases -------------------- */ + +// A function argument cannot provide another defined theme type conflicting with `undefined` +function passingFunctionAnyTheme(theme: MyTheme): Styles { + return { + someClassName: '', + anotherClassName: { + fontWeight: 'bold' + } + } +} + +function passingFunctionUnknownTheme(theme: MyTheme): Styles { + return { + someClassName: '', + anotherClassName: { + fontWeight: 'bold' + } + } +} + +function failingFunctionNullTheme(theme: MyTheme): Styles { + return { + someClassName: '', + anotherClassName: { + fontWeight: 'bold' + } + } +} + +// @ts-expect-error +withStyles(failingFunctionNullTheme)(SimpleComponent) +withStyles(passingFunctionAnyTheme)(SimpleComponent) +withStyles(passingFunctionUnknownTheme)(SimpleComponent) + +// A functional component cannot be passed to the HOC +const SimpleFunctionComponent: React.FC = () =>
This should fail
+ +// @ts-expect-error +withStyles({})(SimpleFunctionComponent) + +// Conflicting props are not allowed +interface ConflictingProps { + classes?: Record + invalidProp: number +} + +const conflictingStyles: Styles = { + someClassName: props => ({fontSize: props.invalidProp}) +} + +// @ts-expect-error +withStyles(conflictingStyles)(SimpleComponent) diff --git a/yarn.lock b/yarn.lock index 70fa8bbee..6d686d744 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10030,10 +10030,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.7.0: - version "3.9.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a" - integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ== +typescript@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3" + integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== uglify-js@^3.1.4: version "3.8.0"