)
+) {
+ 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"