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

feat(store): add set method to akita store #663

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

field123
Copy link
Contributor

@field123 field123 commented Apr 26, 2021

Created a public method on the akita store that will completely replace the current state with the new provided state and not just update the value.

Closes #661

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

What is the current behavior?

Issue Number: #661

What is the new behavior?

When the set() method is used the current state of the store will be replaced not just { ...currentState, ...newState } merged together.

See issue #661 for more details.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Created a public method on the akita store that will completely replace the current state with the new provided state and not just update the value.

Closes salesforce#661
Renamed the new set method to overwrite in an effort to avoid conflicts with the entityStore method set.

Closes salesforce#663
PR salesforce#661
type SessionState = SessionStateAuthenticated | SessionStateUnauthenticated;

interface SessionStateAuthenticated {
_tag: 'session-state-authenticated';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer using a simple example state and not copy-paste from your application.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a much reduced version of the original, I though that session state was a good simple example and "real world" enough to be useful. My preference when reading documentation has always been simplified "real world" over slightly contrived.

I could simplify the example further to

type SessionState = AuthenticatedState | UnauthenticatedState;

interface AuthenticatedState {
  _tag: 'authenticated';
  uid: string;
}

interface UnauthenticatedState {
  _tag: 'unauthenticated'
}

or even do something like

type SessionState = TokenState | InitialState;

interface TokenState {
  name: string;
  token: string;
}

interface InitialState{
  name: string;
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the latter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll change to the second option.

}
```

Using the above state model and value of current state, if we call `this.store.update({ _tag: 'session-state-unauthenticated' })` our state will be updated to the following:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need all of this. The method name is straightforward.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was concerned that newer users may be unsure when to use each method, but it could be way to verbose.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overwrite name is clear enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll make the change.

}

private prepareNewState<S>(stateOrCallback: Partial<S> | UpdateStateCallback<S>, currentState: S, withHookFn: (c: S, n: S) => S): S {
const constructNewState = (x: Partial<S> | UpdateStateCallback<S>, cs: S) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Why do we need a function?
  • Why do we need to create a new function each time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Why do we need a function?

I've used a hook function so the prepareNewState method is not bound to a specific class method e.g akitaPreOverwrite or akitaPreUpdate instead a wrapping function of the relevant class method is used in update, overwrite or any future methods. This allows for the same class class overriding of the methods yet still being able to pass them as HOF.
const hookFn = (curr: S, newS: S) => this.akitaPreOverwrite(curr, newS as S)

Using the hook means the ordering of operations can be maintaned.

  1. Why do we need to create a new function each time?

In an effort to make small single purpose functions I like to lexically scope util functions. It does have the possible performance hit but I think the clarity of being able to scroll to the bottom of a method/fn and know what's happening is worth it.

const newState = extractNewState(stateOrCallback, currentState);
const withHook = hookFn(currentState, newState);
return resolveFinalState(currentState, withHook);

This makes it clear I'm extracting the new state from my callback and current state -> apply my hook to the currentState and newState -> finally resolving the final state. I could makes these more descriptive but don't understand what the _producerFn is or why your checking if the state is an object.

You can also pluck out these small functions and raising their scope if you have a need for them elsewhere.

The alternative would be

let newState;
if (isFunction(stateOrCallback)) {
 newState = isFunction(this._producerFn) ? this._producerFn(currentState, stateOrCallback) : stateOrCallback(currentState);
} else {
 newState = stateOrCallback;
};
const withHook = hookFn(currentState, newState);
return isPlainObject(currentState) ? withHook : new (currentState as any).constructor(withHook);

While this can be made more clear through comments it could be tricky to understand what the fn/method is doing.

If the nest functions are a concern from performance we could put the functions in private methods, extract them out of the class or of course just remove them and have all the code blocked together.

// Option 1
// Inside the Store class

private extractNewState(x: Partial<S> | UpdateStateCallback<S>, cs: S) {
    if (isFunction(x)) {
      return isFunction(this._producerFn) ? this._producerFn(cs, x) : x(cs);
    } else {
      return x;
    }
  }

  private resolveFinalState(currentState: S, withHook: S): S {
    return isPlainObject(currentState) ? withHook : new (currentState as any).constructor(withHook);
  }

// Option 2

// Before or after the Store class def

const extractNewState = <S>(x: Partial<S> | UpdateStateCallback<S>, cs: S, _producerFn: (state: any, fn: any) => any) => {
  if (isFunction(x)) {
    return isFunction(_producerFn) ? _producerFn(cs, x) : x(cs);
  } else {
    return x;
  }
};

const resolveFinalState = <S>(currentState: S, withHook: S): S => {
  return isPlainObject(currentState) ? withHook : new (currentState as any).constructor(withHook);
};

That's my reasoning, happy to do what ever fits best with the project. 👍

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need that constructNewState and resolveFinalState to be local functions?

Why are you not inline them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this:

const extractNewState = (x: Partial<S> | UpdateStateCallback<S>, cs: S) => {
      if (isFunction(x)) {
        return isFunction(this._producerFn) ? this._producerFn(cs, x) : x(cs);
      } else {
        return x;
      }
};

const inCaseClassResolveState= (currentState: S, withHook: S): S => {
   return isPlainObject(currentState) ? withHook : new (currentState as any).constructor(withHook);
};

const newState = extractNewState(stateOrCallback, currentState);
const withHook = hookFn(currentState, newState);
return inCaseClassResolveState(currentState, withHook);

To be more immediately understandable than:

let newState;
if (isFunction(stateOrCallback)) {
 newState = isFunction(this._producerFn) ? this._producerFn(currentState, stateOrCallback) : stateOrCallback(currentState);
} else {
 newState = stateOrCallback;
};
const withHook = hookFn(currentState, newState);
return isPlainObject(currentState) ? withHook : new (currentState as any).constructor(withHook);

If future me or another developer comes along and sees the line of code isPlainObject(currentState) ? withHook : new (currentState as any).constructor(withHook); it may not make sense without diving in and understanding it. But if the block of code is wrapped in a function called inCaseClassResolveState you get some understanding straight away of what the operation is doing.

With the added benefit of not mutating (with variable reassignment) the newState variable once declared. e.g. using const and not let.

I'm happy to just change the class method to match below if you prefer?

private prepareNewStateAlt<S>(stateOrCallback: Partial<S> | UpdateStateCallback<S>, currentState: S, hookFn: (curr: Readonly<S>, newS: Readonly<S>) => S): S {
    let newState;
    if (isFunction(stateOrCallback)) {
      newState = isFunction(this._producerFn) ? this._producerFn(currentState, stateOrCallback) :    stateOrCallback(currentState);
    } else {
      newState = stateOrCallback;
    }
    const withHook = hookFn(currentState, newState);
    return isPlainObject(currentState) ? withHook : new (currentState as any).constructor(withHook);
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comments please, we don't need to create redundant functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes done.

return x;
}
}
const resolveFinalState = (currentState: S, withHook: S): S => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Removed redundant local functions in favour of inlining.

Simplified the documentation example for overwrite()

Closes salesforce#661
PR salesforce#663
@tony
Copy link
Contributor

tony commented Jun 11, 2022

@field123 Can you rebase?

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

Successfully merging this pull request may close these issues.

Store Update - merges current and previous state values.
3 participants