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

Proposal: State Operators #545

Closed
markwhitfeld opened this issue Aug 15, 2018 · 17 comments
Closed

Proposal: State Operators #545

markwhitfeld opened this issue Aug 15, 2018 · 17 comments
Assignees
Projects
Milestone

Comments

@markwhitfeld
Copy link
Member

markwhitfeld commented Aug 15, 2018

Versions

* ngxs: v3.2.0
* @angular/core: v6.1

Background

The NGXS patchState method is used to do immutable object updates to the container state slice without the typical long-handed syntax. This is very neat and convenient because you do not have to use the getState and setState as well as the Object.assign(…)or the spread operator to update the state. The patchState method only offers a shallow patch and as a result is left wanting in more advanced scenarios. 
What if it is possible to make the patchState method extensible to create a more advanced yet simple mechanism for state updates?… Let me introduce patch operators…

Proposal

The basic idea of this proposal is that we could describe the modifications to the state using curried functions that are given any inputs that they need to describe the change and are assigned to the part of the state tree that they should be applied to. The curried function is then run using the state slice that they are assigned to and the returned value is used as the new value for that state slice.

The function that produces the curried function that is still awaiting the slice of state to be applied to is called a patch operator. The type of this curried function could be defined as follows:

type op<T> = (state: T) => T;

We can make a number of these patch operator functions in order to help with common scenarios for state manipulation. Some ideas are:

  • patch<T>(value: T) : op<T>
  • set<T>(value: T) : op<T>
  • iif<T>(condition: Predicate<T>, then: op<T>, else: op<T>) : op<T>

And some array operators:

  • patchItem<T>(selector: number | Predicate<T>, operator: op<T>) : op<T[]>
  • removeItem<T>(selector: number | Predicate<T>) : op<T[]>
  • insertItem<T>(value: T, beforePosition? : number) : op<T[]>

Example:

We could express some modifications to a state as follows:

ctx.patchState({
  greeting: 'Howzit',
  token: patch({
    lastUpdate: new Date(),
    active: true,
    origin: iif( origin => !origin.ip, patch({ ip: '192.168.1.1'})
  }),
  roles: [
    removeItem(3),
    insertItem({id:55, name: 'Admin'}),
    patchItem( x => x.id === 64, {name:'Editor'})
  ]
});

Please add any other ideas for operators to the comments.
Feedback welcome!

PS. Related Meduim posts here:
Initial proposal: https://medium.com/@mark.whitfeld/ngxs-proposal-patch-operators-75b24d309b91
Announcement before v3.4: https://medium.com/ngxs/ngxs-state-operators-8b339641b220

@markwhitfeld markwhitfeld self-assigned this Aug 16, 2018
@garthmason
Copy link

Great ideas @markwhitfeld - our team would really benefit from those array operators, that's where I think we have the most verbose patching code. This would really clean things up for us.

@MurhafSousli
Copy link

MurhafSousli commented Sep 10, 2018

Currently, you can create a child state (sub state) for nested objects and trigger patchState for that same action in both parent and child states, but having this would be useful too!

@markwhitfeld
Copy link
Member Author

@garthmason That's great to hear. I have been busy moving house lately so have not had much chance to work on it.
Any other operators that you think would be useful to you?
You could even paste code that you think could benefit from an operator and we can have a look at what it might look like.

@MurhafSousli The child state would be useful if you had a nested child object (as opposed to an array) but I see the main utility for this where you want to do nested updates of an array.

@juracy
Copy link
Contributor

juracy commented Sep 13, 2018

@markwhitfeld why not split that in two parts:

  • a library or plugin with the operators
  • a change in patchState (core) to support a function as parameter: patchState(val: () => Partial<T> | Partial<T>)

In this way, the core functionality doesn't grow much and at same time allows other kind of mutation by function, maybe passing the current state for it: patchState(val: (state: T) => Partial<T> | Partial<T>)

@markwhitfeld
Copy link
Member Author

@juracy This is almost exactly the idea. The operators will exist in a separate package and the base patchState method will receive a slight tweak (which also will just include use of a base patch operator).

The signature will be more like this: patchState(val: (state: T) => T | Partial<T> | PatchObject<T>).
The PatchObject<T> is essentially an extension of Partial<T> where each property could be T[K] or (state: T[K]) => T[K].

This would allow for the use of something like immer using this syntax:
ctx.patchState(produce((state) => state.counter++ ));

@markwhitfeld
Copy link
Member Author

See this stackblitz for the playground:
https://stackblitz.com/edit/ngxs-patch-operators?embed=1&file=src/patch-operators.ts

@juracy
Copy link
Contributor

juracy commented Sep 13, 2018

@markwhitfeld Humm, sorry, a typical TL;DR mistake (well not too long, indead) !

So, why not submit a PR for patchState right away?

@markwhitfeld
Copy link
Member Author

I'm wanting to get to get the Typescript typings definitions working great for this first and this requires TypeScript 2.8 and above. @amcdnl is wanting to wait until Angular supports Typescript 2.8 before we take that dependency. I think that Angular 6.1 may be on TypeScript 2.9 now. I can bring this in with @amcdnl's go ahead.

@Newbie012
Copy link
Contributor

Newbie012 commented Sep 16, 2018

I just stumbled across an issue that could've been solved with this approach, looking forward! :)

@markwhitfeld markwhitfeld added this to the 3.3.0 milestone Oct 21, 2018
@splincode

This comment has been minimized.

@markwhitfeld
Copy link
Member Author

markwhitfeld commented Nov 7, 2018

Hi everyone. So I think that this idea is evolving a bit...
I am going to be adding another option in the way that you can call ctx.setState where it could be given a function. The ctx.setState method would then execute this function, passing the previous state and set the state to the value returned by the function. For example:
Before

@Action(MyAction)
public addValue(ctx: StateContext, { payload }: MyAction) {
  ctx.setState({ ...ctx.getState(), value: payload  });
}

After

@Action(MyAction)
public addValue(ctx: StateContext, { payload }: MyAction) {
  ctx.setState((state) => ({ ...state, value: payload }));
}

Doesn't seem like much but this function could then be extracted and reused. Also this is the exact signature of these "patch operators". So you could forseeably do this:

@Action(RemoveContact)
public removeContact(ctx: StateContext<Contact[]>, { payload }: RemoveContact) {
  ctx.setState(removeItem( x => x.id === payload.id ));
}

So, what do I need from you? Since these operators could be more general purpose than just for patching we could think of a better name. Some of the options I have brainstormed with @eranshmil are:

  • State Operator
  • State Transformation
  • State Transformer
    ... or maybe we just keep the "Patch Operators" name.

As a general description of what they are I would say that they are functions that define transformations to apply to an existing immutable state to produce a new state.

Can I ask for your votes or suggestions for a name?

@markwhitfeld markwhitfeld modified the milestones: 3.3.0, 3.4.0 Nov 11, 2018
@poloagustin
Copy link
Contributor

poloagustin commented Dec 18, 2018

State Operators sounds awesome, @markwhitfeld !

@splincode
Copy link
Member

Can be closed?

@markwhitfeld
Copy link
Member Author

Published a teaser for the feature ahead of the 3.4 release:
https://medium.com/ngxs/ngxs-state-operators-8b339641b220

@splincode splincode moved this from @markwhithfeld to @arturovt in 3.4.0 Feb 15, 2019
@splincode splincode moved this from @arturovt to ready to release in 3.4.0 Feb 15, 2019
@markwhitfeld markwhitfeld moved this from ready to release to Done in 3.4.0 Feb 28, 2019
@splincode splincode unpinned this issue Feb 28, 2019
@isochronous
Copy link
Contributor

isochronous commented Aug 19, 2021

Your documentation is still pointing to this thread like this feature is a WIP
https://www.ngxs.io/concepts/state

@markwhitfeld
Copy link
Member Author

@isochronous Thank you for pointing that out!
Would you like to submit a PR with an improvement?
I definitely think that State Operators are one of the magic ingredients in ngxs and it should be highlighted more here... and definitely the fact that it is no longer a Work In Progress.

@isochronous
Copy link
Contributor

isochronous commented Aug 20, 2021 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
No open projects
3.4.0
  
Done
Development

No branches or pull requests

8 participants