├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── jsconfig.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json └── src ├── rehook ├── __tests__ │ ├── branch-test.js │ ├── default-props-test.js │ ├── flatten-prop-test.js │ ├── index-test.js │ ├── map-props-test.js │ ├── pipe-test.js │ ├── rename-prop-test.js │ ├── rename-props-test.js │ ├── render-component-test.js │ ├── render-nothing-test.js │ ├── with-handlers-test.js │ ├── with-props-on-change-test.js │ ├── with-props-test.js │ ├── with-reducer-test.js │ ├── with-state-handlers-test.js │ └── with-state-test.js ├── branch.js ├── catch-render.js ├── default-props.js ├── flatten-prop.js ├── index.js ├── lifecycle.js ├── map-props.js ├── namespace.js ├── pipe.js ├── rename-prop.js ├── rename-props.js ├── render-component.js ├── render-nothing.js ├── test-utils.js ├── with-handlers.js ├── with-props-on-change.js ├── with-props.js ├── with-reducer.js ├── with-state-handlers.js └── with-state.js └── setupTests.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/react"], 3 | "ignore": ["**/__tests__"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "standard", 4 | "prettier", 5 | "plugin:react/recommended", 6 | "prettier/react", 7 | "prettier/standard", 8 | "plugin:jsx-a11y/recommended" 9 | ], 10 | "plugins": ["babel", "react", "prettier", "standard", "jsx-a11y"], 11 | "parser": "babel-eslint", 12 | "parserOptions": { 13 | "sourceType": "module", 14 | "ecmaFeatures": { 15 | "jsx": true 16 | } 17 | }, 18 | "env": { 19 | "es6": true, 20 | "node": true 21 | }, 22 | "rules": { 23 | "prettier/prettier": "error", 24 | "react/display-name": [0], 25 | "no-unused-vars": [2, { "args": "after-used", "argsIgnorePattern": "^_" }] 26 | }, 27 | "globals": { 28 | "FormData": true, 29 | "fetch": true, 30 | "Event": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | dist 26 | build 27 | 28 | # microbundle requires yarn :( for jsx 29 | package-lock.json 30 | 31 | # A test-utils folder is also generated to aid in testing 32 | test-utils 33 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | public 2 | src 3 | yarn.lock 4 | build 5 | coverage 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "11" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ryan Allred 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rehook 2 | 3 | [![Build Status](https://travis-ci.org/Synvox/rehook.svg?branch=master)](https://travis-ci.org/Synvox/rehook) 4 | 5 | Rehook implements an API similar to [Recompose](https://github.com/acdlite/recompose), but using React Hooks. 6 | 7 | ``` 8 | npm i @synvox/rehook 9 | ``` 10 | 11 | ## What is Rehook? 12 | 13 | Hooks are a great idea and I want to migrate my enhancers from Recompose to React Hooks. 14 | 15 | React Hooks can do most of what Recompose can do, but without wrapping components in other components. This is a huge win! But what happens to all the code written to use recompose? Rehook is a migration strategy from higher order components to hooks. 16 | 17 | _With Rehook_ 18 | 19 | ```js 20 | import React from 'react' 21 | 22 | import { withState, pipe, withHandlers } from '@synvox/rehook' 23 | 24 | const useCount = pipe( 25 | withState('count', 'setCount', 0), 26 | withHandlers({ 27 | increment: ({ count, setCount }) => () => setCount(count + 1), 28 | decrement: ({ count, setCount }) => () => setCount(count - 1), 29 | }) 30 | ) 31 | 32 | function Counter() { 33 | const { count, increment, decrement } = useCount() 34 | 35 | return ( 36 |
37 | 38 | {count} 39 | 40 |
41 | ) 42 | } 43 | 44 | export default Counter 45 | ``` 46 | 47 | _With Recompose_ 48 | 49 | ```js 50 | import React from 'react' 51 | 52 | import { compose, withState, withHandlers } from 'recompose' 53 | 54 | const enhance = compose( 55 | withState('count', 'setCount', 0), 56 | withHandlers({ 57 | increment: ({ count, setCount }) => () => setCount(count + 1), 58 | decrement: ({ count, setCount }) => () => setCount(count - 1), 59 | }) 60 | ) 61 | 62 | function Counter({ count, increment, decrement }) { 63 | return ( 64 |
65 | 66 | {count} 67 | 68 |
69 | ) 70 | } 71 | 72 | export default enhance(Counter) 73 | ``` 74 | 75 | Notice how subtle the changes are. 76 | 77 | ### Smart/Presentational Components: 78 | 79 | In Recompose, you are required to pass all props through each component until it reaches your presentational component. This is not the case with Rehook, but you may choose run all your props through an enhancer using `pipe()`. This will look more familiar to those who have used recompose before. 80 | 81 | ```js 82 | import React from 'react' 83 | 84 | import { withState, pipe, withHandlers } from '@synvox/rehook' 85 | 86 | const enhance = pipe( 87 | withState('count', 'setCount', 0), 88 | withHandlers({ 89 | increment: ({ count, setCount }) => () => setCount(count + 1), 90 | decrement: ({ count, setCount }) => () => setCount(count - 1), 91 | }) 92 | ) 93 | 94 | function Counter({ count, increment, decrement }) { 95 | return ( 96 |
97 | 98 | {count} 99 | 100 |
101 | ) 102 | } 103 | 104 | export default pipe( 105 | enhance, 106 | Counter 107 | ) 108 | ``` 109 | 110 | ## Docs 111 | 112 | _Full disclaimer: Most of these docs are modified from the Recompose docs._ 113 | 114 | - [`pipe()`](#pipe) 115 | - [`mapProps()`](#mapprops) 116 | - [`withProps()`](#withprops) 117 | - [`withPropsOnChange()`](#withpropsonchange) 118 | - [`withHandlers()`](#withhandlers) 119 | - [`namespace()`](#withhandlers) 120 | - [`defaultProps()`](#defaultprops) 121 | - [`renameProp()`](#renameprop) 122 | - [`renameProps()`](#renameprops) 123 | - [`flattenProp()`](#flattenprop) 124 | - [`withState()`](#withstate) 125 | - [`withStateHandlers()`](#withstatehandlers) 126 | - [`withReducer()`](#withreducer) 127 | - [`branch()`](#branch) 128 | - [`renderComponent()`](#rendercomponent) 129 | - [`renderNothing()`](#rendernothing) 130 | - [`catchRender()`](#rehook-1) 131 | - [`lifecycle()`](#lifecycle) 132 | 133 | ### `pipe()` 134 | 135 | ```js 136 | pipe(...functions: Array): Function 137 | ``` 138 | 139 | In recompose, you `compose` enhancers. In `rehook` each enhancer is a function that takes `props` and returns new `props`. Use `pipe` instead of `compose` to chain these together. 140 | 141 | ### `mapProps()` 142 | 143 | ```js 144 | mapProps( 145 | propsMapper: (ownerProps: Object) => Object, 146 | ): (props: Object) => Object 147 | ``` 148 | 149 | Accepts a function that maps owner props to a new collection of props that are passed to the base component. 150 | 151 | ### `withProps()` 152 | 153 | ```js 154 | withProps( 155 | createProps: (ownerProps: Object) => Object | Object 156 | ): (props: Object) => Object 157 | ``` 158 | 159 | Like `mapProps()`, except the newly created props are merged with the owner props. 160 | 161 | Instead of a function, you can also pass a props object directly. In this form, it is similar to `defaultProps()`, except the provided props take precedence over props from the owner. 162 | 163 | ### `withPropsOnChange()` 164 | 165 | ```js 166 | withPropsOnChange( 167 | shouldMapOrKeys: Array | (props: Object, nextProps: Object) => boolean, 168 | createProps: (ownerProps: Object) => Object 169 | ): (props: Object) => Object 170 | ``` 171 | 172 | Like `withProps()`, except the new props are only created when one of the owner props specified by `shouldMapOrKeys` changes. This helps ensure that expensive computations inside `createProps()` are only executed when necessary. 173 | 174 | Instead of an array of prop keys, the first parameter can also be a function that returns a boolean, given the current props and the next props. This allows you to customize when `createProps()` should be called. 175 | 176 | ### `withHandlers()` 177 | 178 | ```js 179 | withHandlers( 180 | handlerCreators: { 181 | [handlerName: string]: (props: Object) => Function 182 | } | 183 | handlerCreatorsFactory: (initialProps) => { 184 | [handlerName: string]: (props: Object) => Function 185 | } 186 | ): (props: Object) => Object 187 | ``` 188 | 189 | Takes an object map of handler creators or a factory function. These are higher-order functions that accept a set of props and return a function handler: 190 | 191 | This allows the handler to access the current props via closure, without needing to change its signature. 192 | 193 | Usage example: 194 | 195 | ```js 196 | const useForm = pipe( 197 | withState('value', 'updateValue', ''), 198 | withHandlers({ 199 | onChange: props => event => { 200 | props.updateValue(event.target.value) 201 | }, 202 | onSubmit: props => event => { 203 | event.preventDefault() 204 | submitForm(props.value) 205 | }, 206 | }) 207 | ) 208 | 209 | function Form() { 210 | const { value, onChange, onSubmit } = useForm() 211 | 212 | return ( 213 |
214 | 218 |
219 | ) 220 | } 221 | ``` 222 | 223 | ### `namespace()` 224 | 225 | ```js 226 | namespace( 227 | namespaceKey: string | symbol, 228 | createProps: (ownerProps: Object) => () => Object 229 | ): (props: Object) => Object 230 | ``` 231 | 232 | The namespace function allows you to scope an enhancer at a key. It does the opposite of `flattenProp()`, by assigning the result of a call to a key specified by `namespaceKey` on the props object. 233 | 234 | Usage Example: 235 | 236 | ```js 237 | const useForm = pipe( 238 | withState('value', 'updateValue', ''), 239 | namespace('handlers', parentProps => 240 | pipe( 241 | withHandlers({ 242 | onChange: props => event => { 243 | parentProps.updateValue(event.target.value) 244 | }, 245 | onSubmit: props => event => { 246 | event.preventDefault() 247 | submitForm(parentProps.value) 248 | }, 249 | }) 250 | ) 251 | ) 252 | ) 253 | 254 | function Form() { 255 | const { 256 | value, 257 | handlers: { onChange, onSubmit }, 258 | } = useForm() 259 | 260 | return ( 261 |
262 | 266 |
267 | ) 268 | } 269 | ``` 270 | 271 | ### `defaultProps()` 272 | 273 | ```js 274 | defaultProps( 275 | props: Object 276 | ): (props: Object) => Object 277 | ``` 278 | 279 | Specifies props to be included by default. Similar to `withProps()`, except the props from the owner take precedence over props provided to `defaultProps()`. 280 | 281 | ### `renameProp()` 282 | 283 | ```js 284 | renameProp( 285 | oldName: string, 286 | newName: string 287 | ): (props: Object) => Object 288 | ``` 289 | 290 | Renames a single prop. 291 | 292 | ### `renameProps()` 293 | 294 | ```js 295 | renameProps( 296 | nameMap: { [key: string]: string } 297 | ): (props: Object) => Object 298 | ``` 299 | 300 | Renames multiple props, using a map of old prop names to new prop names. 301 | 302 | ### `flattenProp()` 303 | 304 | ```js 305 | flattenProp( 306 | propName: string 307 | ): (props: Object) => Object 308 | ``` 309 | 310 | Flattens a prop so that its fields are spread out into the props object. 311 | 312 | ```js 313 | const useProps = pipe( 314 | withProps({ 315 | object: { a: 'a', b: 'b' }, 316 | c: 'c', 317 | }), 318 | flattenProp('object') 319 | ) 320 | 321 | // useProps() returns: { a: 'a', b: 'b', c: 'c', object: { a: 'a', b: 'b' } } 322 | ``` 323 | 324 | ### `withState()` 325 | 326 | ```js 327 | withState( 328 | stateName: string, 329 | stateUpdaterName: string, 330 | initialState: any | (props: Object) => any 331 | ): (props: Object) => Object 332 | ``` 333 | 334 | Includes two additional props: a state value, and a function to update that state value. The state updater has the following signature: 335 | 336 | ```js 337 | stateUpdater((prevValue: T) => T, ?callback: Function): void 338 | stateUpdater(newValue: any, ?callback: Function): void 339 | ``` 340 | 341 | The first form accepts a function which maps the previous state value to a new state value. You'll likely want to use this state updater along with `withHandlers()` to create specific updater functions. For example, to create an enhancer that adds basic counting functionality to a component: 342 | 343 | ```js 344 | const addCounting = pipe( 345 | withState('counter', 'setCounter', 0), 346 | withHandlers({ 347 | increment: ({ setCounter }) => () => setCounter(n => n + 1), 348 | decrement: ({ setCounter }) => () => setCounter(n => n - 1), 349 | reset: ({ setCounter }) => () => setCounter(0), 350 | }) 351 | ) 352 | ``` 353 | 354 | The second form accepts a single value, which is used as the new state. 355 | 356 | Both forms accept an optional second parameter, a callback function that will be executed once `setState()` is completed and the component is re-rendered. 357 | 358 | An initial state value is required. It can be either the state value itself, or a function that returns an initial state given the initial props. 359 | 360 | ### `withStateHandlers()` 361 | 362 | ```js 363 | withStateHandlers( 364 | (initialState: Object | ((props: Object) => any)), 365 | (stateUpdaters: { 366 | [key: string]: ( 367 | state: Object, 368 | props: Object 369 | ) => (...payload: any[]) => Object, 370 | }) 371 | ) 372 | ``` 373 | 374 | Passes state object properties and immutable updater functions 375 | in a form of `(...payload: any[]) => Object`. 376 | 377 | Every state updater function accepts state, props and payload and must return a new state or undefined. The new state is shallowly merged with the previous state. 378 | Returning undefined does not cause a component rerender. 379 | 380 | Example: 381 | 382 | ```js 383 | const useCounter = withStateHandlers( 384 | ({ initialCounter = 0 }) => ({ 385 | counter: initialCounter, 386 | }), 387 | { 388 | incrementOn: ({ counter }) => value => ({ 389 | counter: counter + value, 390 | }), 391 | decrementOn: ({ counter }) => value => ({ 392 | counter: counter - value, 393 | }), 394 | resetCounter: (_, { initialCounter = 0 }) => () => ({ 395 | counter: initialCounter, 396 | }), 397 | } 398 | ) 399 | 400 | function Counter() { 401 | const { counter, incrementOn, decrementOn, resetCounter } = useCounter() 402 | 403 | return ( 404 |
405 | 406 | 407 | 408 |
409 | ) 410 | } 411 | ``` 412 | 413 | ### `withReducer()` 414 | 415 | ```js 416 | withReducer( 417 | stateName: string, 418 | dispatchName: string, 419 | reducer: (state: S, action: A) => S, 420 | initialState: S | (ownerProps: Object) => S 421 | ): (props: Object) => Object 422 | ``` 423 | 424 | Similar to `withState()`, but state updates are applied using a reducer function. A reducer is a function that receives a state and an action, and returns a new state. 425 | 426 | Passes two additional props to the base component: a state value, and a dispatch method. The dispatch method has the following signature: 427 | 428 | ```js 429 | dispatch(action: Object, ?callback: Function): void 430 | ``` 431 | 432 | It sends an action to the reducer, after which the new state is applied. It also accepts an optional second parameter, a callback function with the new state as its only argument. 433 | 434 | ### `branch()` 435 | 436 | ```js 437 | branch( 438 | test: (props: Object) => boolean, 439 | left: (props: Object) => Object, 440 | right: ?(props: Object) => Object 441 | ): (props: Object) => Object 442 | ``` 443 | 444 | Accepts a test function and two functions. The test function is passed the props from the owner. If it returns true, the `left` function called with `props`; otherwise, the `right` function is called with `props`. If the `right` is not supplied, it will return `props` like normal. 445 | 446 | ### `renderComponent()` 447 | 448 | ```js 449 | renderComponent( 450 | Component: ReactClass | ReactFunctionalComponent | string 451 | ): (props: Object) => Object 452 | ``` 453 | 454 | Stops the function execution and renders a component. Use with `catchRender()`. 455 | 456 | > `renderComponent()` is a tricky enhancer to implement with hooks. 😔 It will `throw` a component to signal to `rehook()` that it should stop the function and render that component. This sometimes causes issues with hook’s positional state system. It is advised to use `renderComponent()` after stateful enhancers like `withState` and after effect handlers like `lifecycle`. React will throw an error if this is called too soon. 457 | 458 | This is useful in combination with another enhancer like `branch()`: 459 | 460 | ```js 461 | // `isLoading()` is a function that returns whether or not the component 462 | // is in a loading state 463 | const spinnerWhileLoading = isLoading => 464 | branch( 465 | isLoading, 466 | renderComponent(Spinner) // `Spinner` is a React component 467 | ) 468 | 469 | // Now use the `spinnerWhileLoading()` helper to add a loading spinner to any 470 | // base component 471 | const break = spinnerWhileLoading( 472 | props => !(props.title && props.author && props.content) 473 | ) 474 | 475 | const Post = catchRender((props) => { 476 | useSpinner(props) 477 | const { title, author, content } = props 478 | 479 | return ( 480 |
481 |

{title}

482 |

By {author.name}

483 |
{content}
484 |
485 | ) 486 | }) 487 | 488 | export default Post 489 | ``` 490 | 491 | ### `renderNothing()` 492 | 493 | ```js 494 | renderNothing: (props: Object) => Object 495 | ``` 496 | 497 | An enhancer that always renders `null`. Use with `catchRender()`. 498 | 499 | > `renderNothing()` is a tricky enhancer to implement with hooks. 😔 It will `throw` a component to signal to `rehook()` that it should stop the function and render that component. This sometimes causes issues with hook’s positional state system. It is advised to use `renderNothing()` after stateful enhancers like `withState` and after effect handlers like `lifecycle`. React will throw an error if this is called too soon. 500 | 501 | This is useful in combination with another helper that expects a higher-order component, like `branch()`: 502 | 503 | ```js 504 | // `hasNoData()` is a function that returns true if the component has 505 | // no data 506 | const hideIfNoData = hasNoData => branch(hasNoData, renderNothing) 507 | 508 | // Now use the `hideIfNoData()` helper to hide any base component 509 | const useHidden = hideIfNoData( 510 | props => !(props.title && props.author && props.content) 511 | ) 512 | 513 | const Post = catchRender(props => { 514 | useHidden(props) 515 | const { title, author, content } = props 516 | 517 | return ( 518 |
519 |

