Skip to content

Commit

Permalink
Add Documentation for a Migration HOC
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
ITenthusiasm committed Mar 23, 2021
1 parent 3a4486a commit c72c1ad
Show file tree
Hide file tree
Showing 7 changed files with 374 additions and 14 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ module.exports = {
{
files: ['docs/*.md', 'docs/**/*.md'],
rules: {
'no-console': 'off'
'no-console': 'off',
'func-names': 'off'
}
},
{
Expand Down
134 changes: 134 additions & 0 deletions docs/react-jss-hoc-migration.md
Original file line number Diff line number Diff line change
@@ -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 <WrappedComponent {...passThroughProps} classes={reactJssClasses} />
}

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<ReturnType<typeof createUseStyles>>}

/**
* Creates a Higher Order Component that injects the CSS specified in `styles`.
* @param styles
*/
function withStyles<C extends string, Pr extends ReactJSSProps, T>(
styles: Styles<C, Pr, T> | ((theme: T) => Styles<C, Pr>)
) {
return function<P extends Pr, S>(WrappedComponent: React.ComponentClass<P, S>): React.FC<P> {
const useStyles = createUseStyles<C, P, T>(styles)

const StyledComponent: React.FC<P> = (props: P) => {
const {classes, ...passThroughProps} = props
const theme = useTheme<T>()
const reactJssClasses = useStyles({...(passThroughProps as P), theme})

return <WrappedComponent {...passThroughProps as P} classes={reactJssClasses} />
}

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.
2 changes: 1 addition & 1 deletion docs/react-jss.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
16 changes: 9 additions & 7 deletions packages/react-jss/tests/types/withStyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ interface MyTheme {
color: 'red'
}

function SimpleComponent(props: MyProps) {
return <div>{props.property}</div>
class SimpleComponent extends React.Component<MyProps> {
render() {
return <div>{this.props.property}</div>
}
}

// Intended to test the output of withStyles to make sure the props are still valid
Expand Down Expand Up @@ -137,7 +139,7 @@ ComponentTest = () => <ResultingComponent property="" />
/* -------------------- Failing Cases -------------------- */

// A function argument cannot provide another defined theme type conflicting with `undefined`
function failingFunctionRedefineTheme(theme: MyTheme): Styles<string, unknown, any> {
function failingFunctionNullTheme(theme: MyTheme): Styles<string, unknown, null> {
return {
someClassName: '',
anotherClassName: {
Expand All @@ -146,7 +148,7 @@ function failingFunctionRedefineTheme(theme: MyTheme): Styles<string, unknown, a
}
}

function passingFunctionUnknownTheme(theme: MyTheme): Styles<string, unknown, unknown> {
function passingFunctionAnyTheme(theme: MyTheme): Styles<string, unknown, any> {
return {
someClassName: '',
anotherClassName: {
Expand All @@ -155,7 +157,7 @@ function passingFunctionUnknownTheme(theme: MyTheme): Styles<string, unknown, un
}
}

function passingFunctionNullTheme(theme: MyTheme): Styles<string, unknown, null> {
function passingFunctionUnknownTheme(theme: MyTheme): Styles<string, unknown, unknown> {
return {
someClassName: '',
anotherClassName: {
Expand All @@ -165,6 +167,6 @@ function passingFunctionNullTheme(theme: MyTheme): Styles<string, unknown, null>
}

// @ts-expect-error
withStyles(failingFunctionRedefineTheme)(SimpleComponent)
withStyles(failingFunctionNullTheme)(SimpleComponent)
withStyles(passingFunctionAnyTheme)(SimpleComponent)
withStyles(passingFunctionUnknownTheme)(SimpleComponent)
withStyles(passingFunctionNullTheme)(SimpleComponent)
Loading

0 comments on commit c72c1ad

Please sign in to comment.