MutableSelectable
MutableSelectable implements the SelectableInterface and provides some additional helper functions like .flush
Usage
Creating a store
Creating a store is as simple as creating a new instance of MutableSelectable
const store = new MutableSelectable({
username: "",
password: "",
})
Updating state
The only way to update state is by calling .set
on the store
store.set((state) => {
state.username = "John"
state.password = "password123"
})
Subscribing to changes
You can subscribe to changes on the store with .subscribe
.
This will run the listener immediately and then every time .set
is called.
const unsubscribe = store.subscribe(() => {
console.log(store.state.username)
})
Subscribing to a slice of state
If you only want your listener to run when a subset of state changes you can use the .select
method to "select" which part of state you are interested in.
For example the listener in the snippet below would run once immediately and then whenever the "username" value changes
const listener = (username) => console.log(username)
const unsub = store.select((state) => state.username, listener)
// we selected the username above so this will trigger the listener
store.set((state) => {
state.username = "example_password"
})
// the listener is not called because we haven't selected the password
store.set((state) => {
state.password = "example_password"
})
Importantly the listener also receieves
Computing values from state
You can compute a value from any part of the store in the selector function. The listener will then only rerun when the computed value changes
const selectHasMatchingPassword = (state) => state.username === state.password
const matchingPasswordListener = (isMatching) => {
if (isMatching) {
console.log("Your password can't be the same as your username!")
}
}
const unsubscribe = store.select(
selectHasMatchingPassword,
matchingPasswordListener
)
Custom equality functions
By default the equality function used is Object.is
.
This means that if you return a new object or array in the selector the listener will run on every change.
To avoid your listener running on every change you can supply your own equality function as the third argument to .select
const unsubscribe = store.select(
(state) => [state.username, state.password],
([username, password]) => {
console.log("Username or password changed")
},
// returns true if an array has equal length and every item is strictly equal
shallowEqualArray
)
Subscribing to multiple parts of a store
As alluded to above you can return an object or array in the selector function to subscribe to multiple parts of state.
import { shallowEqual, shallowEqualArray, deepEqual, strictEqual } from "@selkt/core"
// updates when either username or password changes
store.select(
(state) => [state.username, state.password]
([username, password]) => {
console.log('Username or password changed')
},
shallowEqualArray
)
// updates when either username or password changes
store.select(
state => ({ username: state.username, password: state.password }),
({ username, password }) => {
console.log('Username or password changed')
},
shallowEqual
)
// updates when any value in state changes
store.select(
state => state,
state => {
console.log('Username or password changed')
},
deepEqual
)
Forcing a listener to re-run
If you need to force a listener to re-run you can call the .update
method on the returned unsubscribe function
Calling .update
is a last resort and if you find a situation where you need
to call it you should probably try and refactor your code to avoid it.
const listener = store.select(
(state) => state.username,
(username) => {
// runs once on mount and again when listener.update() is called
console.log(username)
}
)
listener.update()
Delay updating listeners while making multiple changes to state
If you need to make multiple changes to state and only want the listeners to run once you can use the .flush
method.
const store = new MutableSelectable({
username: "",
password: "",
})
store.select(
(state) => state.username,
(username) => {
console.log(username)
}
)
// logs "" on mount
store.flush(() => {
store.set((state) => {
state.username = "John"
})
store.set((state) => {
state.username = "Johnny"
})
})
// logs "Johnny" after the .flush callback has finished executing
Because both calls to .set
happen inside the callback above it would only trigger a single listener update
Unsubscribing from a store
To unsubscribe from a store simply call the returned unsubscribe function.
In the example below the listeners would be called once on mount but will not be called again when .set
is called
const listener1 = store.subscribe(() => {
console.log(store.state.username)
})
listener1()
// or
const listener2 = store.select(
(state) => state.username,
(username) => {
console.log(username)
}
)
listener2()
store.set((state) => {
state.username = "John"
state.password = "password123"
})
A note on mutability
MutableSelectable is unconcerned with the mutability of objects. So if you select an object from state and mutate one if it's properties the listener selecting that object will not run as the object identity has not changed.
To get around this you can either avoid mutating in your .set
calls:
const store = new MutableSelectable({
user: {
username: "",
password: "",
},
})
store.select(
(state) => state.user,
(user) => {
console.log(user)
}
)
// This would not trigger the listener as the object identity has not changed
store.set((state) => {
state.user.username = "John"
})
// This would trigger the listener as the object identity has changed
store.set((state) => {
state.user = {
...state.user,
username: "John",
}
})
You could also use a third party library like immer (opens in a new tab) to make your .set
calls immutable by default.
This is essentially the same as using the Selectable
class, except Selectable has a few performance optimisations that make it faster if the .set
call produces no change.
import produce from "immer"
store.set(
produce((state) => {
state.user = produce(state.user, (draft) => {
draft.username = "John"
})
})
)
another option is to provide a custom equality function to your selector
const store = new MutableSelectable({
user: {
username: "",
password: "",
},
})
store.select(
(store) => store.user,
(user) => {
console.log(user)
},
shallowEqual
)
// This would trigger the listener as the shallowEqual function would return false
store.set((state) => {
state.user.username = "John"
})
Playground
import { MutableSelectable } from "@selkt/core" console.clear() const myStore = new MutableSelectable({ username: "", password: "", }) const listener = (username) => console.log(username) myStore.select( (state) => state.username, listener // runs immediately with "" ) myStore.set((state) => { state.username = "John" }) // listener reruns with "John"
API
export class MutableSelectable<StateType>
MutableSelectable<StateType>.subscribe
Subscribes to a store. Returns an unsubscribe function. The returned function also has an .update
method that can be used to force the listener to rerun
subscribe(
listener: (state: StateType) => void,
): (Unsubscribe & { update: () => void })
MutableSelectable<StateType>.select
Subscibes to changes on a slice of state. The listener runs immediately and then whenever the SelectedSlice
changes.
The listener also recieves the previous value of SelectedSlice
as the second argument so you can compare the current and previous values in your listener
select<SelectedSlice>(
selector: (state: StateType) => SelectedSlice,
listener: (state: SelectedSlice, prevState?: SelectedSlice | undefined) => void,
equalityFn?: (a: SelectedSlice, b: SelectedSlice) => boolean
): (Unsubscribe & { update: () => void })
MutableSelectable<StateType>.set
This is the only way to change the state of a store. It accepts a single callback which is called with the current state. The callback can return a new state or mutate the current state.
set(
updater: (currentState: StateType) => void | StateType
) => void
MutableSelectable<StateType>.state
The current state of the store.
state: StateType
MutableSelectable<StateType>.version
The current version of the store. This is incremented every time the state is changed.
version: number
MutableSelectable<StateType>.flush
Delays calling listeners until the callback has finished executing. This is useful when you want to make multiple changes to state and only want to the listeners to update once.
flush(
callback: () => void
): void