{title}

520 |

By {author.name}

521 |
{content}
522 |
523 | ) 524 | }) 525 | 526 | export default Post 527 | ``` 528 | 529 | ### `catchRender()` 530 | 531 | ```js 532 | catchRender( 533 | component: (props: Object) => ReactElement 534 | ): FunctionComponent 535 | ``` 536 | 537 | If you use `renderComponent()` or `renderNothing()` wrap your function component with with `catchRender()`. 538 | 539 | ### `lifecycle()` 540 | 541 | ```js 542 | lifecycle( 543 | spec: Object, 544 | ): (props: Object) => Object 545 | ``` 546 | 547 | Lifecycle supports `componentDidMount`, `componentWillUnmount`, `componentDidUpdate`. 548 | 549 | Any state changes made in a lifecycle method, by using `setState`, will be merged with props. 550 | 551 | Example: 552 | 553 | ```js 554 | const usePosts = lifecycle({ 555 | componentDidMount() { 556 | fetchPosts().then(posts => { 557 | this.setState({ posts }) 558 | }) 559 | }, 560 | }) 561 | 562 | function PostsList() { 563 | const { posts = [] } = usePosts() 564 | 565 | return ( 566 |
    567 | {posts.map(p => ( 568 |
  • {p.title}
  • 569 | ))} 570 |
571 | ) 572 | } 573 | ``` 574 | 575 | ### Test Utility: 576 | 577 | Rehook also provides a test utility for testing enhancers. This makes writing tests easy and readable. This depends on `enzyme`. 578 | 579 | Usage Example: 580 | 581 | ```js 582 | import testEnhancer from '@synvox/rehook/test-utils' 583 | 584 | // Somehow import your enhancer: 585 | const enhancer = withState('state', 'setState', 0) 586 | 587 | test('with state', () => { 588 | const getProps = testEnhancer(enhancer) 589 | 590 | expect(getProps().state).toEqual(0) 591 | getProps().setState(1) 592 | expect(getProps().state).toEqual(1) 593 | }) 594 | ``` 595 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "jsx": "react", 5 | "module": "commonjs", 6 | "baseUrl": "src", 7 | "checkJs": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@synvox/rehook", 3 | "public": true, 4 | "version": "0.0.17", 5 | "main": "dist/index.js", 6 | "devDependencies": { 7 | "@babel/cli": "^7.1.5", 8 | "@babel/preset-env": "^7.1.6", 9 | "@babel/preset-react": "^7.0.0", 10 | "@otris/jsdoc-tsd": "^1.0.4", 11 | "@types/jest": "^24.0.11", 12 | "babel-eslint": "9.0.0", 13 | "enzyme": "^3.7.0", 14 | "enzyme-adapter-react-16": "^1.6.0", 15 | "eslint": "5.6.0", 16 | "eslint-config-prettier": "^3.1.0", 17 | "eslint-config-standard": "^12.0.0", 18 | "eslint-plugin-babel": "^5.2.1", 19 | "eslint-plugin-import": "^2.14.0", 20 | "eslint-plugin-jsx-a11y": "^6.1.2", 21 | "eslint-plugin-node": "^7.0.1", 22 | "eslint-plugin-prettier": "^3.0.0", 23 | "eslint-plugin-promise": "^4.0.1", 24 | "eslint-plugin-react": "^7.11.1", 25 | "eslint-plugin-standard": "^4.0.0", 26 | "husky": "^1.1.3", 27 | "jsdoc": "^3.5.5", 28 | "prettier": "1.14.3", 29 | "react": "^16.8.6", 30 | "react-dom": "^16.8.6", 31 | "react-scripts": "2.1.0", 32 | "tsc": "^1.20150623.0" 33 | }, 34 | "peerDependencies": { 35 | "react": "^16.8.6", 36 | "react-dom": "^16.8.6" 37 | }, 38 | "scripts": { 39 | "start": "react-scripts start", 40 | "build:cra": "react-scripts build", 41 | "test:eslint": "eslint src --fix", 42 | "test:jest": "react-scripts test", 43 | "test": "CI=true npm run test:eslint && CI=true npm run test:jest --findRelatedTests", 44 | "eject": "react-scripts eject", 45 | "build": "npm run build:bundle && npm run build:types && npm run build:package", 46 | "build:bundle": "NODE_ENV='production' babel -s -d ./dist/ ./src/rehook", 47 | "build:package": "cp ./README.md dist/README.md && cp ./package.json dist/package.json", 48 | "build:types": "jsdoc -t node_modules/@otris/jsdoc-tsd -r ./src/rehook -d dist/rehook.d.ts", 49 | "pub": "npm run build && npm publish" 50 | }, 51 | "browserslist": [ 52 | ">0.2%", 53 | "not dead", 54 | "not ie <= 11", 55 | "not op_mini all" 56 | ], 57 | "dependencies": {}, 58 | "husky": { 59 | "hooks": { 60 | "pre-commit": "yarn test", 61 | "pre-push": "yarn test" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synvox/rehook/5a2164ca94820fbc2661cace87887fb7504b4176/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/rehook/__tests__/branch-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import testUtil from '../test-utils' 4 | import branch from '../branch' 5 | import withProps from '../with-props' 6 | 7 | test('branch left', () => { 8 | const getProps = testUtil( 9 | branch( 10 | ({ val }) => val, 11 | withProps({ left: true }), 12 | withProps({ right: true }) 13 | ), 14 | { val: true } 15 | ) 16 | 17 | expect(getProps().left).toBe(true) 18 | }) 19 | 20 | test('branch right', () => { 21 | const getProps = testUtil( 22 | branch( 23 | ({ val }) => val, 24 | withProps({ left: true }), 25 | withProps({ right: true }) 26 | ), 27 | { val: false } 28 | ) 29 | 30 | expect(getProps().right).toBe(true) 31 | }) 32 | 33 | test('branch without right', () => { 34 | const getProps = testUtil( 35 | branch(({ val }) => val, withProps({ left: true })), 36 | { val: false } 37 | ) 38 | 39 | expect(getProps()).toEqual({ val: false }) 40 | }) 41 | -------------------------------------------------------------------------------- /src/rehook/__tests__/default-props-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import testUtil from '../test-utils' 4 | import defaultProps from '../default-props' 5 | 6 | test('default props without prop', () => { 7 | const getProps = testUtil( 8 | defaultProps({ 9 | val: false, 10 | }), 11 | {} 12 | ) 13 | 14 | expect(getProps().val).toBe(false) 15 | }) 16 | 17 | test('default props with prop', () => { 18 | const getProps = testUtil( 19 | defaultProps({ 20 | val: false, 21 | }), 22 | { val: true } 23 | ) 24 | 25 | expect(getProps().val).toBe(true) 26 | }) 27 | -------------------------------------------------------------------------------- /src/rehook/__tests__/flatten-prop-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import testUtil from '../test-utils' 4 | import flattenProp from '../flatten-prop' 5 | 6 | test('flattens props', () => { 7 | const getProps = testUtil(flattenProp('obj'), { obj: { a: true, b: false } }) 8 | 9 | expect(getProps().a).toBe(true) 10 | expect(getProps().b).toBe(false) 11 | }) 12 | -------------------------------------------------------------------------------- /src/rehook/__tests__/index-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import * as exported from '../' 4 | 5 | test('exports expected', () => { 6 | expect(Object.keys(exported)).toEqual([ 7 | 'branch', 8 | 'catchRender', 9 | 'defaultProps', 10 | 'flattenProp', 11 | 'lifecycle', 12 | 'mapProps', 13 | 'namespace', 14 | 'pipe', 15 | 'renameProp', 16 | 'renameProps', 17 | 'renderComponent', 18 | 'renderNothing', 19 | 'withHandlers', 20 | 'withPropsOnChange', 21 | 'withProps', 22 | 'withReducer', 23 | 'withStateHandlers', 24 | 'withState', 25 | ]) 26 | }) 27 | -------------------------------------------------------------------------------- /src/rehook/__tests__/map-props-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import testUtil from '../test-utils' 4 | import mapProps from '../map-props' 5 | 6 | test('maps props', () => { 7 | const getProps = testUtil(mapProps(({ b }) => ({ b })), { 8 | a: true, 9 | b: false, 10 | }) 11 | 12 | expect(getProps().a).toBe(undefined) // 13 | expect(getProps().b).toBe(false) 14 | }) 15 | -------------------------------------------------------------------------------- /src/rehook/__tests__/pipe-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import pipe from '../pipe' 4 | 5 | test('pipe', () => { 6 | expect( 7 | pipe( 8 | num => num + 1, 9 | num => num + 1 10 | )(0) 11 | ).toBe(2) 12 | 13 | expect( 14 | pipe( 15 | () => ({ num: 0 }), 16 | x => ({ num: x.num + 1 }) 17 | )() 18 | ).toEqual({ num: 1 }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/rehook/__tests__/rename-prop-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import testUtil from '../test-utils' 4 | import renameProp from '../rename-prop' 5 | 6 | test('branch left', () => { 7 | const getProps = testUtil(renameProp('val', 'renamed'), { val: true }) 8 | 9 | expect(getProps().renamed).toBe(true) 10 | }) 11 | -------------------------------------------------------------------------------- /src/rehook/__tests__/rename-props-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import testUtil from '../test-utils' 4 | import renameProps from '../rename-props' 5 | 6 | test('branch left', () => { 7 | const getProps = testUtil(renameProps({ val: 'renamed', val2: 'renamed2' }), { 8 | val: true, 9 | val2: 0, 10 | other: false, 11 | }) 12 | 13 | expect(getProps().renamed).toBe(true) 14 | expect(getProps().renamed2).toBe(0) 15 | expect(getProps().other).toBe(false) 16 | }) 17 | -------------------------------------------------------------------------------- /src/rehook/__tests__/render-component-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import renderComponent from '../render-component' 4 | 5 | test('render component', () => { 6 | let e 7 | 8 | try { 9 | renderComponent(() => 'something')() 10 | } catch (thrown) { 11 | e = thrown 12 | } 13 | 14 | expect(e).toBe('something') 15 | }) 16 | -------------------------------------------------------------------------------- /src/rehook/__tests__/render-nothing-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import renderNothing from '../render-nothing' 3 | 4 | test('render nothing', () => { 5 | let e 6 | 7 | try { 8 | renderNothing() 9 | } catch (thrown) { 10 | e = thrown 11 | } 12 | 13 | expect(e).toBe(null) 14 | }) 15 | -------------------------------------------------------------------------------- /src/rehook/__tests__/with-handlers-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import testUtil from '../test-utils' 4 | import withHandlers from '../with-handlers' 5 | 6 | test('with handlers', () => { 7 | let result = null 8 | const getProps = testUtil( 9 | withHandlers({ 10 | handle: ({ a }) => ({ b }) => (result = { a, b }), 11 | }), 12 | { a: true } 13 | ) 14 | 15 | getProps().handle({ b: true }) 16 | expect(result).toEqual({ a: true, b: true }) 17 | }) 18 | 19 | test('with handlers memo', () => { 20 | let result = null 21 | let called = 0 22 | const getProps = testUtil( 23 | withHandlers(() => { 24 | called += 1 25 | return { 26 | handle: ({ a }) => ({ b }) => (result = { a, b }), 27 | } 28 | }), 29 | { a: true } 30 | ) 31 | 32 | getProps().handle({ b: true }) 33 | expect(called).toBe(1) 34 | expect(result).toEqual({ a: true, b: true }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/rehook/__tests__/with-props-on-change-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import testUtil from '../test-utils' 4 | import pipe from '../pipe' 5 | import withState from '../with-state' 6 | import withPropsOnChange from '../with-props-on-change' 7 | import { act } from 'react-dom/test-utils' 8 | 9 | test('maps props using fn true', () => { 10 | let called = 0 11 | const getProps = testUtil( 12 | pipe( 13 | withState('b', 'setB', 0), 14 | withPropsOnChange( 15 | () => true, 16 | ({ b }) => { 17 | called += 1 18 | return { c: b } 19 | } 20 | ) 21 | ), 22 | { 23 | a: true, 24 | } 25 | ) 26 | 27 | expect(getProps().a).toBe(true) 28 | expect(getProps().b).toBe(0) 29 | expect(called).toBe(1) 30 | act(() => getProps().setB(1)) 31 | expect(called).toBe(2) 32 | expect(getProps().a).toBe(true) 33 | expect(getProps().c).toBe(1) 34 | }) 35 | 36 | test('maps props using fn false', () => { 37 | let called = 0 38 | const getProps = testUtil( 39 | pipe( 40 | withState('b', 'setB', 0), 41 | withPropsOnChange( 42 | () => false, 43 | ({ b }) => { 44 | called += 1 45 | return { c: b } 46 | } 47 | ) 48 | ), 49 | { 50 | a: true, 51 | } 52 | ) 53 | 54 | expect(getProps().a).toBe(true) 55 | expect(getProps().b).toBe(0) 56 | expect(called).toBe(1) 57 | act(() => getProps().setB(1)) 58 | expect(called).toBe(1) 59 | expect(getProps().a).toBe(true) 60 | expect(getProps().c).toBe(0) 61 | }) 62 | 63 | test('maps props using keys', () => { 64 | let called = 0 65 | const getProps = testUtil( 66 | pipe( 67 | withState('b', 'setB', 0), 68 | withPropsOnChange(['b'], ({ b }) => { 69 | called += 1 70 | return { b } 71 | }) 72 | ), 73 | { 74 | a: true, 75 | } 76 | ) 77 | 78 | expect(getProps().a).toBe(true) 79 | expect(getProps().b).toBe(0) 80 | expect(called).toBe(1) 81 | act(() => getProps().setB(1)) 82 | expect(called).toBe(2) 83 | expect(getProps().a).toBe(true) 84 | expect(getProps().b).toBe(1) 85 | }) 86 | -------------------------------------------------------------------------------- /src/rehook/__tests__/with-props-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import testUtil from '../test-utils' 4 | import withProps from '../with-props' 5 | 6 | test('maps props', () => { 7 | const getProps = testUtil(withProps(({ b }) => ({ b })), { 8 | a: true, 9 | b: false, 10 | }) 11 | 12 | expect(getProps().a).toBe(true) 13 | expect(getProps().b).toBe(false) 14 | }) 15 | -------------------------------------------------------------------------------- /src/rehook/__tests__/with-reducer-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import testUtil from '../test-utils' 4 | import withReducer from '../with-reducer' 5 | import { act } from 'react-dom/test-utils' 6 | 7 | test('with state handlers', () => { 8 | const getProps = testUtil( 9 | withReducer( 10 | 'state', 11 | 'dispatch', 12 | (state, action) => { 13 | switch (action.type) { 14 | case 'INCREMENT': 15 | return { count: state.count + 1 } 16 | default: 17 | return state 18 | } 19 | }, 20 | { count: 0 } 21 | ), 22 | {} 23 | ) 24 | 25 | expect(getProps().state).toEqual({ count: 0 }) 26 | act(() => getProps().dispatch({ type: 'INCREMENT' })) 27 | expect(getProps().state).toEqual({ count: 1 }) 28 | }) 29 | 30 | test('with state handlers memo', () => { 31 | const getProps = testUtil( 32 | withReducer( 33 | 'state', 34 | 'dispatch', 35 | (state, action) => { 36 | switch (action.type) { 37 | case 'INCREMENT': 38 | return { count: state.count + 1 } 39 | default: 40 | return state 41 | } 42 | }, 43 | () => ({ count: 0 }) 44 | ), 45 | {} 46 | ) 47 | 48 | expect(getProps().state).toEqual({ count: 0 }) 49 | act(() => getProps().dispatch({ type: 'INCREMENT' })) 50 | expect(getProps().state).toEqual({ count: 1 }) 51 | }) 52 | -------------------------------------------------------------------------------- /src/rehook/__tests__/with-state-handlers-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import testUtil from '../test-utils' 4 | import withStateHandlers from '../with-state-handlers' 5 | import { act } from 'react-dom/test-utils' 6 | 7 | test('with state handlers', () => { 8 | const getProps = testUtil( 9 | withStateHandlers( 10 | { b: false }, 11 | { 12 | handle: () => ({ b }) => ({ b }), 13 | } 14 | ), 15 | {} 16 | ) 17 | 18 | act(() => getProps().handle({ b: true })) 19 | expect(getProps().b).toEqual(true) 20 | }) 21 | 22 | test('with state handlers calling undefined', () => { 23 | const getProps = testUtil( 24 | withStateHandlers( 25 | { b: false }, 26 | { 27 | handle: () => () => undefined, 28 | } 29 | ), 30 | {} 31 | ) 32 | 33 | act(() => getProps().handle()) 34 | expect(getProps().b).toEqual(false) 35 | }) 36 | 37 | test('with state handlers memo', () => { 38 | const getProps = testUtil( 39 | withStateHandlers(() => ({ b: false }), { 40 | handle: () => ({ b }) => ({ b }), 41 | }), 42 | {} 43 | ) 44 | 45 | act(() => getProps().handle({ b: true })) 46 | expect(getProps().b).toEqual(true) 47 | }) 48 | -------------------------------------------------------------------------------- /src/rehook/__tests__/with-state-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | import testUtil from '../test-utils' 4 | import withState from '../with-state' 5 | import { act } from 'react-dom/test-utils' 6 | 7 | test('with state', () => { 8 | const getProps = testUtil(withState('state', 'setState', 0)) 9 | 10 | expect(getProps().state).toEqual(0) 11 | act(() => getProps().setState(1)) 12 | expect(getProps().state).toEqual(1) 13 | }) 14 | test('with state function', () => { 15 | let called = 0 16 | const getProps = testUtil( 17 | withState('state', 'setState', () => { 18 | called += 1 19 | return 0 20 | }) 21 | ) 22 | 23 | expect(getProps().state).toEqual(0) 24 | act(() => getProps().setState(1)) 25 | expect(getProps().state).toEqual(1) 26 | expect(called).toEqual(1) 27 | }) 28 | -------------------------------------------------------------------------------- /src/rehook/branch.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { useMemo } from 'react' 3 | // Note, branching disobeys one of the hook rules because 4 | // it wraps hooks in a condition. For this reason, the branch 5 | // is cached and kept the same regardless of updates. 6 | 7 | /** 8 | * @param {Function} condition 9 | * @param {Function} left 10 | * @param {Function} right 11 | * @returns {Function} 12 | */ 13 | const branch = (condition, left, right = x => x) => (props = {}) => { 14 | const conditionResult = useMemo(() => condition(props), []) 15 | 16 | return conditionResult ? left(props) : right(props) 17 | } 18 | 19 | export default branch 20 | -------------------------------------------------------------------------------- /src/rehook/catch-render.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React from 'react' 3 | 4 | /** 5 | * @param {Function} component 6 | * @returns {object} 7 | */ 8 | const catchRender = component => { 9 | const newComponent = (props = {}) => { 10 | let result = null 11 | 12 | try { 13 | result = component(props) 14 | } catch (e) { 15 | if (typeof e !== 'object' || React.isValidElement(e)) result = e 16 | else throw e 17 | } 18 | 19 | return result 20 | } 21 | 22 | newComponent.displayName = 23 | // @ts-ignore 24 | component.displayName || component.name || 'Component' 25 | 26 | return newComponent 27 | } 28 | 29 | export default catchRender 30 | -------------------------------------------------------------------------------- /src/rehook/default-props.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * @param {object} defaultProps 4 | * @returns {object} 5 | */ 6 | const defaultProps = defaultProps => (props = {}) => ({ 7 | ...defaultProps, 8 | ...props, 9 | }) 10 | 11 | export default defaultProps 12 | -------------------------------------------------------------------------------- /src/rehook/flatten-prop.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * @param {string|symbol} propName 4 | * @returns {object} 5 | */ 6 | const flattenProp = propName => (props = {}) => ({ 7 | ...props, 8 | ...props[propName], 9 | }) 10 | 11 | export default flattenProp 12 | -------------------------------------------------------------------------------- /src/rehook/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | export { default as branch } from './branch' 3 | export { default as catchRender } from './catch-render' 4 | export { default as defaultProps } from './default-props' 5 | export { default as flattenProp } from './flatten-prop' 6 | export { default as lifecycle } from './lifecycle' 7 | export { default as mapProps } from './map-props' 8 | export { default as namespace } from './namespace' 9 | export { default as pipe } from './pipe' 10 | export { default as renameProp } from './rename-prop' 11 | export { default as renameProps } from './rename-props' 12 | export { default as renderComponent } from './render-component' 13 | export { default as renderNothing } from './render-nothing' 14 | export { default as withHandlers } from './with-handlers' 15 | export { default as withPropsOnChange } from './with-props-on-change' 16 | export { default as withProps } from './with-props' 17 | export { default as withReducer } from './with-reducer' 18 | export { default as withStateHandlers } from './with-state-handlers' 19 | export { default as withState } from './with-state' 20 | -------------------------------------------------------------------------------- /src/rehook/lifecycle.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { useEffect, useRef, useState } from 'react' 3 | 4 | function usePrevious(value) { 5 | const ref = useRef(null) 6 | 7 | useEffect(() => { 8 | ref.current = value 9 | }) 10 | 11 | return ref.current 12 | } 13 | 14 | /** 15 | * @param {object} spec 16 | * @returns {object} 17 | */ 18 | const lifecycle = spec => (props = {}) => { 19 | const [state, setStateRaw] = useState({}) 20 | const setState = update => { 21 | setStateRaw({ 22 | ...state, 23 | ...(typeof update === 'function' ? update(state) : update), 24 | }) 25 | } 26 | 27 | const self = { props, state, setState } 28 | 29 | if (spec.componentDidMount) { 30 | useEffect(() => { 31 | spec.componentDidMount.call(self) 32 | }, []) 33 | } 34 | 35 | if (spec.componentWillUnmount) { 36 | useEffect(() => { 37 | return () => { 38 | spec.componentWillUnmount.call(self) 39 | } 40 | }, []) 41 | } 42 | 43 | if (spec.componentDidUpdate) { 44 | const previousProps = usePrevious(props) 45 | useEffect(() => { 46 | spec.componentDidUpdate.call(self, previousProps, null, null) 47 | }) 48 | } 49 | 50 | return { ...props, ...state } 51 | } 52 | 53 | export default lifecycle 54 | -------------------------------------------------------------------------------- /src/rehook/map-props.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * @param {Function} fn 4 | * @returns {object} 5 | */ 6 | const mapProps = fn => (props = {}) => fn(props) 7 | 8 | export default mapProps 9 | -------------------------------------------------------------------------------- /src/rehook/namespace.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * @param {string|symbol} propName 4 | * @param {Function} enhance 5 | * @returns {object} 6 | */ 7 | const namespace = (propName, enhance) => (props = {}) => ({ 8 | ...props, 9 | [propName]: enhance(props)(), 10 | }) 11 | 12 | export default namespace 13 | -------------------------------------------------------------------------------- /src/rehook/pipe.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * @param {...Function[]} fns 4 | * @returns {object} 5 | */ 6 | const pipe = (...fns) => (props = {}) => fns.reduce((v, f) => f(v), props) 7 | 8 | export default pipe 9 | -------------------------------------------------------------------------------- /src/rehook/rename-prop.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * @param {string|symbol} a 4 | * @param {string|symbol} b 5 | * @returns {object} 6 | */ 7 | const renameProp = (a, b) => ({ [a]: prop, ...props } = {}) => ({ 8 | ...props, 9 | [b]: prop, 10 | }) 11 | 12 | export default renameProp 13 | -------------------------------------------------------------------------------- /src/rehook/rename-props.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * @param {object} propMap 4 | * @returns {object} 5 | */ 6 | const renameProps = propMap => (props = {}) => ({ 7 | // Remove renamed props 8 | ...Object.entries(props) 9 | .filter(([key]) => !(key in propMap)) 10 | .reduce((obj, [k, v]) => Object.assign(obj, { [k]: v }), {}), 11 | // Rename props 12 | ...Object.entries(propMap) 13 | .map(([oldName, newName]) => [newName, props[oldName]]) 14 | .reduce((obj, [k, v]) => Object.assign(obj, { [k]: v }), {}), 15 | }) 16 | 17 | export default renameProps 18 | -------------------------------------------------------------------------------- /src/rehook/render-component.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * @param {any} comp 4 | * @returns {object} 5 | */ 6 | const renderComponent = comp => (props = {}) => { 7 | throw comp(props) 8 | } 9 | 10 | export default renderComponent 11 | -------------------------------------------------------------------------------- /src/rehook/render-nothing.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * @returns {object} 4 | */ 5 | const renderNothing = (/* props */) => { 6 | // eslint-disable-next-line 7 | throw null 8 | } 9 | 10 | export default renderNothing 11 | -------------------------------------------------------------------------------- /src/rehook/test-utils.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React from 'react' 3 | import { mount } from 'enzyme' 4 | 5 | export default function(enhancer, propsIn) { 6 | let propsOut = null 7 | 8 | function Component(props) { 9 | propsOut = enhancer(props) 10 | return null 11 | } 12 | 13 | mount(React.createElement(Component, propsIn)) 14 | 15 | return () => propsOut 16 | } 17 | -------------------------------------------------------------------------------- /src/rehook/with-handlers.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { useMemo } from 'react' 3 | 4 | /** 5 | * 6 | * @param {object} handlers 7 | * @returns {object} 8 | */ 9 | const withHandlers = handlers => (props = {}) => { 10 | const realHandlers = useMemo( 11 | () => (typeof handlers === 'function' ? handlers(props) : handlers), 12 | [] 13 | ) 14 | 15 | const actionTypes = Object.keys(realHandlers) 16 | 17 | const boundHandlers = actionTypes.reduce( 18 | (obj, type) => 19 | Object.assign(obj, { 20 | [type]: (...payload) => realHandlers[type](props)(...payload), 21 | }), 22 | {} 23 | ) 24 | 25 | return { ...props, ...boundHandlers } 26 | } 27 | 28 | export default withHandlers 29 | -------------------------------------------------------------------------------- /src/rehook/with-props-on-change.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { useMemo, useRef, useEffect } from 'react' 3 | 4 | function usePrevious(value) { 5 | const ref = useRef(null) 6 | 7 | useEffect(() => { 8 | ref.current = value 9 | }) 10 | 11 | return ref.current 12 | } 13 | 14 | /** 15 | * 16 | * @param {any} shouldMapOrKeys 17 | * @param {Function} createProps 18 | * @returns {Object} 19 | */ 20 | const withPropsOnChange = (shouldMapOrKeys, createProps) => (props = {}) => { 21 | const previousProps = usePrevious(props) 22 | 23 | const keys = Array.isArray(shouldMapOrKeys) 24 | ? shouldMapOrKeys.map(key => props[key]) 25 | : shouldMapOrKeys(props, previousProps) 26 | ? undefined 27 | : [] 28 | 29 | const mappedProps = useMemo(() => createProps(props), keys) 30 | 31 | return { 32 | ...props, 33 | ...mappedProps, 34 | } 35 | } 36 | 37 | export default withPropsOnChange 38 | -------------------------------------------------------------------------------- /src/rehook/with-props.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * @param {Function|object} fn 4 | */ 5 | const withProps = fn => (props = {}) => ({ 6 | ...props, 7 | ...(typeof fn === 'function' ? fn(props) : fn), 8 | }) 9 | 10 | export default withProps 11 | -------------------------------------------------------------------------------- /src/rehook/with-reducer.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { useReducer, useMemo } from 'react' 3 | 4 | /** 5 | * @param {string|symbol} stateName 6 | * @param {string|symbol} dispatchName 7 | * @param {Function} reducer 8 | * @param {any} initialValue 9 | */ 10 | const withReducer = ( 11 | stateName, 12 | dispatchName, 13 | reducer, 14 | initialValue 15 | ) => props => { 16 | const [state, dispatch] = useReducer( 17 | // @ts-ignore 18 | reducer, 19 | typeof initialValue === 'function' 20 | ? useMemo(() => initialValue(props), []) 21 | : initialValue 22 | ) 23 | 24 | return { ...props, [stateName]: state, [dispatchName]: dispatch } 25 | } 26 | 27 | export default withReducer 28 | -------------------------------------------------------------------------------- /src/rehook/with-state-handlers.js: -------------------------------------------------------------------------------- 1 | import { useReducer, useMemo } from 'react' 2 | 3 | /** 4 | * @param {any} initialValue 5 | * @param {object} handlers 6 | * @returns {object} 7 | */ 8 | const withStateHandlers = (initialValue, handlers) => (props = {}) => { 9 | const actionTypes = Object.keys(handlers) 10 | 11 | const reducer = (state, action) => { 12 | return { 13 | ...state, 14 | ...handlers[action.type](state, props)(...action.payload), 15 | } 16 | } 17 | 18 | const [state, dispatch] = useReducer( 19 | reducer, 20 | typeof initialValue === 'function' 21 | ? useMemo(() => initialValue(props), []) 22 | : initialValue 23 | ) 24 | 25 | const boundHandlers = actionTypes.reduce( 26 | (obj, type) => 27 | Object.assign(obj, { 28 | [type]: (...payload) => { 29 | if (payload !== undefined) dispatch({ type, payload }) 30 | }, 31 | }), 32 | {} 33 | ) 34 | 35 | return { ...props, ...state, ...boundHandlers } 36 | } 37 | 38 | export default withStateHandlers 39 | -------------------------------------------------------------------------------- /src/rehook/with-state.js: -------------------------------------------------------------------------------- 1 | import { useState, useMemo } from 'react' 2 | 3 | /** 4 | * @param {string|symbol} stateName 5 | * @param {string|symbol} stateUpdaterName 6 | * @param {any} initialState 7 | */ 8 | const withState = (stateName, stateUpdaterName, initialState) => ( 9 | props = {} 10 | ) => { 11 | const [state, update] = useState( 12 | typeof initialState === 'function' 13 | ? useMemo(() => initialState(props), []) 14 | : initialState 15 | ) 16 | 17 | return { 18 | ...props, 19 | [stateName]: state, 20 | [stateUpdaterName]: update, 21 | } 22 | } 23 | 24 | export default withState 25 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme' 2 | import Adapter from 'enzyme-adapter-react-16' 3 | 4 | Enzyme.configure({ adapter: new Adapter() }) 5 | --------------------------------------------------------------------------------