refun
v3.1.0
Published
A collection of React Hook-enabled functions that compose harmoniously with each other. Similar to `recompose`, but:
Downloads
285,276
Readme
refun
A collection of React Hook-enabled functions that compose harmoniously with each other. Similar to recompose
, but:
- Uses
Props -> Props
functions instead ofComponent -> 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
Usage example (with TypeScript)
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<TButton>(),
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
}) => (
<button
disabled={isDisabled}
id={id}
onClick={onClick}
onMouseEnter={onPointerEnter}
onMouseLeave={onPointerLeave}
style={style}
>
{children}
</button>
)
)
Several things to note:
component
is used instead of a regularcompose
(from Ramda, Recompose or Redux for example) becausecomponent
accurately propagates the types throughout the entire chainBecause 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.
startWithType
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, thecomponent
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:// Note: This is currently not possible export const Button = component<TButtonProps>( 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 asuseEffect
, are missing. The reason is simply that wrapping them in the composition chain ofrefun
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 themapSafeTimeout
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.
API
- 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
component
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 tellcomponent
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.
pureComponent
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 oldshouldComponentUpdate
, is to be reserved for cases where there is a clear need for optimization.
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<TCounter>(),
mapDefaultProps({
initialCount: 0
}),
mapState('counter', 'setCounter', ({ initialCount }) => initialCount, ['initialCount']),
mapHandlers
)(({ counter, dispatch }) = (
<main>
<AComplexHeader />
<AnExpensiveToComputeSidebar />
<button onClick>
Add
</button>
<p>{counter}</p>
</main>
))
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.
mapContext
Signature:
const mapContext: <T>(context: React.Context<T>) => // ...
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:
import React, { createContext } from 'react'
import { component, mapContext, startWithType } from 'refun'
type TThemeContext = {
darkMode: boolean,
}
const ThemeContext = createContext<TThemeContext>({
darkMode: false
})
type TMessage = {
label: string
}
component(
startWithType<TMessage>(),
mapContext(ThemeContext)
)(({ darkMode, label }) => (
<p style={{ color: darkMode ? 'white' : 'black' }}>
{label}
</p>
))
mapDebouncedHandlerTimeout
Signature:
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
.
import React from 'react'
import { component, mapHandlers, mapDebouncedHandlerTimeout, startWithType } from 'refun'
type TButton = {
onClick: () => void
}
export default component(
startWithType<TButton>(),
mapHandlers({
onClick: () => () => console.log("the handler was now called")
}),
mapDebouncedHandlerTimeout('onClick', 1000)
)(({ onClick }) => (
<div>
<p>
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
</p>
<button onClick={onClick}>Click me</button>
</div>
))
mapDebouncedHandlerFactory
Signature:
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:
export const mapDebouncedHandlerTimeout = mapDebouncedHandlerFactory(setTimeout, clearTimeout)
mapDefaultProps
Signature:
const mapDefaultProps: <P>(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.
import React from 'react'
import { component, mapDefaultProps, startWithType } from 'refun'
type TMessage = {
label?: string
}
export default component(
startWithType<TMessage>(),
mapDefaultProps({
label: 'Hello World!'
})
)(({ label }) => (
<p>{label}</p>
))
mapFocused
Signature: Not callable.
This function sets the isFocused
prop to true
when the onFocus
handler is called and to false
when onBlur
is called.
import React from 'react'
import { component, mapFocused, startWithType, TMapFocused } from 'refun'
type TButton = {
label: string
} & TMapFocused
export default component(
startWithType<TButton>(),
mapFocused
)(
({ isFocused, label, onBlur, onFocus }) => (
<button
onBlur={onBlur}
onFocus={onFocus}
style={{
borderWidth: 2,
borderStyle: "solid",
borderColor: isFocused ? "red" : "grey",
outline: "none"
}}
>
{label}
</button>
)
)
mapHandlers
Signature:
const mapHandlers: <P>(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:
// 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 <input
onChange={handleChange}
value={target.value}
/>
}
…it allows you to do:
import React from 'react'
import { component, mapHandlers, startWithType } from 'refun'
type TInput = {
onChange: (string) => void,
value: string,
}
export default component(
startWithType<TInput>(),
mapHandlers({
onChange: ({ onChange }) => ({ target }) => onChange(target.value),
})
)(
({ onChange, value }) => (
<input
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.
mapHovered
Signature: 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.
import React from 'react'
import { component, mapHovered, startWithType, TMapHovered } from 'refun'
type TButton = {
label: string
} & TMapHovered
export default component(
startWithType<TButton>(),
mapHovered
)(
({ isHovered, label, onPointerLeave, onPointerEnter }) => (
<button
onMouseLeave={onPointerLeave}
onMouseEnter={onPointerEnter}
style={{
borderWidth: 2,
borderStyle: "solid",
borderColor: isHovered ? "red" : "grey",
outline: "none"
}}
>
{label}
</button>
)
)
mapKeyboardFocused
Signature: 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.
import React from 'react'
import { component, mapKeyboardFocused, startWithType, TMapKeyboardFocused } from 'refun'
type TButton = {
label: string
} & TMapKeyboardFocused
export default component(
startWithType<TButton>(),
mapKeyboardFocused
)(
({ isKeyboardFocused, label, onBlur, onFocus, onPressIn, onPressOut }) => (
<button
onBlur={onBlur}
onFocus={onFocus}
onMouseDown={onPressIn}
onMouseUp={onPressOut}
style={{
borderWidth: 2,
borderStyle: "solid",
borderColor: isKeyboardFocused ? "red" : "grey",
outline: "none"
}}
>
{label}
</button>
)
)
mapPressed
Signature: 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.
import React from 'react'
import { component, mapPressed, startWithType, TMapPressed } from 'refun'
type TButton = {
label: string
} & TMapPressed
export default component(
startWithType<TButton>(),
mapPressed
)(
({ isPressed, label, onPressIn, onPressOut }) => (
<button
onMouseDown={onPressIn}
onMouseUp={onPressOut}
style={{
borderWidth: 2,
borderStyle: "solid",
borderColor: isPressed ? "red" : "grey",
outline: "none"
}}
>
{label}
</button>
)
)
mapProps
Signature:
const mapProps: <P, R>(getFn: (props: P) => R) => // ...
This function takes a handler that receives all props and returns new props.
import React from 'react'
import { component, mapProps, startWithType } from 'refun'
type TButton = {
label: string
}
export default component(
startWithType<TButton>(),
mapProps(({ label }) => ({ children: label }))
)(
({ children }) => <button>{children}</button>
)
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 mapWithProps
mapRef
Signature:
const mapRef: <T>(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:
import React from 'react'
import { component, mapRef, onMount, startWithType } from 'refun'
type TButton = {
label: string
}
export default component(
startWithType<TButton>(),
mapRef('buttonElementRef', null),
onMount(({ buttonElementRef }) => {
if (buttonElementRef.current !== null) {
console.log(buttonElementRef)
}
})
)(
({ buttonElementRef, label }) => (
<button ref={buttonElementRef}>
{label}
</button>
)
)
mapSafeRequestAnimationFrame
Signature:
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.
import React from 'react'
import {
component,
mapState,
mapSafeRequestAnimationFrame,
startWithType,
mapHandlers
} from 'refun'
type TLoader = {
initialPosition: number
}
const Loader = component(
startWithType<TLoader>(),
mapState(
"position",
"setPosition",
({ initialPosition }) => initialPosition,
[]
),
mapSafeRequestAnimationFrame("setAnimationFrameCallback")
)(({ position, setPosition, setAnimationFrameCallback }) => {
setAnimationFrameCallback(() => {
setPosition((position + 1) % 80)
})
return (
<div
style={{
width: 100,
height: 8,
border: "1px solid black"
}}
>
<div
style={{
width: 20,
marginLeft: position,
height: 8,
backgroundColor: "black"
}}
/>
</div>
)
})
type TApp = {
loading: boolean
}
export default component(
startWithType<TApp>(),
mapState(
"loading",
"setLoading",
({ loading }) => loading !== undefined ? loading : true,
[]
),
mapHandlers({
onStop: ({ setLoading }) => () => setLoading(false)
})
)(({ loading, onStop }) => (
<div>
{loading && <Loader initialPosition={0} />}
<button onClick={onStop}>Stop loading</button>
</div>
))
mapSafeTimeout
Signature:
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.
import React from 'react'
import {
component,
mapHandlers,
mapState,
mapSafeTimeout,
startWithType
} from 'refun'
type TMessage = {
onClose: () => void
}
const Message = component(
startWithType<TMessage>(),
mapState("autoClose", "setAutoClose", () => false, []),
mapState("secondsRemaining", "setSecondsRemaining", () => 5, []),
mapSafeTimeout("setLocalTimeout")
)(
({
onClose,
secondsRemaining,
setSecondsRemaining,
setLocalTimeout,
autoClose,
setAutoClose
}) => (
<div
style={{
backgroundColor: "#f0f0f0",
padding: 20
}}
>
{autoClose ? (
<React.Fragment>
<p>This message will close in {secondsRemaining} seconds</p>
<button onClick={onClose}>Close immediately</button>
</React.Fragment>
) : (
<React.Fragment>
<p>
This message that will close {secondsRemaining} seconds after you
press OK
</p>
<button
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
</button>
</React.Fragment>
)}
</div>
)
)
type TApp = {
show?: boolean
}
export default component(
startWithType<TApp>(),
mapState("show", "setShow", ({ show }) => true, []),
mapHandlers({
onClose: ({ setShow }) => () => setShow(false),
onShow: ({ setShow }) => () => setShow(true)
})
)(({ show, onClose, onShow }) => (
<div>
{show ? (
<Message onClose={onClose} />
) : (
<button onClick={onShow}>Show message again</button>
)}
</div>
))
[📺 Check out live demo](https://codesandbox.io/s/refun-mapsafetimeout-7mqmh]
mapState
Signature:
const mapState: <P, R>(stateName: string, setterName: string, getValue: (props: P) => R, watchKeys: string[]) => // ...
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
.
import React from 'react'
import { component, mapState, startWithType } from 'refun'
type TCounter = {
counter: number
}
const InternalCounter = component(
startWithType<TCounter>(),
mapState("internalCounter", "setCounter", ({ counter }) => counter, [])
)(({ internalCounter, setCounter }) => (
<div>
<button onClick={() => setCounter(internalCounter + 1)}>
Add to internal counter
</button>
<p>{internalCounter}</p>
</div>
));
const OverridableInternalCounter = component(
startWithType<TCounter>(),
mapState("internalCounter", "setCounter", ({ counter }) => counter, [
"counter"
])
)(({ internalCounter, setCounter }) => (
<div>
<button onClick={() => setCounter(internalCounter + 1)}>
Add to overridable internal counter
</button>
<p>{internalCounter}</p>
</div>
));
export default component(
startWithType<TCounter>(),
mapState("externalCounter", "setExternalCounter", ({ counter }) => counter, [
"counter"
])
)(({ externalCounter, setExternalCounter }) => (
<div>
<button onClick={() => setExternalCounter(externalCounter + 1)}>
Add to external counter
</button>
<p>{externalCounter}</p>
<OverridableInternalCounter counter={externalCounter} />
<InternalCounter counter={externalCounter} />
</div>
))
mapStateRef
Signature:
const mapStateRef: <P, R>(stateName: string, flushName: string, getValue: (props: P) => R, watchKeys: string[]) => // ...
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
.
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<TValue>(),
onMount(({ index, onChange }) => {
onChange(index, Math.random())
})
)(({ value }) => (
<span>{value}</span>
));
type TComp = {
numValues: number
}
export default component(
startWithType<TComp>(),
mapStateRef('valuesRef', 'flushValues', ({ numValue }) => Array(numValues).fill(0), ['numValues']),
mapHandlers({
onChange: ({ valuesRef, flushValues }) => (i, value) => {
valuesRef.current[i] = value
flushValues()
}
})
)(({ valuesRef }) => (
<div>
{valuesRef.current.map((value) => (
<GetValue value={value} />
))}
</div>
))
mapThrottledHandlerTimeout
Signature:
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.
import React from 'react'
import {
component,
mapThrottledHandlerTimeout,
startWithType,
mapHandlers
} from 'refun'
type TSlider = {
onChange: (string) => void
}
export default component(
startWithType<TSlider>(),
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 }) => <input type="range" onChange={onChange} max="1000" />)
mapThrottledHandlerAnimationFrame
Signature:
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 amapDebouncedHandlerTimeout
. The reason is that the behavior of that function would be identical to this one, so it's skipped.
import React from 'react'
import {
component,
mapThrottledHandlerAnimationFrame,
startWithType,
mapHandlers
} from 'refun'
type TSlider = {
onChange: (string) => void
}
export default component(
startWithType<TSlider>(),
mapHandlers({
onChange: () => (value) =>
console.log(`the handler has now been invoked with value: ${value}`)
}),
mapThrottledHandlerAnimationFrame('onChange'),
mapHandlers({
onChange: ({ onChange }) => ({ target: { value } }) => onChange(value)
})
)(({ onChange }) => <input type="range" onChange={onChange} max="1000" />)
mapThrottledHandlerFactory
Signature:
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:
export const mapThrottledHandlerTimeout = mapThrottledHandlerFactory(setTimeout, clearTimeout)
mapWithProps
Signature:
const mapWithProps: <P, R>(getFn: (props: P) => R) => // ...
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.
import React from 'react'
import {
component,
mapFocused,
mapWithProps,
startWithType,
TMapFocused
} from 'refun'
type TButton = {
label: string
} & TMapFocused
export default component(
startWithType<TButton>(),
mapFocused,
mapWithProps(({ isFocused }) => ({
borderColor: isFocused ? "red" : "grey"
}))
)(({ borderColor, label, onBlur, onFocus }) => (
<button
onBlur={onBlur}
onFocus={onFocus}
style={{
borderWidth: 2,
borderStyle: "solid",
borderColor,
outline: "none"
}}
>
{label}
</button>
))
Note that this function just adds props to the component. If you want to replace all of them, you can use mapProps
instead.
mapWithPropsMemo
Signature:
const mapWithPropsMemo: <P, R>(getFn: (props: P) => R, watchKeys: string[]) => // ...
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:
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<TFibonacci>(),
mapWithPropsMemo(
({ position }) => ({
fibonacci: inefficientFibonacci(position)
}),
["position"]
)
)(({ position, fibonacci }) => (
<p>
The Fibonacci numbers in position {position} is <mark>{fibonacci}</mark>
</p>
))
Notice that mapWithPropsMemo
takes two arguments, and that memoization happens for the props that are specified in the second argument, in this case position
.
onChange
Signature:
const onChange: <P>(handler: (props: P) => Promise<void> | void, watchKeys: string[]) => // ...
This function calls the passed in callback when the component is updated, sending the current Props as argument.
For example:
import React from 'react'
import { component, onChange, startWithType } from 'refun'
type TButton = {
label: string,
}
export default component(
startWithType<TButton>(),
onChange(({ label }) => {
console.log('Updated with label', label)
}, ['label'])
)(({ label }) => (
<button>
{label}
</button>
))
onLayout
Signature:
const onLayout: <P>(onLayoutHandler: (props: P) => Promise<void> | void, watchKeys: string[]) => // ...
This function calls the passed in callback when the component is updated, sending the current Props as argument.
For example:
import React from 'react'
import { component, onLayout, startWithType } from 'refun'
type TButton = {
label: string,
}
export default component(
startWithType<TButton>(),
onLayout(({ label }) => {
console.log('Updated with label', label)
}, ['label'])
)(({ label }) => (
<button>
{label}
</button>
))
onUpdate
Signature:
const onUpdate: <P>(onUpdateFn: (props: P) => (() => void) | void, watchKeys: string[]) => // ...
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:
import React from 'react'
import { component, onUpdate, startWithType } from 'refun'
type TButton = {
label: string,
}
export default component(
startWithType<TButton>(),
onUpdate(({ label }) => {
console.log('Updated with label', label)
const handler = () => {}
window.addEventListener('resize', handler)
return () => {
window.removeEventListener('resize', handler)
}
}, ['label'])
)(({ label }) => (
<button>
{label}
</button>
))
onUpdateAsync
Signature:
const onUpdateAsync: <P>(onUpdateFn: (propsRef: React.RefObject<P>) => (props: { cancelOthers: () => void, index: number }) => Generator<Promise<unknown>>, watchKeys: string[]) => // ...
This is onUpdate
variant that properly handles asynchronous behavior.
propsRef
is aprops
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 usefinally
keyword to make some cleanup.
Think offunction*
as the usualasync
function, which usesyield
instead ofawait
.
For example:
import React from 'react'
import { component, onUpdateAsync, startWithType } from 'refun'
type TComponent = {
ID: string,
}
export default component(
startWithType<TComponent>(),
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 }) => (
<div>
{state}
</div>
))
startWithType
Signature:
const startWithType: <P>() => // ...
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.
import React from 'react'
import { component, startWithType } from 'refun'
type TButton = {
label: string,
}
export default component(
startWithType<TButton>(),
)(({ label }) = (
<button>
{label}
</button>
))
Once this is fixed in TypeScript this function will be redundant and it will be possible to pass the generic directly into component
:
import React from 'react'
import { component } from 'refun'
type TButton = {
label: string,
}
// Remember: this is currently *not* supported
export default component<TButton>(
(props) => props
)(({ label }) = (
<button>
{label}
</button>
))
…meanwhile startWithType
is a straightforward workaround.
StoreContextFactory
Signature:
const StoreContextFactory: <S>(store: Redux.Store<S>) => {
mapStoreState: <R>(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:
- Work with Hooks, avoiding higher-order components
- Respect the types all throughout
- 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 theStoreContextFactory
mapStoreDispatch
is a function to be used as part of acomponent
orpureComponent
composition, which will simply adddispatch
to the props, so that the component being wrapped by it can dispatch actions.mapStoreState
is a function to be used as part of acomponent
orpureComponent
composition, which will add props derived from the state. Much likeconnect
from React Redux, it receives amapStateToProps
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 themapStateToProps
function: if none of the listed props have changed, themapStateToProps
will not be ran.
Check the example below for a full use case.
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 }) => (
<div>
<p>Counter: {counter}</p>
</div>
))
const ResetButton = component(
startWithType<{}>(),
mapStoreDispatch('dispatch'),
mapHandlers({
onClick: ({ dispatch }) => () =>
dispatch({
type: "RESET"
})
})
)(({ onClick }) => (
<div>
<button onClick={onClick}>Reset</button>
</div>
))
const IncrementButton = component(
startWithType<{}>(),
mapStoreDispatch('dispatch'),
mapHandlers({
onClick: ({ dispatch }) => () =>
dispatch({
type: "INCREMENT",
payload: 1
})
})
)(({ onClick }) => (
<div>
<button onClick={onClick}>Increment</button>
</div>
))
export default () => (
<div>
<CounterDisplay />
<ResetButton />
<IncrementButton />
</div>
)
Caveats
React Synthetic Events vs debouncing / throttling
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.
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.
import React from 'react'
import {
component,
mapThrottledHandlerTimeout,
startWithType,
mapHandlers
} from 'refun'
type TSlider = {
onChange: (string) => void
}
export default component(
startWithType<TSlider>(),
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 }) => <input type="range" onChange={onChange} max="1000" />)