reducers
Reducers store values
Reducers store your data and change it in response to actions. They are based on the reducer concept from Redux.
Here's an example of a basic counter:
import { kea, reducers } from 'kea'
const logic = kea([
actions({
increment: (amount) => ({ amount }),
setCounter: (counter) => ({ counter }),
reset: true,
}),
reducers({
counter: [
0,
{
increment: (state, { amount }) => state + amount,
setCounter: (_, { counter }) => counter,
reset: () => 0,
},
],
}),
])
When defining reducers in kea you write pure functions
that take two arguments: the current state
of the reducer and the payload
of the action that was
just dispatched. You then combine the two and return a new state.
In the example above we have three actions: increment
, setCounter
and reset
. We also have a
reducer called counter
that will update its value in response to those actions.
It will be 0
by default.
No direct access
Please note that the only way to change the value of counter
is by dispatching actions and reacting
to them. You can't just jump in there and call reducers.counter += 1
somewhere. All data manipulation
must always go through an action.
While this may feel limiting at first, there is method to madness here. Pushing all state changes through actions makes for stable and predictable apps that run better, crash less often and even do your laundry. We all want that, don't we?
Being explicit with the relationships between actions and reducers makes for very composable code.
Suppose our logic also stores a name
. Can we make the reset
action clear both pieces of data? Naturally:
import { kea, actions, reducers } from 'kea'
const logic = kea([
actions({
setName: (name) => ({ name }),
increment: (amount) => ({ amount }),
setCounter: (counter) => ({ counter }),
reset: true,
}),
reducers({
counter: [
0,
{
increment: (state, { amount }) => state + amount,
setCounter: (_, { counter }) => counter,
reset: () => 0,
},
],
name: [
'',
{
setName: (_, { name }) => name,
reset: () => '',
},
],
}),
])
It's starting to look like a neatly defined state graph of sorts... 🤔
You can have any reducer depend on any action, even ones defined in other logic files.
Anti-pattern warning
Kea's actions and reducers are intended to mix together freely within a logic.
If you find yourself constantly writing code that has actions such as setName
, setPrice
,
setLoading
and setError
with corresponding reducers name
, price
, loading
and error
and a singular 1:1 mapping between them, you're probably following an anti-pattern and doing something wrong.
You'll see an example of this anti-pattern in the section about listeners.
Pure functions
Just like actions, reducers are also pure functions.
That means no matter how many times you call a reducer with the same input (same state
and payload
),
it should always give the same output.
More importantly, reducers must never modify their inputs. In practice this means that
instead of adding an element to an array via state.push(newThing)
, you instead create and return a new
array that contains this new element with [...state, newThing]
.
For example, here's todo list that stores strings in an array:
import { kea, actions, reducers } from 'kea'
const todosLogic = kea([
actions({
addTodo: (todo) => ({ todo }),
removeTodo: (index) => ({ index }),
updateTodo: (index, todo) => ({ index, todo }),
}),
reducers({
// defaults to [], an empty array
todos: [
[],
{
addTodo: (state, { todo }) => {
// make a new array and add `todo` at the end
return [...state, todo]
},
removeTodo: (state, { index }) => {
// filter out the `todo` at the given `index`
return state.filter((todo, i) => i !== index)
},
updateTodo: (state, { index, todo }) => {
// swap out the `todo` in the array at the given `index`
return state.map((t, i) => (i === index ? todo : t))
},
},
],
}),
])
This may seem weird and slow at first, but writing immutable code like this greatly improves performance in React, by making it obvious what has changed and what hasn't. If you really do want to write mutable code, feel free to wrap your reducers with immer.
No side effects
In a reducer you can not dispatch actions, nor run any asynchronous code. For this you use listeners.
Using in React
To use the values stored in reducers in React, use the useValues
hook:
import React from 'react'
import { useValues } from 'kea'
function Todos() {
const { todos } = useValues(todosLogic)
return (
<ul>
{todos.map((todo) => (
<li>{todo}</li>
))}
</ul>
)
}
Questions & Answers
Ask questions about this page here.