diff --git a/README.md b/README.md index 647b5c7..096697c 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ I highly recommend to add a bounty to the issue that you're waiting for to incre - [- HOC wrapping a component](#--hoc-wrapping-a-component) - [- HOC wrapping a component and injecting props](#--hoc-wrapping-a-component-and-injecting-props) - [- Nested HOC - wrapping a component, injecting props and connecting to redux 🌟](#--nested-hoc---wrapping-a-component-injecting-props-and-connecting-to-redux-) + - [- Legacy Recompose examples](#--legacy-recompose-examples) - [Redux Connected Components](#redux-connected-components) - [- Redux connected counter](#--redux-connected-counter) - [- Redux connected counter with own props](#--redux-connected-counter-with-own-props) @@ -974,6 +975,174 @@ export default () => ( [⇧ back to top](#table-of-contents) +### - Legacy Recompose examples + +[`recompose`](https://github.com/acdlite/recompose) is no longer actively maintained, so prefer [Hooks](#hooks) for new code. +These examples are intended for maintaining or migrating existing HOC-based codebases that already depend on `recompose`. + +The examples keep consumer props separate from injected props while using `compose`, `withState`, `withHandlers`, `withProps`, and `withStateHandlers`. +Only `@types/recompose` is added to the playground so these source-backed documentation examples stay type-checked without adding the legacy runtime package to the app bundle. + +```tsx +import * as React from 'react'; +import { compose, withHandlers, withProps, withState, withStateHandlers } from 'recompose'; + +type CounterOuterProps = { + title: string; + initialCount?: number; + step?: number; +}; + +type CounterStateProps = { + count: number; + setCount: (nextCount: number | ((currentCount: number) => number)) => void; +}; + +type CounterHandlers = { + onIncrement: () => void; + onReset: () => void; +}; + +type CounterLabelProps = { + label: string; +}; + +type CounterProps = CounterOuterProps & + CounterStateProps & + CounterHandlers & + CounterLabelProps; + +const CounterView: React.FC = ({ + count, + label, + onIncrement, + onReset, +}) => ( +
+

{label}

+

Current count: {count}

+ + +
+); + +const withCounterState = withState< + CounterOuterProps, + number, + 'count', + 'setCount' +>( + 'count', + 'setCount', + ({ initialCount = 0 }) => initialCount +); + +const withCounterHandlers = withHandlers< + CounterOuterProps & CounterStateProps, + CounterHandlers +>({ + onIncrement: + ({ setCount, step = 1 }) => + () => { + setCount(count => count + step); + }, + onReset: + ({ initialCount = 0, setCount }) => + () => { + setCount(initialCount); + }, +}); + +const withCounterLabel = withProps< + CounterLabelProps, + CounterOuterProps & CounterStateProps & CounterHandlers +>(({ count, title }) => ({ + label: `${title}: ${count}`, +})); + +const enhanceCounter = compose( + withCounterState, + withCounterHandlers, + withCounterLabel +); + +export const RecomposeCounter = enhanceCounter(CounterView); + +type ToggleOuterProps = { + label: string; + initiallyOpen?: boolean; +}; + +type ToggleState = { + isOpen: boolean; +}; + +type ToggleUpdaters = { + toggle: () => ToggleState; + close: () => ToggleState; +}; + +type ToggleProps = ToggleOuterProps & ToggleState & ToggleUpdaters; + +const ToggleView: React.FC = ({ close, isOpen, label, toggle }) => ( +
+ + {isOpen && ( + + )} +
+); + +const withToggleState = withStateHandlers< + ToggleState, + ToggleUpdaters, + ToggleOuterProps +>( + ({ initiallyOpen = false }) => ({ + isOpen: initiallyOpen, + }), + { + toggle: + ({ isOpen }) => + () => ({ + isOpen: !isOpen, + }), + close: () => () => ({ + isOpen: false, + }), + } +); + +export const RecomposeToggle = withToggleState(ToggleView); + +``` +
Click to expand

+ +```tsx +import * as React from 'react'; + +import { RecomposeCounter, RecomposeToggle } from './recompose-examples'; + +export default () => ( + <> + + + +); + +``` +

+ +[⇧ back to top](#table-of-contents) + --- ## Redux Connected Components diff --git a/README_SOURCE.md b/README_SOURCE.md index 358fcc3..cb36243 100644 --- a/README_SOURCE.md +++ b/README_SOURCE.md @@ -108,6 +108,7 @@ I highly recommend to add a bounty to the issue that you're waiting for to incre - [- HOC wrapping a component](#--hoc-wrapping-a-component) - [- HOC wrapping a component and injecting props](#--hoc-wrapping-a-component-and-injecting-props) - [- Nested HOC - wrapping a component, injecting props and connecting to redux 🌟](#--nested-hoc---wrapping-a-component-injecting-props-and-connecting-to-redux-) + - [- Legacy Recompose examples](#--legacy-recompose-examples) - [Redux Connected Components](#redux-connected-components) - [- Redux connected counter](#--redux-connected-counter) - [- Redux connected counter with own props](#--redux-connected-counter-with-own-props) @@ -438,6 +439,19 @@ Adds error handling using componentDidCatch to any component [⇧ back to top](#table-of-contents) +### - Legacy Recompose examples + +[`recompose`](https://github.com/acdlite/recompose) is no longer actively maintained, so prefer [Hooks](#hooks) for new code. +These examples are intended for maintaining or migrating existing HOC-based codebases that already depend on `recompose`. + +The examples keep consumer props separate from injected props while using `compose`, `withState`, `withHandlers`, `withProps`, and `withStateHandlers`. +Only `@types/recompose` is added to the playground so these source-backed documentation examples stay type-checked without adding the legacy runtime package to the app bundle. + +::codeblock='playground/src/hoc/recompose-examples.tsx':: +::expander='playground/src/hoc/recompose-examples.usage.tsx':: + +[⇧ back to top](#table-of-contents) + --- ## Redux Connected Components diff --git a/playground/package-lock.json b/playground/package-lock.json index 3eaa7ec..e21421c 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -19,6 +19,7 @@ "@types/react-dom": "18.0.3", "@types/react-redux": "7.1.24", "@types/react-router-dom": "5.3.3", + "@types/recompose": "0.30.15", "axios": "0.26.1", "cuid": "2.1.8", "react": "18.1.0", @@ -9537,6 +9538,16 @@ "@types/react": "*" } }, + "node_modules/@types/recompose": { + "version": "0.30.15", + "resolved": "https://registry.npmjs.org/@types/recompose/-/recompose-0.30.15.tgz", + "integrity": "sha512-glX9JbRTG4WSaWxDrsHlinoRC1YRb0vNr+ocPBgBTJgMawkYx8fqwuduahzy25XBSc3xfG/k9b5XPyW6FuHVkw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -37655,6 +37666,15 @@ "@types/react": "*" } }, + "@types/recompose": { + "version": "0.30.15", + "resolved": "https://registry.npmjs.org/@types/recompose/-/recompose-0.30.15.tgz", + "integrity": "sha512-glX9JbRTG4WSaWxDrsHlinoRC1YRb0vNr+ocPBgBTJgMawkYx8fqwuduahzy25XBSc3xfG/k9b5XPyW6FuHVkw==", + "requires": { + "@types/prop-types": "*", + "@types/react": "*" + } + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", diff --git a/playground/package.json b/playground/package.json index 5bc923f..d9db9ec 100644 --- a/playground/package.json +++ b/playground/package.json @@ -31,6 +31,7 @@ "@types/react-dom": "18.0.3", "@types/react-redux": "7.1.24", "@types/react-router-dom": "5.3.3", + "@types/recompose": "0.30.15", "axios": "0.26.1", "cuid": "2.1.8", "react": "18.1.0", diff --git a/playground/src/hoc/recompose-examples.tsx b/playground/src/hoc/recompose-examples.tsx new file mode 100644 index 0000000..dc87e0b --- /dev/null +++ b/playground/src/hoc/recompose-examples.tsx @@ -0,0 +1,138 @@ +import * as React from 'react'; +import { compose, withHandlers, withProps, withState, withStateHandlers } from 'recompose'; + +type CounterOuterProps = { + title: string; + initialCount?: number; + step?: number; +}; + +type CounterStateProps = { + count: number; + setCount: (nextCount: number | ((currentCount: number) => number)) => void; +}; + +type CounterHandlers = { + onIncrement: () => void; + onReset: () => void; +}; + +type CounterLabelProps = { + label: string; +}; + +type CounterProps = CounterOuterProps & + CounterStateProps & + CounterHandlers & + CounterLabelProps; + +const CounterView: React.FC = ({ + count, + label, + onIncrement, + onReset, +}) => ( +
+

{label}

+

Current count: {count}

+ + +
+); + +const withCounterState = withState< + CounterOuterProps, + number, + 'count', + 'setCount' +>( + 'count', + 'setCount', + ({ initialCount = 0 }) => initialCount +); + +const withCounterHandlers = withHandlers< + CounterOuterProps & CounterStateProps, + CounterHandlers +>({ + onIncrement: + ({ setCount, step = 1 }) => + () => { + setCount(count => count + step); + }, + onReset: + ({ initialCount = 0, setCount }) => + () => { + setCount(initialCount); + }, +}); + +const withCounterLabel = withProps< + CounterLabelProps, + CounterOuterProps & CounterStateProps & CounterHandlers +>(({ count, title }) => ({ + label: `${title}: ${count}`, +})); + +const enhanceCounter = compose( + withCounterState, + withCounterHandlers, + withCounterLabel +); + +export const RecomposeCounter = enhanceCounter(CounterView); + +type ToggleOuterProps = { + label: string; + initiallyOpen?: boolean; +}; + +type ToggleState = { + isOpen: boolean; +}; + +type ToggleUpdaters = { + toggle: () => ToggleState; + close: () => ToggleState; +}; + +type ToggleProps = ToggleOuterProps & ToggleState & ToggleUpdaters; + +const ToggleView: React.FC = ({ close, isOpen, label, toggle }) => ( +
+ + {isOpen && ( + + )} +
+); + +const withToggleState = withStateHandlers< + ToggleState, + ToggleUpdaters, + ToggleOuterProps +>( + ({ initiallyOpen = false }) => ({ + isOpen: initiallyOpen, + }), + { + toggle: + ({ isOpen }) => + () => ({ + isOpen: !isOpen, + }), + close: () => () => ({ + isOpen: false, + }), + } +); + +export const RecomposeToggle = withToggleState(ToggleView); diff --git a/playground/src/hoc/recompose-examples.usage.tsx b/playground/src/hoc/recompose-examples.usage.tsx new file mode 100644 index 0000000..1250d30 --- /dev/null +++ b/playground/src/hoc/recompose-examples.usage.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; + +import { RecomposeCounter, RecomposeToggle } from './recompose-examples'; + +export default () => ( + <> + + + +);