Skip to content

Commit c72c1ad

Browse files
committed
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.
1 parent 3a4486a commit c72c1ad

File tree

7 files changed

+374
-14
lines changed

7 files changed

+374
-14
lines changed

.eslintrc.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ module.exports = {
1414
{
1515
files: ['docs/*.md', 'docs/**/*.md'],
1616
rules: {
17-
'no-console': 'off'
17+
'no-console': 'off',
18+
'func-names': 'off'
1819
}
1920
},
2021
{

docs/react-jss-hoc-migration.md

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Migrating the `withStyles` HOC
2+
3+
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.
4+
5+
A simple solution may look something like this:
6+
7+
```jsx
8+
import React from 'react'
9+
import {createUseStyles, useTheme} from 'react-jss'
10+
11+
/**
12+
* Creates a Higher Order Component that injects the CSS specified in `styles`.
13+
* @param styles
14+
*/
15+
function withStyles(styles) {
16+
return function(WrappedComponent) {
17+
const useStyles = createUseStyles(styles)
18+
19+
const StyledComponent = props => {
20+
const {classes, ...passThroughProps} = props
21+
const theme = useTheme()
22+
const reactJssClasses = useStyles({...passThroughProps, theme})
23+
24+
return <WrappedComponent {...passThroughProps} classes={reactJssClasses} />
25+
}
26+
27+
StyledComponent.displayName = `withStyles(${WrappedComponent.name})`
28+
29+
return StyledComponent
30+
}
31+
}
32+
33+
export default withStyles
34+
```
35+
36+
Note that `useTheme` can be excluded if your application is not using a theme.
37+
38+
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.
39+
40+
**Warning**: Because this HOC makes use of hooks, it cannot be used as a decorator.
41+
42+
## Adding TypeScript to Your HOC
43+
44+
If you're using TypeScript, you'll likely want to add types for your custom `withStyles` HOC, like so:
45+
46+
```tsx
47+
import React from 'react'
48+
import {createUseStyles, useTheme, Styles} from 'react-jss'
49+
50+
type ReactJSSProps = {classes?: ReturnType<ReturnType<typeof createUseStyles>>}
51+
52+
/**
53+
* Creates a Higher Order Component that injects the CSS specified in `styles`.
54+
* @param styles
55+
*/
56+
function withStyles<C extends string, Pr extends ReactJSSProps, T>(
57+
styles: Styles<C, Pr, T> | ((theme: T) => Styles<C, Pr>)
58+
) {
59+
return function<P extends Pr, S>(WrappedComponent: React.ComponentClass<P, S>): React.FC<P> {
60+
const useStyles = createUseStyles<C, P, T>(styles)
61+
62+
const StyledComponent: React.FC<P> = (props: P) => {
63+
const {classes, ...passThroughProps} = props
64+
const theme = useTheme<T>()
65+
const reactJssClasses = useStyles({...(passThroughProps as P), theme})
66+
67+
return <WrappedComponent {...passThroughProps as P} classes={reactJssClasses} />
68+
}
69+
70+
StyledComponent.displayName = `withStyles(${WrappedComponent.name})`
71+
72+
return StyledComponent
73+
}
74+
}
75+
76+
export default withStyles
77+
```
78+
79+
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.
80+
81+
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.
82+
83+
## Migrating from Decorators
84+
85+
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:
86+
87+
```javascript
88+
import React from 'react'
89+
import decorator1 from 'some-hoc-library'
90+
import decorator2 from 'another-hoc-library'
91+
// ...
92+
import withStyles from 'path/to/custom-hoc'
93+
94+
const styles = {
95+
/* ... */
96+
}
97+
98+
@decorator1
99+
@decorator2
100+
// ...
101+
@withStyles(styles)
102+
class MyComponent extends React.Component {
103+
// ...
104+
}
105+
106+
export default MyComponent
107+
```
108+
109+
to this:
110+
111+
```javascript
112+
import React from 'react'
113+
import decorator1 from 'some-hoc-library'
114+
import decorator2 from 'another-hoc-library'
115+
// ...
116+
import withStyles from 'path/to/custom-hoc'
117+
118+
const styles = {
119+
/* ... */
120+
}
121+
122+
@decorator1
123+
@decorator2
124+
// ...
125+
class MyComponent extends React.Component {
126+
// ...
127+
}
128+
129+
export default withStyles(styles)(MyComponent)
130+
```
131+
132+
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.
133+
134+
If you don't use decorators or aren't familiar with them, then this won't be a concern for you.

docs/react-jss.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ React-JSS integrates [JSS](https://github.com/cssinjs/jss) with React using the
44

55
Try it out in the [playground](https://codesandbox.io/s/j3l06yyqpw).
66

7-
**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).**
7+
**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).**
88

99
### Benefits compared to using the core JSS package directly:
1010

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
"rollup-plugin-terser": "^7.0.2",
100100
"shelljs": "^0.8.2",
101101
"sinon": "4.5.0",
102-
"typescript": "^3.7.0",
102+
"typescript": "^4.2.3",
103103
"webpack": "^4.28.3",
104104
"zen-observable": "^0.6.0"
105105
}

packages/react-jss/tests/types/withStyles.tsx

+9-7
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ interface MyTheme {
1515
color: 'red'
1616
}
1717

18-
function SimpleComponent(props: MyProps) {
19-
return <div>{props.property}</div>
18+
class SimpleComponent extends React.Component<MyProps> {
19+
render() {
20+
return <div>{this.props.property}</div>
21+
}
2022
}
2123

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

139141
// A function argument cannot provide another defined theme type conflicting with `undefined`
140-
function failingFunctionRedefineTheme(theme: MyTheme): Styles<string, unknown, any> {
142+
function failingFunctionNullTheme(theme: MyTheme): Styles<string, unknown, null> {
141143
return {
142144
someClassName: '',
143145
anotherClassName: {
@@ -146,7 +148,7 @@ function failingFunctionRedefineTheme(theme: MyTheme): Styles<string, unknown, a
146148
}
147149
}
148150

149-
function passingFunctionUnknownTheme(theme: MyTheme): Styles<string, unknown, unknown> {
151+
function passingFunctionAnyTheme(theme: MyTheme): Styles<string, unknown, any> {
150152
return {
151153
someClassName: '',
152154
anotherClassName: {
@@ -155,7 +157,7 @@ function passingFunctionUnknownTheme(theme: MyTheme): Styles<string, unknown, un
155157
}
156158
}
157159

158-
function passingFunctionNullTheme(theme: MyTheme): Styles<string, unknown, null> {
160+
function passingFunctionUnknownTheme(theme: MyTheme): Styles<string, unknown, unknown> {
159161
return {
160162
someClassName: '',
161163
anotherClassName: {
@@ -165,6 +167,6 @@ function passingFunctionNullTheme(theme: MyTheme): Styles<string, unknown, null>
165167
}
166168

167169
// @ts-expect-error
168-
withStyles(failingFunctionRedefineTheme)(SimpleComponent)
170+
withStyles(failingFunctionNullTheme)(SimpleComponent)
171+
withStyles(passingFunctionAnyTheme)(SimpleComponent)
169172
withStyles(passingFunctionUnknownTheme)(SimpleComponent)
170-
withStyles(passingFunctionNullTheme)(SimpleComponent)

0 commit comments

Comments
 (0)