JotaiJotai

状態
Primitive and flexible state management for React

Functional programming and Jotai

Unexpected similarities

If you look at getter functions long enough, you may see a striking resemblence to a certain JavaScript language feature.

const nameAtom = atom('Visitor')
const countAtom = atom(1)
const greetingAtom = atom((get) => {
const name = get(nameAtom)
const count = get(countAtom)
return (
<div>
Hello, {name}! You have visited this page {count} times.
</div>
)
})

Compare that code with asyncawait:

const namePromise = Promise.resolve('Visitor')
const countPromise = Promise.resolve(1)
const greetingPromise = (async function () {
const name = await namePromise
const count = await countPromise
return (
<div>
Hello, {name}! You have visited this page {count} times.
</div>
)
})()

This similarity is no coincidence. Both atoms and promises are Monads†, a concept from functional programming. The syntax used in both greetingAtom and greetingPromise is known as do-notation, a syntax sugar for the plainer monad interface.

About monads

The monad interface is responsible for the fluidity of the atom and promise interfaces. The monad interface allowed us to define greetingAtom in terms of nameAtom and countAtom, and allowed us to define greetingPromise in terms of namePromise and countPromise.

If you're curious, a structure (like Atom or Promise) is a monad if you can implement the following functions for it. A fun exercise is trying to implement of, map and join for Arrays.

type SomeMonad<T> = /* for example... */ Array<T>
declare function of<T>(plainValue: T): SomeMonad<T>
declare function map<T, V>(
anInstance: SomeMonad<T>,
transformContents: (contents: T) => V,
): SomeMonad<V>
declare function join<T>(nestedInstances: SomeMonad<SomeMonad<T>>): SomeMonad<T>

The shared heritage of Promises and Atoms means many patterns and best-practices can be reused between them. Let's take a look at one.

Sequencing

When talking about callback hell, we often mention the boilerplate, the indentation and the easy-to-miss mistakes. However, plumbing a single async operation into another single async operation was not the end of the callback struggle. What if we made four network calls and needed to wait for them all? A snippet like this was common:

const nPending = 4
const results: string[]
function callback(err, data) {
if (err) throw err
results.push(data)
if (results.length === nPending) {
// do something with results...
}
}

But what if the results have different types? and the order was important? Well, we'd have a lot more frustrating work to do! This logic would be duplicated at each usage, and would be easy to mess up. Since ES6, we simply call Promise.all:

declare function promiseAll<T>(promises: Array<Promise<T>>): Promise<Array<T>>

Promise.all "rearranges" Array and Promise. It turns out this concept, sequencing, can be implemented for all monad–Traversable pairs. Many kinds of collections are Traversables, including Arrays. For example, this is a case of sequencing specialized for atoms and arrays:

function sequenceAtomArray<T>(atoms: Array<Atom<T>>): Atom<Array<T>> {
return atom((get) => atoms.map(get))
}

Culmination

Monads have been an interest to mathematicians for 60 years, and to programmers for 40. There are many resources out there on patterns for monads. Take a look at them! Here are a select few:

Learning a neat trick on using promises may well translate to atoms, as Promise.all and sequenceAtomArray did. Monads are not magic, just unusually useful, and a tool worth knowing.


Notes

[†] The ES6 Promise is not a completely valid monad because it cannot nest other Promises, e.g. Promise<Promise<number>> is semantically equivalent to Promise<number>. This is why Promises only have a .then, and not both a .map and .flatMap. ES6 Promises are probably more properly described as "monadic" rather than as monads.

Unlike ES6 Promises, the ES6 Array is a completely lawful monad.