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 |
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 |
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 |