Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@frp-ts/state addition #53

Open
PalmZE opened this issue Apr 20, 2022 · 7 comments
Open

@frp-ts/state addition #53

PalmZE opened this issue Apr 20, 2022 · 7 comments
Labels
enhancement New feature or request

Comments

@PalmZE
Copy link

PalmZE commented Apr 20, 2022

@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).

@raveclassic
Copy link
Owner

@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?

@raveclassic
Copy link
Owner

Solid's implementation also looks nice and in fact it's much more composable

@raveclassic
Copy link
Owner

raveclassic commented Apr 21, 2022

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');

@raveclassic
Copy link
Owner

Btw, any ideas for a better name for produceMany?

@raveclassic raveclassic added the enhancement New feature or request label Apr 21, 2022
@PalmZE
Copy link
Author

PalmZE commented Apr 21, 2022

@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

@Fyzu
Copy link
Collaborator

Fyzu commented Apr 25, 2022

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

@Fyzu
Copy link
Collaborator

Fyzu commented Apr 25, 2022

@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" })

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants