Home

Awesome

Kinfolk

Atoms and selectors for React.

Why?

Kinfolk is a more powerful context for React that allows:

Kinfolk uses a series of simple techniques to ensure optimal re-renders. The granularity of atoms and selectors can be seen as an alternative to signals, because of how it allows to create granular fine tuned re-renders.

The benefits of Kinfolk over React Context are:

Optimal re-renders

Kinfolk has several layers that makes it performant:

Example

import React from 'react'
import { Provider, atom, selector, useSelector, useSetter } from 'kinfolk'

const checkout = atom({
  order: [{ food: 'pizza' }, { food: 'burger' }],
  card: null,
})
const count = selector(() => checkout().order.length)

export function App() {
  return (
    <Provider>
      <Header />
      <OrderDetails />
      <CardDetails />
    </Provider>
  )
}

function Header() {
  const c = useSelector(count)
  return <div>Items in basket: {c}</div>
}

function OrderDetails() {
  const order = useSelector(() => checkout().order, [])
  return (
    <div>
      {order.map((item) => (
        <div key={item.food}>{item.food}</div>
      ))}
    </div>
  )
}

function CardDetails() {
  const card = useSelector(() => checkout().card, [])
  const setCheckoutState = useSetter(checkout)
  return (
    <input
      type='text'
      value={card}
      onChange={(e) => {
        setCheckoutState((c) => ({ ...c, card: e.target.value }))
      }}
    />
  )
}

API

Provider

Accepts an optional store prop. See the Advanced API below.

<Provider />
<Provider store={createStore()} />

atom(initialState, { label })

Create an atom. The label option is only used when viewing the contents of the store using store.debug(). Atom values are persisted throughout the lifetime of the application, use a setter if you want to clear the value of an atom.

const counter = atom(0)
const users = atom([], { label: 'users' })

selector(selectorFn, { label, equal, persist = true })

Create a selector that derives state from other atoms and selectors. Selectors cache their values to avoid recomputing the values upon each re-render. If the upstream dependencies did not change, the selector will return the memoised calculation. If all components that previously dependended on a selector were unmounted, the selector will persist it's memoisation cache. Set persist option to false to clear out the memoisation cache as soon as a selector is no longer used.

const double = selector(() => counter() * 2)
const enhancedUsers = selector(() => users().map(withFullName), {
  label: 'enhancedUsers',
  persist: false,
})

Selectors can also take arguments and compute multiple memoised values (aka selector families).

const multiple = selector((n) => counter() * n)
const calc = useSelector(() => multiple(1) + multiple(2) + multiple(3))

useSelector(selectorFn, dependencies, { label, equal })

Read an atom or selector inside a React component and subscribe to the value. Only if the value computed by the selectorFn has changed will the component re-render. Note: when comparing the value the default equal function is used but can by customised. The default equal function will not only compare strict object equality, but will also compare objects shallowly (every key/value in both objects equals) and compare arrays shallowly (every item in the array in both objects equals).

const count = useSelector(counter, [])
const doubleCount = useSelector(double, [])
const tripleCount = useSelector(() => counter() * 3, [], {
  label: 'tripleCount',
})

useSetter(atomRef)

Update an atom with a setter that takes a value or an update function.

const set = useSetter(counter)
set(1)
set((c) => c + 1)

useReducer(atomRef, reducer)

Update an atom with a reducer.

const calculator = (state, action) => {
  if (action === 'inc') return state + 1
  if (action === 'dec') return state + 1
  return state
}
const dispatch = useReducer(counter, calculator)
dispatch('inc')
dispatch('dec')

Advanced API

createStore()

By default a store is automatically created in the Provider. Creating a store and passing it in explicitly allows to:

const store = createStore()
<Provider store={store} />
const val = store.get(counter)
store.set(counter, val + 1)
store.set(counter, c => c + 1)
store.debug() // returns an object with all values of all atoms and selectors

createContext()

Create an isolated context with it's own Provider, for use within libraries. Note that it is still possible to share the store if the same store instance is provided to application and library Providers.

const { atom, selector, Provider, useSelector, useSetter, useReducer } =
  createContext()

Alternatives

Caution

While the ideas for this library are old and sourced from a lot of inspiration in the React ecosystem, given the library is new at this point and given the certain amount of complexity in semantics of React's concurrent rendering and the useSyncExternalStore hook below are some things to be aware of.