@frp-ts/state addition
PalmZE opened this issue · 7 comments
@raveclassic, hi!
Would you accept the below feature as a separate frp-ts package\addition to the core package?
// API
type StateActions<S> = Record<string, (this: S, ...args: any[]) => undefined | void>;
type OmitThis<AS extends StateActions<any>> = {
[K in keyof AS]: AS[K] extends (this: any, ...args: infer U) => void | undefined ? (...args: U) => void : never;
};
export const newState = <S, AS extends StateActions<S>>(initial: S, actions: AS): Property<S> & OmitThis<AS> => {
throw Error('impl is skipped');
};
// USAGE
interface Dog {
name: string;
}
interface House {
dog: Dog;
}
interface AppState {
house: House;
}
const initialState: AppState = { house: { dog: { name: 'Fido' } } };
const store = newState(initialState, {
renameTheDog(newName: string) {
// this is typed correctly : AppState
this.house.dog.name = newName;
},
});
// typed correctly, this is omitted
store.renameTheDog('Odif');
Internally it will use immer to support concise mutation syntax.
Motivation
I find it convenient to group store\vm state into a single object and expose a single atom in the API. This leads to some boilerplate when you need to modify parts of the state.
Immer helps a lot, but we still need to use modify calls. This change simplifies this use case.
Besides, some libraries provide this out of the box (SolidJS as example).
@PalmZE Looks interesting, I'll try to book some time today to digest this.
In the meantime, I'm not a big fan of this
, how about turning the methods into functions taking current mutable state as an argument?
Solid's implementation also looks nice and in fact it's much more composable
Ok, I played a bit with immer
and came up with this solution #54
@PalmZE I don't think mixing methods with Atom
interface is a good idea as it leads to possible name clashes (e.g. we might want to add a subscribe
, set
etc. methods which would overwrite original methods defined for Atom
). Instead, I suggest working with method records directly. Still, you can create a bunch of methods and mix them into resulting Atom
by hand.
interface Dog {
name: string;
}
interface House {
dog: Dog;
}
interface AppState {
house: House;
}
const initialState: AppState = { house: { dog: { name: 'Fido' } } };
const storeState = newAtom(initialState)
const storeMethods = produceMany(storeState, {
renameTheDog: (newName: string) => state => {
state.house.dog.name = newName
}
})
const store = { ...storeState, ...storeMethods }
store.renameTheDog('Odif');
Btw, any ideas for a better name for produceMany
?
@raveclassic looks nice 👍
as for a better name, some ideas:
- newActions
- newSetters
- newReducer (handler functions ~ events)
Anyway, I guess jsdoc explaining that the API uses immer internally will do
I love this concept! I thought immer would fit well here :)
produceMany
is similar to immer naming and looks nice
But I agree with PalmZE that we can choose any name
@raveclassic Might as well try making a variant with a reducer like approach?
const dispatch = newAtomReducer(state, (draft, action: Actions) => {
if (action.type === "toggle") {
const todo = draft.find((todo) => todo.id === action.id)
todo.done = !todo.done
}
})
dispatch({ type: "toggle", id: "1" })