@heliopolis/hooks v0.4.1
Modular hooks
This little utility library provides a few helpful hooks and hook factories for working with function components in React >=16.9.
Actors
State can be managed in a safe way with 2 helpful functions:
useActor(caseReducers, initialState)
createActorHook(stateFactory, caseReducers)
Given both functions require some state to be defined and some case reducers to act upon said state, let's define a function that initializes state and case reducers to act on this state.
interface Person {
name: string
age: number
}
function createPerson(name?: Person['name']): Person {
return {
name: name ?? 'Unnamed person',
age: 40
}
}
const reducers = {
incrementAge(person: Person): Person {
return {
...person,
age: person.age + 1
}
},
rename(person: Person, newName: Person['name']): Person {
return {
...person,
name: newName
}
}
}
Note: the inspiration for the Actor name is due to the similarities between the redux/reducer approach to state management and the Actor model. Namely, state is mutated internally by the actor (in this case, reducer) and changes to said state are effectuated by sending messages to the actor (actions to the reducer). Changes are handled linearly in the order in which they are received.
useActor
Designed for a reducer/redux-like experience, useActor
is a convenience hook around useReducer
that, given case reducers and an initial state, returns a state (like useReducer
) and a set of type-safe dispatchers for each of the case reducers.
// Using `Person`, `createPerson` and `reducers` from the previous example
const initialPerson = createPerson('Sarah Kerrigan')
function PersonCard() {
const [person, dispatchers] = useActor(reducers, initialPerson)
return <div>
<h1>Name: {person.name}</h1>
{/* Text input whose `onEnter` callback is called when pressing ENTER */}
<TextInput onEnter={dispatchers.rename} />
</div>
}
// De-structuring is also supported since the hook returns plain JS objects
function PersonCard() {
const [{ name }, { incrementAge, rename }] = useActor(reducers, initialPerson)
// incrementAge: () => void
// rename: (newName: string) => void
return <div>
<h1>Name: {name}</h1>
<TextInput onEnter={rename}>
<button onClick={incrementAge}>Age by 1 year</button>
</div>
}
createActorHook
While useActor
may be convenient, we can make this interface even more convenient by removing the boilerplate of providing the case reducers
with every call to useActors
. This is where createActorHook
comes in.
Given case reducers and a factory that returns state, createActorHook
will return a hook with the same signature as the provided factory. This hook when used will return a tuple of [state, dispatchers]
, precisely like useActor
above while keeping component logic very readable.
// Using the `Person`, `createPerson` and `reducers` in the first example
const usePerson = createActorHook(createPerson, reducers)
// usePerson: (name?: string) => [Person, Dispatchers]
function PersonCard() {
const [{ name }, { rename }] = usePerson('Zeratul')
return <div>
<h1>Name: {name}</h1>
<TextInput onChange={rename} />
</div>
}
The main benefits we see here are:
- No need to provide
reducers
explicitly with every hook use - No mention of actors in the component which may allows us to stick to the language of our business domain
- Customizable hook arguments signature since it inherits its arguments signature from the provided factory