I have an interesting proposal for the derived state that is flexible and simple for the user both in JS and TS and supports nested derived state.
Derived State
First, the derived state is like this, inspired by Overmind:
const state = {
users: [],
numberOfUsers: ({ state }) => state.users.length
}
// later on...
state.users.push({ name: "Jon", surname: "Snow" });
state.numberOfUsers; // === 1
They get the state and return the value.
Types are like this:
type State = {
users: User[],
numberOfUsers: DerivedState<State, number>
}
- First argument: the state.
- Third argument: the returned type.
I agree with the Overmind designer (Christian Alfoni) that MobxStateTree references are not really needed because you can store references in the state and derive the real value in other property like this:
const state = {
users: {},
selectedUserId: 1,
selectedUser: ({ state }) => state.users[state.selectedUserId]
}
// later on...
state.users[1] = { name: "Jon", surname: "Snow" };
state.selectedUserId = 1;
state.selectedUser.name // === "Jon"
Derived Functions
But Overmind doesn’t support derived functions, and those are quite useful. My proposal is that if your derived state returns a function, then it is a derived function:
const state = {
users: [],
userById: ({ state }) => id => state.users.find(user => user.id === id)
}
// later on...
state.users.push({ id: 1, name: "Jon", surname: "Snow" });
state.userById(1).name // === "Jon"
The types are:
type State = {
users: User[],
userById: DerivedFunction<State, number, User>
}
- First argument: the state.
- Second argument: the input of the function.
- Third argument: the returned type.
Nested Derived State
Now the tricky part, derived state nested in arrays or objects that are populated dynamically.
My idea is, instead of users: []
, use a function like users: array(...)
. Inside the array()
or object()
goes a generator
function that receives the non-derived state and return the combination of non-derived and derived state. Like this:
const user = ({ name, surname }) => ({
name,
surname,
fullname: ({ self }) => `${self.name} ${self.surname}`
})
const state: State = {
users: array(user)
}
// later on...
state.users.push({ name: "Jon", surname: "Snow" });
state.users[0].fullname // === "Jon Snow"
We need self
along with state
to access the item itself. Maybe the index as well. For example:
const user = ({ name, surname }, index }) => ({
name,
surname,
message: ({ state, self }) =>
`${self.name} is the number ${index} of ${state.app}`
})
The types are:
type User = {
name: string;
surname: string;
fullname: DerivedState<State, User, string>
}
type State = {
users: User[]
}
So it’s not complex. Same for objects:
const user = ({ name, surname }) => ({
name,
surname,
fullname: ({ self }) => `${self.name} ${self.surname}`
})
const state: State = {
users: object(user)
}
// later on...
state.users.jonSnow = { name: "Jon", surname: "Snow" };
state.users.jonSnow.fullname // === "Jon Snow"
The types are:
type User = {
name: string;
surname: string;
fullname: DerivedState<State, User, string>
}
type State = {
users: { [key: string]: User }
}
And it can receive the key instead of the index:
const user = ({ name, surname }, key) => ({
name,
surname,
message: ({ state, self }) =>
`${self.name} with key ${key} of ${state.app}`
})
I’ve tested part of the code and I think it’s doable with proxies and Typescript with something like this, that joins the input of the generator with an optional version of the output:
const array = <U, R>(generator: (item: U) => R): (U & Partial<R>)[] => {
// ...
}
// later on...
state.push({ name: "Jon", surname: "Snow" }) // fullname is optional