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

RFC: Add action creator factory to Store #1480

Closed
brandonroberts opened this issue Dec 20, 2018 · 8 comments
Closed

RFC: Add action creator factory to Store #1480

brandonroberts opened this issue Dec 20, 2018 · 8 comments
Labels
community watch Someone from the community is working this issue/PR enhancement Project: Store

Comments

@brandonroberts
Copy link
Member

brandonroberts commented Dec 20, 2018

We encourage users to create many unique actions to describe application events. We should make creating and using these actions easier without abstractions.

Goals:

  • Should be type-safe
  • Implements a function that returns an Action
  • Optional usage with schematics

This would also make mapping within observable operators easier if no changes are required for the payload.

Describe any alternatives/workarounds you're currently using

Using action classes

Other information:

Similar request #379, but this is focused on creating actions and not dispatching and decorators.

@bossqone
Copy link

TIP: There is a project called typescript-fsa (https://github.com/aikoven/typescript-fsa) which does something similar.

@MichaelWarneke
Copy link
Contributor

MichaelWarneke commented Dec 22, 2018

What about Martin's proposal?
It seems straight forward and would be quite easy to add to the action.ts.
link to article

Also the syntax is very good for schematics to add actions.

@dummdidumm
Copy link
Contributor

Could you elaborate more on the requirements?

  • Should reducers written in switch-case-style get type inference/safety?
  • Should reducers written in if-style get type inference/safety?
  • Should there be a related reducer-factory package that works well together with the action-creator-factory? (similar to typescript-fsa-reducers on top of typescript-fsa)
  • How much flexibility should the API have? E.g: Transform inputs before creating the action payload, possibility to add arbitrary properties at top level (meta/isError/..)

@brandonroberts
Copy link
Member Author

The requirements are only dealing with action creators. I think something similar to what @MichaelWarneke mentioned is what I'd like to see. Reducer factories and AoT don't mix, as you end up having to wrap it in another function. At a minimum there is a generic actionCreator<T extends Action> function that implements a function that returns T.

@tja4472
Copy link
Contributor

tja4472 commented Feb 8, 2019

Something like?

// RFC: Add action creator factory to Store
// https://github.com/ngrx/platform/issues/1480
//
// 
// tslint:disable:max-classes-per-file
import { Injectable } from '@angular/core';

import { Actions, Effect, ofType } from '@ngrx/effects';
// import { Action } from '@ngrx/store';

import { tap } from 'rxjs/operators';

/* ngrx
export interface Action {
  type: string;
}
*/

/**************************************************
 * rex-tils
 * https://github.com/Hotell/rex-tils
 *************************************************/
export type AnyFunction = (...args: any[]) => any

// tslint:disable-next-line:interface-over-type-literal
export type StringMap<T> = { [key: string]: T }

export type Action<T extends string = string, P = void> = P extends void
  ? Readonly<{ type: T }>
  : Readonly<{ type: T; payload: P }>

export function createAction<T extends string>(type: T): Action<T>
export function createAction<T extends string, P>(
  type: T,
  payload: P
): Action<T, P>
export function createAction<T extends string, P>(type: T, payload?: P) {
  const action = payload === undefined ? { type } : { type, payload }

  // return IS_DEV ? Object.freeze(action) : action  
  return action
}

export type ActionsUnion<A extends StringMap<AnyFunction>> = ReturnType<
  A[keyof A]
>

/**************************************************
 * Actions
 *************************************************/
export const enum ActionTypes {
  Login = '[Login Page] Login',
  Logout = '[Login Page] Logout',
}

const RexActions = {
  login: (username: string, password: string) =>
    createAction(ActionTypes.Login, { username, password }),
  logout: () => createAction(ActionTypes.Logout),
};

type RexActions = ActionsUnion<typeof RexActions>;

const action1 = RexActions.login('UserName', 'Password');
const action2 = RexActions.logout();

/**************************************************
 * Effects
 *************************************************/
@Injectable()
export class AuthEffects {
  @Effect({ dispatch: false })
  authSignInSuccess$ = this.actions$.pipe(
    ofType(ActionTypes.Login),
    tap((loginAction) => {
      console.log(loginAction.payload.username);
    }));

    @Effect({ dispatch: false })
    logout$ = this.actions$.pipe(
      ofType(ActionTypes.Logout),
      tap((logoutAction) => {
        console.log(logoutAction.type);
      })
    );
  

  constructor(private actions$: Actions<RexActions>) {}
}

/**************************************************
 * Reducer
 *************************************************/
interface AuthState
  extends Readonly<{
    hasChecked: boolean;
  }> {}

const initialState: AuthState = {
  hasChecked: false,
};

function authReducer(state = initialState, action: RexActions): AuthState {
  switch (action.type) {
    case ActionTypes.Login:
      const a = action.payload.username;
      return { ...initialState, hasChecked: true };

    default:
      return state;
  }
}

@dummdidumm
Copy link
Contributor

I would like to have an API sort of like this

//
// Creating actions
//
const someActionFollowingPayloadScheme = actionCreator.withPayload<string>("MyAction");
someActionFollowingPayloadScheme("bla"); // --> {type: "MyAction", payload: "bla"}


const myAction = actionCreator<{bla: boolean; blubb: string}>("MyAction");
myAction({bla: true, blubb: "blubb"}); // --> {type: "MyAction", bla: true, blubb: "blubb"}

const someActionWithPayloadTransformation = actionCreator("MyAction", (a: boolean, b: string) => ({c: !a, d: b}));
someActionWithPayloadTransformation(true, "bla"); // --> {type: "MyAction", c: false, d: "bla"};
// Above also supported on the withPayload-actionCreator

//
// Match Util
//

if (myAction.match(someOtherAction)) {
   // someOtherAction inferred as type of myAction
}

effect$ = this.actions$.pipe(
   filter(myAction.match),
   map(action => {
     // inferred as type of myAction
  )
);

Maybe this can be combined with the ideas above.

@brandonroberts
Copy link
Member Author

@dummdidumm I don't see that as part of this implementation. Something similar to what @tja4472 posted is where we want to start.

@dummdidumm
Copy link
Contributor

dummdidumm commented Feb 18, 2019

Well, at least some form of match-utility would be nice, it would make the action's type inference self-contained and not dependent on SomeActionsUnion-tricks that are needed for reducers and ofType (and kind of enforces the ducks-pattern).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
community watch Someone from the community is working this issue/PR enhancement Project: Store
Projects
None yet
Development

No branches or pull requests

6 participants