A collection of React Hook-enabled functions that compose harmoniously with each other. Similar to `recompose`, but:
npm install refunA collection of React Hook-enabled functions that compose harmoniously with each other. Similar to recompose, but:
- Uses Props -> Props functions instead of Component -> Component functions. This is all around better. It means:
- Less nodes in the React tree
- Better optimization options for the JavaScript engine
- Being able to reuse general purpose functions (all of Ramda's object manipulation functions work)
- Propagates TypeScript types through the composition chain without any losses.
- Works entirely with React Hooks instead of class components
``tsx
import React from 'react'
import {
component,
mapDefaultProps,
mapHandlers,
mapHovered,
mapState,
mapWithPropsMemo,
startWithType,
TMapHovered
} from 'refun'
type TButton = {
isDisabled?: boolean
clickCounter?: number
id?: string
} & TMapHovered
export const Button = component(
startWithType
mapDefaultProps({
clickCounter: 0,
isDisabled: false
}),
mapState(
"clickCounter",
"setClickCounter",
({ clickCounter }) => clickCounter,
["clickCounter"]
),
mapHandlers({
onClick: ({ clickCounter, setClickCounter }) => () =>
setClickCounter(clickCounter + 1)
}),
mapHovered,
mapWithPropsMemo(
({ clickCounter, isDisabled, isHovered }) => ({
children: Click count: ${clickCounter},`
style: {
cursor: isDisabled ? "auto" : "pointer",
borderColor: isHovered ? "black" : "grey"
}
}),
["clickCounter", "isDisabled", "isHovered"]
)
)(
({
id,
isDisabled,
style,
onPointerEnter,
onPointerLeave,
onClick,
children
}) => (
disabled={isDisabled}
id={id}
onClick={onClick}
onMouseEnter={onPointerEnter}
onMouseLeave={onPointerLeave}
style={style}
>
{children}
)
)
Several things to note:
- component is used instead of a regular compose (from Ramda, Recompose or Redux for example) because component accurately propagates the types throughout the entire chainstartWithType
- Because of the type propagation, it's convenient to write down all the functions in place in the composition chain. This way, the types will be inferred: otherwise the types will have to be specified manually.
- is necessary because of a TypeScript shortcoming. It is used to make the type that will be received by the first function in the composition chain available. It shouldn't be necessary to do this with a specific function: ideally, the component function itself should be able to propagate the type variable of the generic down to the functions inside, but at the time of this writing (2019-06-26) TypeScript does not support this. If it would, the right way to start the composition chain would be:
`ts`
// Note: This is currently not possible
export const Button = component
mapWithPropsMemo(({ isDisabled }) => ({
...
> Note: you might notice that refun has functions that serve as the equivalent of most of the React Hooks, so it might seem odd that some, such as useEffect, are missing. The reason is simply that wrapping them in the composition chain of refun provides no benefit: useEffect in particular does not result in any prop being added or removed, and there is no implicit state to keep track of, as is the case with the mapSafeTimeout and similar functions.
> It is not the goal of refun to be a replacement of direct usage of React Hooks, rather a way to use them as a clean and decoupled composition chain and with good TypeScript typings, features that are only relevant to certain Hooks.
- component
- pureComponent
- mapContext
- mapDebouncedHandlerTimeout
- mapDebouncedHandlerFactory
- mapDefaultProps
- mapFocused
- mapHandlers
- mapHovered
- mapKeyboardFocused
- mapPressed
- mapProps
- mapRef
- mapSafeRequestAnimationFrame
- mapSafeTimeout
- mapState
- mapStateRef
- mapThrottledHandlerTimeout
- mapThrottledHandlerAnimationFrame
- mapThrottledHandlerFactory
- mapWithProps
- mapWithPropsMemo
- onChange
- onLayout
- onUpdate
- onUpdateAsync
- startWithType
- StoreContextFactory
This function is an analog of compose and it performs simple function composition, with two caveats:
- The value sent into the chain is presumed to be a React Function Component (FC type)component
- will use the output type of one function in the chain as the input type of the next function in the chain, allowing the functions to modify the type along the way. It is not necessary to tell component what the output type at the end of the chain is going to be, since it will be inferred correctly from the functions passed into it.
This function is identical to component except that it memoizes the React element that results from rendering with a certain set of props. The props that are memoized are the _inner_ props, that is, the props that the component will get as the result of the entire composition chain. These are different from the _outer_ props, that are the ones that consumers pass manually into the component.
The purpose of this component is to prevent a re-render from happening when the React tree is known to be the same. It is particularly useful when the React tree is a complex one, since the cost grows fast with the amount of nodes in the tree. Since the memoization is done in the inside of the component, all map functions will be run, making it ideal for components that control their own state.
> Note that this function is meant to be used to avoid pointless re renders of complex trees, which is a concern that should be treated at the high level, in an app for example, rather than in small presentational components. Memoization comes with a cost, and React is already providing optimizations via reconciliation, so the type of optimizations that pureComponent does, similar to the old shouldComponentUpdate, is to be reserved for cases where there is a clear need for optimization.
`tsx
import React from 'react'
import { mapReducer, pureComponent, startWithType } from 'refun'
import AComplexHeader from './AComplexHeader'
import AnExpensiveToComputeSidebar from './AnExpensiveToComputeSidebar'
type TCounter = {
initialCount?: number,
}
export default pureComponent(
startWithType
mapDefaultProps({
initialCount: 0
}),
mapState('counter', 'setCounter', ({ initialCount }) => initialCount, ['initialCount']),
mapHandlers
)(({ counter, dispatch }) = (
{counter}
So to be clear, the component that receives
counter and dispatch as props is the one that is going to be memoized. If your intention is to memoize an expensive computation in a function in the composition chain, such as calculating a value in the mapWithProps, take a look at mapWithPropsMemo instead.pureComponent should only be used in components that receive no children and no complex props, since otherwise the overhead of memoization is not worth it. If the component receives children or complex props (objects / arrays), pureComponent will not provide any benefit, since those are very likely (or guaranteed in the case of children) to be different on every render. pureComponent works by doing a shallow comparison of the current props with the previous props. Shallow comparison means that each prop is compared with hard equality with the previous value of that same props.mapContextSignature:
`ts
const mapContext: (context: React.Context) => // ...
`This function receives a React Context object as created by the
React.createContext function. The assumption is that the value property inside the Context is an object: mapContext will spread that object into the props of the components.For example:
`tsx
import React, { createContext } from 'react'
import { component, mapContext, startWithType } from 'refun'type TThemeContext = {
darkMode: boolean,
}
const ThemeContext = createContext({
darkMode: false
})
type TMessage = {
label: string
}
component(
startWithType(),
mapContext(ThemeContext)
)(({ darkMode, label }) => (
{label}
))
`mapDebouncedHandlerTimeoutSignature:
`ts
const mapDebouncedHandlerTimeout: (handlerName: string, timeout: number) => // ...
`> This function is affected by the React Synthetic Events vs debouncing / throttling issue.
This function allows you to defer the execution of a handler for a grace period (specified in milliseconds) and if the handler gets invoked again during that period, it cancels the current grace period and overrides it with the new call, restarting the time counter.
Why you ask? Imagine for example that there is a button in the UI in which a user might be tempted to repeatedly click to make sure an action happens, but it doing so they will repeatedly trigger an expensive operation that will freeze the application. To avoid this, you could debounce the
onClick handler for some milliseconds and make sure only the last call will be acted upon.The difference between debouncing and throttling (available in
mapThrottledHandlerTimeout) is that successive calls to a debounced handler will restart the timeout each time, while throttled calls will be executed once the initially set timeout it reached, using the latest arguments. Following the FRP convention, this is how debouncing could be represented:`
debouncing in 3 seconds 1s 2s 3s 4s 5s 6s 7s 8s 9s
received x--y--------z------------
ran ---------y--------z------
`Notice how the timeout initially set for
x is simply cancelled and overridden with a new timeout of 3 seconds for y.`tsx
import React from 'react'
import { component, mapHandlers, mapDebouncedHandlerTimeout, startWithType } from 'refun'type TButton = {
onClick: () => void
}
export default component(
startWithType(),
mapHandlers({
onClick: () => () => console.log("the handler was now called")
}),
mapDebouncedHandlerTimeout('onClick', 1000)
)(({ onClick }) => (
Even if you click the button many times in a row (with each click less
than a second after the other), you will only see one log message, at the
end
))
`mapDebouncedHandlerFactorySignature:
`ts
const mapDebouncedHandlerFactory: (setFn: Function, clearFn: Function) => (handlerName: string, ...setFnArgs: any[]) => // ...
`> All the functions create with this one are affected by the React Synthetic Events vs debouncing / throttling issue.
This function is a constructor for debouncers. It is used under the hood to build the
mapDebouncedHandlerTimeout function. If you have a function that creates a deferred effect and a function that will cancel that deferral, you can build your own debouncer.This is how
mapDebouncedHandlerTimeout is defined:`ts
export const mapDebouncedHandlerTimeout = mapDebouncedHandlerFactory(setTimeout, clearTimeout)
`mapDefaultPropsSignature:
`ts
const mapDefaultProps: (defaultProps: P) => // ...
`This function sets some default prop values based on the object that is passed into it. Alternative to using the static
defaultProps component property. The advantage of using it is that the props passed in will be type checked.`tsx
import React from 'react'
import { component, mapDefaultProps, startWithType } from 'refun'type TMessage = {
label?: string
}
export default component(
startWithType(),
mapDefaultProps({
label: 'Hello World!'
})
)(({ label }) => (
{label}
))
`mapFocusedSignature: Not callable.
This function sets the
isFocused prop to true when the onFocus handler is called and to false when onBlur is called.`tsx
import React from 'react'
import { component, mapFocused, startWithType, TMapFocused } from 'refun'type TButton = {
label: string
} & TMapFocused
export default component(
startWithType(),
mapFocused
)(
({ isFocused, label, onBlur, onFocus }) => (
onBlur={onBlur}
onFocus={onFocus}
style={{
borderWidth: 2,
borderStyle: "solid",
borderColor: isFocused ? "red" : "grey",
outline: "none"
}}
>
{label}
)
)
`mapHandlersSignature:
`ts
const mapHandlers: (handlers: { [key: string]: (props: P) => (...args: any[]) => void }) => // ...
`This function allows you to build custom handlers that will be memoized so that they do not cause a diff in the shallow comparison, which would lead to a re render.
So instead of writing:
`tsx
// This will cause the component to re render every time because the handler is unique in every execution
const Input = ({ onChange, value }) => {
const handleChange = ({ target }) => onChange(target.value) return onChange={handleChange}
value={target.value}
/>
}
`…it allows you to do:
`tsx
import React from 'react'
import { component, mapHandlers, startWithType } from 'refun'type TInput = {
onChange: (string) => void,
value: string,
}
export default component(
startWithType(),
mapHandlers({
onChange: ({ onChange }) => ({ target }) => onChange(target.value),
})
)(
({ onChange, value }) => (
onChange={onChange}
value={value}
/>
)
)
`The first argument that each handler will receive is the current props, and the second is the arguments that had been sent to the handler. Notice that the second argument is curried.
mapHoveredSignature: Not callable
This function sets the
isHovered prop to true when the onPointerEnter handler is called and to false when onPointerLeave is called.Note that
onPointerEnter and onPointerLeave are synthetic event names meant to abstract from platform specific hover states. In web, they will be typically mapped:-
onPointerEnter -> onMouseEnter
- onPointerLeave -> onMouseLeave…and each platform will have their own corresponding mapping.
`tsx
import React from 'react'
import { component, mapHovered, startWithType, TMapHovered } from 'refun'type TButton = {
label: string
} & TMapHovered
export default component(
startWithType(),
mapHovered
)(
({ isHovered, label, onPointerLeave, onPointerEnter }) => (
onMouseLeave={onPointerLeave}
onMouseEnter={onPointerEnter}
style={{
borderWidth: 2,
borderStyle: "solid",
borderColor: isHovered ? "red" : "grey",
outline: "none"
}}
>
{label}
)
)
`mapKeyboardFocusedSignature: Not callable.
This function sets the
isKeyboardFocused prop to true when the target gets focused (after onFocus) but only if the focus was acquired via the keyboard interaction, not a pointer event (so if there was no press event before the onFocus). The prop is set to false once onBlur happens.The reason this is useful is that it allows focus states meant for keyboard navigation to be differentiated from regular focus states. When the user is navigating with the keyboard, for example pressing the Tab key, visual highlighting of the focused elements needs to be more prominent to guide the sight into where the active element is. Pointer events will trigger focus as well, but when the interaction was initiated with a pointer it's not necessary for the highlight to be as prominent, since the user is already focused in the pointer position. In order to distinguish these two states and make it possible to style them separately, you can use
mapFocused for the general case and mapKeyboaredFocused for the specific keyboard navigation case.Note that
onPressIn and onPointerLeave are synthetic event names meant to abstract from platform specific hover states. In web, they will be typically mapped:-
onPressIn -> onMouseDown
- onPressOut -> onMouseUp…and each platform will have their own corresponding mapping.
`tsx
import React from 'react'
import { component, mapKeyboardFocused, startWithType, TMapKeyboardFocused } from 'refun'type TButton = {
label: string
} & TMapKeyboardFocused
export default component(
startWithType(),
mapKeyboardFocused
)(
({ isKeyboardFocused, label, onBlur, onFocus, onPressIn, onPressOut }) => (
onBlur={onBlur}
onFocus={onFocus}
onMouseDown={onPressIn}
onMouseUp={onPressOut}
style={{
borderWidth: 2,
borderStyle: "solid",
borderColor: isKeyboardFocused ? "red" : "grey",
outline: "none"
}}
>
{label}
)
)
`mapPressedSignature: Not callable.
This function sets the
isPressed prop to true when the onPressIn handler is called and to false when onPressOut is called.Note that
onPressIn and onPressOut are synthetic event names meant to abstract from platform specific pressed states. In web, they will be typically mapped:-
onPressIn -> onMouseDown
- onPressOut -> onMouseUp…and each platform will have their own corresponding mapping.
`tsx
import React from 'react'
import { component, mapPressed, startWithType, TMapPressed } from 'refun'type TButton = {
label: string
} & TMapPressed
export default component(
startWithType(),
mapPressed
)(
({ isPressed, label, onPressIn, onPressOut }) => (
onMouseDown={onPressIn}
onMouseUp={onPressOut}
style={{
borderWidth: 2,
borderStyle: "solid",
borderColor: isPressed ? "red" : "grey",
outline: "none"
}}
>
{label}
)
)
`mapPropsSignature:
`ts
const mapProps: (getFn: (props: P) => R) => // ...
`This function takes a handler that receives all props and returns new props.
`tsx
import React from 'react'
import { component, mapProps, startWithType } from 'refun'type TButton = {
label: string
}
export default component(
startWithType(),
mapProps(({ label }) => ({ children: label }))
)(
({ children }) =>
)
`Note that
label is no longer available as a prop to the component. If you want to expand the props with extra ones instead of replacing them consider using mapWithPropsmapRefSignature:
`ts
const mapRef: (name: string, initialValue: T) => // ...
`This function provides a way of making a mutable reference available as a prop. It uses the
useRef hook under the hood.Refs are useful to store derived values that do not support shallow comparison, such as functions, or DOM elements.
For example you can use it to capture the
ref to a DOM element and inspect it:`tsx
import React from 'react'
import { component, mapRef, onMount, startWithType } from 'refun'type TButton = {
label: string
}
export default component(
startWithType(),
mapRef('buttonElementRef', null),
onMount(({ buttonElementRef }) => {
if (buttonElementRef.current !== null) {
console.log(buttonElementRef)
}
})
)(
({ buttonElementRef, label }) => (
)
)
`mapSafeRequestAnimationFrameSignature:
`ts
const mapSafeRequestAnimationFrame: (propName: string) => // ...
`This function allows you to set up operations to be executed in the next animation frame that should only be executed while the component is still mounted, and should be canceled if the component is removed from the tree. Callbacks that are not canceled when unmounted are a common cause of React memory leaks.
Why you ask? Animations. Animations can be done in React by continuously updating style parameters of a component, and the cleanest way of updating those is with
requestAnimationFrame. This function allows you to use requestAnimationFrame without worrying about memory leaks.
> As you can check in this 📺 live demo of the issue, simply using
requestAnimationFrame will cause the problems when pressing the "Stop loading" button. In particular, React will log:
> `
> Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
> `
> mapSafeRequestAnimationFrame does the cleanup for you.`tsx
import React from 'react'
import {
component,
mapState,
mapSafeRequestAnimationFrame,
startWithType,
mapHandlers
} from 'refun'type TLoader = {
initialPosition: number
}
const Loader = component(
startWithType(),
mapState(
"position",
"setPosition",
({ initialPosition }) => initialPosition,
[]
),
mapSafeRequestAnimationFrame("setAnimationFrameCallback")
)(({ position, setPosition, setAnimationFrameCallback }) => {
setAnimationFrameCallback(() => {
setPosition((position + 1) % 80)
})
return (
style={{
width: 100,
height: 8,
border: "1px solid black"
}}
>
style={{
width: 20,
marginLeft: position,
height: 8,
backgroundColor: "black"
}}
/>
type TApp = {
loading: boolean
}
export default component(
startWithType
mapState(
"loading",
"setLoading",
({ loading }) => loading !== undefined ? loading : true,
[]
),
mapHandlers({
onStop: ({ setLoading }) => () => setLoading(false)
})
)(({ loading, onStop }) => (
{loading &&
))
`
Signature:
`ts`
const mapSafeTimeout: (propName: string) => // ...
This function allows you to configure time outs that should only be executed while the component is still mounted, and should be canceled if the component is removed from the tree. Timeouts that are not canceled when unmounted are a common cause of React memory leaks.
> As you can check in this 📺 live demo of the issue, simply using setTimeout will cause the problems when pressing the "Close immediately" button before the countdown is completed. In particular, React will log:`
> `
> Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
> mapSafeTimeout
> does the cleanup for you.
`tsx
import React from 'react'
import {
component,
mapHandlers,
mapState,
mapSafeTimeout,
startWithType
} from 'refun'
type TMessage = {
onClose: () => void
}
const Message = component( This message will close in {secondsRemaining} seconds
startWithType
mapState("autoClose", "setAutoClose", () => false, []),
mapState("secondsRemaining", "setSecondsRemaining", () => 5, []),
mapSafeTimeout("setLocalTimeout")
)(
({
onClose,
secondsRemaining,
setSecondsRemaining,
setLocalTimeout,
autoClose,
setAutoClose
}) => (
style={{
backgroundColor: "#f0f0f0",
padding: 20
}}
>
{autoClose ? (
) : (
This message that will close {secondsRemaining} seconds after you
press OK
onClick={() => {
setAutoClose(true)
setLocalTimeout(() => {
console.log("timeout 1000")
setSecondsRemaining(4)
}, 1000)
setLocalTimeout(() => {
console.log("timeout 2000")
setSecondsRemaining(3)
}, 2000)
setLocalTimeout(() => {
console.log("timeout 3000")
setSecondsRemaining(2)
}, 3000)
setLocalTimeout(() => {
console.log("timeout 4000")
setSecondsRemaining(1)
}, 4000)
setLocalTimeout(() => {
console.log("timeout 5000")
onClose()
}, 5000)
}}
>
Ok
)}
type TApp = {
show?: boolean
}
export default component(
startWithType
mapState("show", "setShow", ({ show }) => true, []),
mapHandlers({
onClose: ({ setShow }) => () => setShow(false),
onShow: ({ setShow }) => () => setShow(true)
})
)(({ show, onClose, onShow }) => (
{show ? (
) : (
)}
))
`mapState
Signature:
` (stateName: string, setterName: string, getValue: (props: P" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">📺 Check out live demo => R, watchKeys: string[]) => // ...ts`
const mapState:
This function allows you to set up a stateful prop and a function for updating that prop. It also supports setting the initial value, derived from the props passed into it, and a list of props to watch to reset that value whenever the external prop changes.
Note in the example how the OverridableInternalCounter sets the ["counter"] as the last argument of mapState. This will cause mapState to watch for incoming changes to the counter prop and use them to update the internalCounter prop, accordingly to the function in the third argument ({ counter }) => counter. In the case of InternalCounter, the array in the last argument is empty ([]) and then mapState does not watch for changes, which causes the external prop counter value to be ignored once updated, effectively working as an initial value only for internalCounter.
`tsx
import React from 'react'
import { component, mapState, startWithType } from 'refun'
type TCounter = {
counter: number
}
const InternalCounter = component( {internalCounter}
startWithType
mapState("internalCounter", "setCounter", ({ counter }) => counter, [])
)(({ internalCounter, setCounter }) => (
));
const OverridableInternalCounter = component( {internalCounter}
startWithType
mapState("internalCounter", "setCounter", ({ counter }) => counter, [
"counter"
])
)(({ internalCounter, setCounter }) => (
));
export default component( {externalCounter}
startWithType
mapState("externalCounter", "setExternalCounter", ({ counter }) => counter, [
"counter"
])
)(({ externalCounter, setExternalCounter }) => (
))
`
Signature:
` (stateName: string, flushName: string, getValue: (props: P) => R, watchKeys: string[]) => // ...ts`
const mapStateRef:
This function allows you to set up a stateful prop and a function for updating that prop. It also supports setting the initial value, derived from the props passed into it, and a list of props to watch to reset that value whenever the external prop changes.
Note in the example how the OverridableInternalCounter sets the ["counter"] as the last argument of mapState. This will cause mapState to watch for incoming changes to the counter prop and use them to update the internalCounter prop, accordingly to the function in the third argument ({ counter }) => counter. In the case of InternalCounter, the array in the last argument is empty ([]) and then mapState does not watch for changes, which causes the external prop counter value to be ignored once updated, effectively working as an initial value only for internalCounter.
`tsx
import React from 'react'
import { component, mapStateRef, onMount, startWithType } from 'refun'
type TGetValue = {
index: number,
value: number,
onChange: (i: number, v: number) => void
}
const GetValue = component(
startWithType
onMount(({ index, onChange }) => {
onChange(index, Math.random())
})
)(({ value }) => (
{value}
));
type TComp = {
numValues: number
}
export default component(
startWithType
mapStateRef('valuesRef', 'flushValues', ({ numValue }) => Array(numValues).fill(0), ['numValues']),
mapHandlers({
onChange: ({ valuesRef, flushValues }) => (i, value) => {
valuesRef.current[i] = value
flushValues()
}
})
)(({ valuesRef }) => (
{valuesRef.current.map((value) => (
))}
))
`
Signature:
`ts`
const mapThrottledHandlerTimeout: (handlerName: string, timeout: number) => // ...
> This function is affected by the React Synthetic Events vs debouncing / throttling issue.
This function allows you to defer the execution of a handler for a grace period (specified in milliseconds) and if the handler gets invoked again during that period, it overrides the call with the new invocation, so that when the specified timeout is reached, the last call will be the one executed.
Why you ask? Imagine for example that you have an application that monitors the window size and updates the layout depending on the new size. Window size updates happen very often while the user is performing the resize, and the new layout calculation might be fairly expensive, so the application might become unresponsive. In this case, you could use mapThrottledHandlerTimeout to make sure the resize update only happens every 500 milliseconds, which will not be too noticeable to the user, but will avoid a lot of unnecessary work. Because mapThrottledHandlerTimeout executes the _last_ invocation of the handler, the value that will be captured is the most recent one, which is important since we want to re layout according to the current size, no the one when the resize action started.
The difference between debouncing and throttling (available in mapDebouncedHandlerTimeout) is that successive calls to a debounced handler will restart the timeout each time, while throttled calls will be executed once the initially set timeout it reached, using the last arguments. Following the FRP convention, this is how debouncing could be represented:
`
throttling in 3 seconds
1s 2s 3s 4s 5s 6s 7s 8s 9s
received x--y--------x------------
ran ------y-----------x------
`
Notice how the timeout initially configured for x is respected and the execution happens 3 seconds after the event for x is received, but y is run instead.
`tsx
import React from 'react'
import {
component,
mapThrottledHandlerTimeout,
startWithType,
mapHandlers
} from 'refun'
type TSlider = {
onChange: (string) => void
}
export default component(
startWithType
mapHandlers({
onChange: () => (value) =>
console.log(the handler has now been invoked with value: ${value})`
}),
mapThrottledHandlerTimeout('onChange', 300),
mapHandlers({
onChange: ({ onChange }) => ({ target}) => onChange(target.value)
})
)(({ onChange }) => )
Signature:
`ts`
const mapThrottledHandlerAnimationFrame: (handlerName: string) => // ...
> This function is affected by the React Synthetic Events vs debouncing / throttling issue.
This function allows you to defer the execution of a handler until the next animation frame. If the handler gets invoked again before that animation frame hits, the new invocation will override the previous one, so that when the animation frame starts the last call will be the one executed.
Why you ask? Pretty much the same reasons that are true for mapThrottledHandlerTimeout. Calls that are done between animation frames are wasteful overhead, since the UI will not be updated until the animation frame anyway, so if you have a handler firing continuously, it's a good idea to skip the wasteful ones. This might happen for handlers monitoring scroll or wheel or finger motion actions.
> You might wonder why there is not mapDebouncedHandlerAnimationFrame if there is a mapDebouncedHandlerTimeout. The reason is that the behavior of that function would be identical to this one, so it's skipped.
`tsx
import React from 'react'
import {
component,
mapThrottledHandlerAnimationFrame,
startWithType,
mapHandlers
} from 'refun'
type TSlider = {
onChange: (string) => void
}
export default component(
startWithType
mapHandlers({
onChange: () => (value) =>
console.log(the handler has now been invoked with value: ${value})`
}),
mapThrottledHandlerAnimationFrame('onChange'),
mapHandlers({
onChange: ({ onChange }) => ({ target: { value } }) => onChange(value)
})
)(({ onChange }) => )
Signature:
`ts`
const mapThrottledHandlerFactory: (setFn: Function, clearFn: Function) => (handlerName: string, ...setFnArgs: any[]) => // ...
> All the functions created with this one is affected by the React Synthetic Events vs debouncing / throttling issue.
This function is a constructor for throttlers. It is used under the hood to build the mapThrottledHandlerTimeout and mapThrottledHandlerAnimationFrame functions. If you have a function that creates a deferred effect and a function that will cancel that deferral, you can build your own throttler.
This is how mapThrottledHandlerTimeout is defined:
`ts`
export const mapThrottledHandlerTimeout = mapThrottledHandlerFactory(setTimeout, clearTimeout)
Signature:
` (getFn: (props: P) => R) => // ...ts`
const mapWithProps:
This function allows you to expand the props passed in to a component with more props derived from them. It is typically used to precalculate values that are to be used in the component, to minimize the amount of logic needed to do in the render.
If the returned props have the same name as incoming props, they will override the incoming props.
`tsx
import React from 'react'
import {
component,
mapFocused,
mapWithProps,
startWithType,
TMapFocused
} from 'refun'
type TButton = {
label: string
} & TMapFocused
export default component(
startWithType
mapFocused,
mapWithProps(({ isFocused }) => ({
borderColor: isFocused ? "red" : "grey"
}))
)(({ borderColor, label, onBlur, onFocus }) => (
onBlur={onBlur}
onFocus={onFocus}
style={{
borderWidth: 2,
borderStyle: "solid",
borderColor,
outline: "none"
}}
>
{label}
))
`
Note that this function just adds props to the component. If you want to replace all of them, you can use mapProps instead.
Signature:
` (getFn: (props: P) => R, watchKeys: string[]) => // ...ts`
const mapWithPropsMemo:
This function does the same as mapWithProps and it memoizes the result for the props specified in the second parameter.
An example use case in which this can prove useful is if you were to be calculating the Fibonacci number of an input, which is known to be expensive for large numbers:
`tsx
import React from 'react'
import { component, mapWithPropsMemo, startWithType } from 'refun'
const inefficientFibonacci = position =>
position < 2
? position
: inefficientFibonacci(position - 1) + inefficientFibonacci(position - 2)
type TFibonacci = {
position: number
}
export default component(
startWithType
mapWithPropsMemo(
({ position }) => ({
fibonacci: inefficientFibonacci(position)
}),
["position"]
)
)(({ position, fibonacci }) => (
The Fibonacci numbers in position {position} is {fibonacci}
))
`
Notice that mapWithPropsMemo takes two arguments, and that memoization happens for the props that are specified in the second argument, in this case position.
Signature:
` (handler: (props: P) => Promisets`
const onChange:
This function calls the passed in callback when the component is updated, sending the current Props as argument.
For example:
`tsx
import React from 'react'
import { component, onChange, startWithType } from 'refun'
type TButton = {
label: string,
}
export default component(
startWithType
onChange(({ label }) => {
console.log('Updated with label', label)
}, ['label'])
)(({ label }) => (
))
`
Signature:
` (onLayoutHandler: (props: P) => Promisets`
const onLayout:
This function calls the passed in callback when the component is updated, sending the current Props as argument.
For example:
`tsx
import React from 'react'
import { component, onLayout, startWithType } from 'refun'
type TButton = {
label: string,
}
export default component(
startWithType
onLayout(({ label }) => {
console.log('Updated with label', label)
}, ['label'])
)(({ label }) => (
))
`
Signature:
` (onUpdateFn: (props: P) => (() => void) | void, watchKeys: string[]) => // ...ts`
const onUpdate:
This function calls the passed in callback when the component is mounted and updated, sending the current Props as an argument.
> Pass certain propery keys as an array, to invoke handler only when such props has been updated.
> It is possible to return some unsubscribe function from onUpdateHandler. It will be called before next onUpdate.
> If watch array is empty onUpdateHandler will be called only for component mount and unmount cases.
For example:
`tsx
import React from 'react'
import { component, onUpdate, startWithType } from 'refun'
type TButton = {
label: string,
}
export default component(
startWithType
onUpdate(({ label }) => {
console.log('Updated with label', label)
const handler = () => {}
window.addEventListener('resize', handler)
return () => {
window.removeEventListener('resize', handler)
}
}, ['label'])
)(({ label }) => (
))
`
Signature:
` (onUpdateFn: (propsRef: React.RefObject ) => (props: { cancelOthers: () => void, index: number }) => Generatorts`
const onUpdateAsync:
This is onUpdate variant that properly handles asynchronous behavior.propsRef
> is a props reference, so current props are always available even after long waiting for some promises. cancelOthers
> provides a way to stop all concurrently running routines, if necessary. Canceled routine can use finally keyword to make some cleanup. function*
> Think of as the usual async function, which uses yield instead of await.
For example:
`tsx
import React from 'react'
import { component, onUpdateAsync, startWithType } from 'refun'
type TComponent = {
ID: string,
}
export default component(
startWithType
mapState('state', 'setState', () => null, [])
onUpdateAsync((propsRef) => function* ({ cancelOthers, index }) {
try {
// cancel all concurrently running routines
cancelOthers()
// use 'yield' instead of 'await'
const res = yield fetch(http://url.com?id=${props.current.ID})
const json = yield res.json()
props.current.setState(json)
} finally {
cleanup()
}
}, ['ID'])
)(({ state }) => (
startWithTypeSignature:
`ts
const startWithType: () => // ...
`This function is simply a way of setting up the initial type in the
component composition chain, since TypeScript does not currently support doing that in the composition function itself (component in this case, but would be compose in Redux, Ramda, etc).It's purpose is entirely for types, and in runtime it's a no-op.
`tsx
import React from 'react'
import { component, startWithType } from 'refun'type TButton = {
label: string,
}
export default component(
startWithType(),
)(({ label }) = (
))
`Once this is fixed in TypeScript this function will be redundant and it will be possible to pass the generic directly into
component:`tsx
import React from 'react'
import { component } from 'refun'type TButton = {
label: string,
}
// Remember: this is currently not supported
export default component(
(props) => props
)(({ label }) = (
))
`…meanwhile
startWithType is a straightforward workaround.StoreContextFactorySignature:
`ts
const StoreContextFactory: (store: Redux.Store) => {
mapStoreState: (mapStateToProps: (state: S) => R, stateKeysToWatch: string[]) => // ...
mapStoreDispatch: (dispatchPropName: string) => // ...
}
`This function is a way of working with Redux stores together with React Hooks. It is an alternative to React Redux, with these goals:
1. Work with Hooks, avoiding higher-order components
2. Respect the types all throughout
3. Match the level of optimization of React Redux.
The way this function works is that it receives a Redux Store object, and returns a component and two functions:
-
StoreProvider is a component that provides the React Context already loaded with the store that was passed in to the StoreContextFactory
- mapStoreDispatch is a function to be used as part of a component or pureComponent composition, which will simply add dispatch to the props, so that the component being wrapped by it can dispatch actions.
- mapStoreState is a function to be used as part of a component or pureComponent composition, which will add props derived from the state. Much like connect from React Redux, it receives a mapStateToProps function that will be called with the full state and which return value will be spread over the component props. mapStoreState takes as a second argument an array of the names of the props to watch in order to run the mapStateToProps function: if none of the listed props have changed, the mapStateToProps will not be ran.Check the example below for a full use case.
`tsx
import React from 'react'
import { createStore } from "redux"
import { component, pureComponent, StoreContextFactory, mapHandlers, startWithType } from 'refun'type TState = {
counter: number
}
type TAction = { type: "INCREMENT" payload: number } | { type: "RESET" }
const reducer = (state: TState, action: TAction): TState => {
switch (action.type) {
case "INCREMENT":
return {
...state,
counter: state.counter + action.payload
}
case "RESET":
return {
...state,
counter: 0
}
default:
return state
}
}
const initialState = {
counter: 7
}
const store = createStore(reducer, initialState)
const { mapStoreState, mapStoreDispatch } = StoreContextFactory(store)
const CounterDisplay = pureComponent(
startWithType<{}>(),
mapStoreState(
({ counter }) => ({
counter
}),
["counter"]
)
)(({ counter }) => (
Counter: {counter}
))const ResetButton = component(
startWithType<{}>(),
mapStoreDispatch('dispatch'),
mapHandlers({
onClick: ({ dispatch }) => () =>
dispatch({
type: "RESET"
})
})
)(({ onClick }) => (
))const IncrementButton = component(
startWithType<{}>(),
mapStoreDispatch('dispatch'),
mapHandlers({
onClick: ({ dispatch }) => () =>
dispatch({
type: "INCREMENT",
payload: 1
})
})
)(({ onClick }) => (
))export default () => (
)
`Caveats
$3
The
mapDebounced and mapThrottled family of functions do not accept React Synthetic Events. As you can see in the examples below, some specific properties of the event (value in that case) need to be extracted from the original Synthetic Event in order for them to work.This is necessary because these two function families store the arguments passed to the handlers for delayed use. If that argument is a Synthetic Event, it will be stored to be reused, but React forbids this, because for performance reasons React reuses the references of Synthetic Events and mutates them.
> If you try the example below without the
mapHandlers, you will get:
>
> `
> Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property target on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist(). See https://fb.me/react-event-pooling for more information.
> `
>
> 📺 Check out live demo
If you are going to use information coming from the Synthetic Event, consider extracting the information you care about using
mapHandlers, which will then let React discard the rest of the Event object.If you are not going to use _any_ information coming from the Event—such as in the example for
mapDebouncedHandlerTimeout—then you will not be affected by this issue.`tsx
import React from 'react'
import {
component,
mapThrottledHandlerTimeout,
startWithType,
mapHandlers
} from 'refun'type TSlider = {
onChange: (string) => void
}
export default component(
startWithType(),
mapHandlers({
onChange: () => (value) =>
console.log(
the handler has now been invoked with value: ${value})
}),
mapThrottledHandlerTimeout("onChange", 300),
mapHandlers({
onChange: ({ onChange }) => ({ target: { value } }) => onChange(value)
})
)(({ onChange }) => )
``