selectors
Select from the state
Selectors are used to get specific values from the state object.
At it's core, a selector is just a function in the format:
const someValueSelector = (state: Record<string, any>) => state.some.value.from.the.state.object
Automatically created for reducers
Each reducer automatically gets a corresponding selector:
const rootLogic = kea([
// our path is "state.root.logic"
path(['root.logic']),
// add reducer "pieceOfData"
reducers({ pieceOfData: ['default value', {}] }),
])
// the following are added to the logic
rootLogic.reducers.pieceOfData = () => 'default value'
rootLogic.selectors.pieceOfData = (state) => state.root.logic.pieceOfData
Values
Every selector has a corresponding value
. Values are just a shorthand for selectors on the current state
of the store.
Basically:
logic.values.pieceOfData === logic.selectors.pieceOfData(store.getState())
That's it.
Use values anywhere in your listeners or elsewhere, where you need to access the current value of a reducer or selector.
Computed values
Use the selectors
builder to create selectors which combine and cache other selectors:
const logic = kea([
actions({
setMonth: (month) => ({ month }),
setRecords: (records) => ({ records }),
}),
reducers({
month: ['2020-04', { setMonth: (_, { month }) => month }],
records: [[], { setRecords: (_, { records }) => records }],
}),
selectors({
recordsForSelectedMonth: [
(s) => [s.month, s.records],
(month, records) => records.filter((r) => r.month === month),
],
}),
])
The s
is a shorthand for selectors
, which just equals logic.selectors
.
Now you can use recordsForSelectedMonth
directly in your component:
function RecordsForThisMonth() {
const { recordsForSelectedMonth } = useValues(logic)
return (
<ul>
{recordsForSelectedMonth.map((r) => (
<li>{r.name}</li>
))}
</ul>
)
}
Default Memoization
Selectors are recalculated only if the value of their inputs changes. In the example above,
no matter how often your components ask for recordsForSelectedMonth
, they will get
a cached response as long as month
and records
haven't changed since last time.
Custom Memoization
To change the default memoization behaviour, pass memoizeOptions
as the third element to the selector array.
This is passed directly to reselect.
interface MemoizeOptions {
equalityCheck?: EqualityFn
resultEqualityCheck?: EqualityFn
maxSize?: number
}
const logic = kea({
selectors: {
widgetKeys: [
(selectors) => [selectors.widgets],
(widgets) => Object.keys(widgets),
// DefaultMemoizeOptions
{ resultEqualityCheck: deepEqual },
],
},
})
Single source of truth
It is good practice to have as many selectors as possible, each of which sort or filter the raw data stored in your reducers further than the last.
It is bad practice to have listeners do this filtering. For example, you should not write code,
where on the action selectUser(id)
, you run a listener that takes the stored value of users
,
filters it to finds the selected user and then calls another action setUser
to store this value
in the user
reducer.
Such an approach will violate the single source of truth
principle. You will end up with two copies of this one user in your store. If you change something in user
,
should you also change the same data in users
?
Instead, on selectUser(id)
, store selectedUserId
in a reducer. Then create a new selector user
that combines selectedUserId
and users
to dynamically find the selected user.
You'll have a lot less bugs this way. 😉
Selectors that return functions
It's also possible to return a function in a selector. Here's a selector that returns a function that finds an user by id
:
const usersLogic = kea([
loaders({ users: { loadUsers: api.loadUsers } }),
selectors({
findById: [
(selectors) => [selectors.uesrs],
(users) => (id: number) => users.find((user) => user.id === id),
],
}),
])
usersLogic.mount()
const user = usersLogic.values.findById(12)
Props in Selectors
Since selectors
need to be recalculated when their inputs change, there's a twist when
using props
with them.
Take the following buggy code:
const counterLogic = kea([
// ...
selectors(({ props }) => ({
diffFromDefault: [
(selectors) => [selectors.counter],
(counter) => counter - props.defaultCounter, // DO NOT do this!
],
})),
])
The code will work, but only partially.
The problem is that the value of diffFromDefault
will only be updated when counter
changes,
but not when props.defaultCounter
changes.
What if we would also like to update the selector when the props change?
Previously we defined a selector as a function like this:
const selector = (state) => state.path.to.something.counter
That's an incomplete definition. All selectors have a second argument called props
.
const selector = (state, props) => state.path.to.something.counter + props.defaultCounter
To make your new selector update itself when props change, use an inline
selector that picks the right value from props
:
const counterLogic = kea([
// ...
selectors({
diffFromDefault: [
(s) => [s.counter, (_, props) => props.defaultCounter],
(counter, defaultCounter) => counter - defaultCounter,
],
}),
])
Questions & Answers
Ask questions about this page here.