Skip to content

Commit

Permalink
feat(redux): type-safe mvp
Browse files Browse the repository at this point in the history
  • Loading branch information
rainerhahnekamp committed Dec 18, 2023
1 parent 284da58 commit aee6def
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 66 deletions.
24 changes: 13 additions & 11 deletions libs/ngrx-toolkit/src/lib/with-redux.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { signalStore } from '@ngrx/signals';
import { patchState, signalStore, withState } from '@ngrx/signals';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpParams } from '@angular/common/http';
import { map, switchMap } from 'rxjs';
import { noPayload, payload, withRedux } from './with-redux';

Expand All @@ -9,6 +9,7 @@ type Flight = { id: number };
describe('with redux', () => {
it('should load flights', () => {
const FlightsStore = signalStore(
withState({ flights: [] as Flight[] }),
withRedux({
actions: {
public: {
Expand All @@ -20,21 +21,22 @@ describe('with redux', () => {
},
},

reducer: (on, actions) => {
on(actions.loadFlights, (action, state) => state);
on(actions.flightsLoaded, (action, state) => state);
reducer: (actions, on) => {
on(actions.flightsLoaded, ({ flights }, state) => {
patchState(state, { flights });
});
},

effects: (actions, create) => {
const httpClient = inject(HttpClient);

create(actions.loadFlights).pipe(
switchMap(({ from, to }) =>
httpClient.get<Flight[]>('www.angulararchitects.io', {
params: { from, to },
})
),
map((flights) => actions.flightsLoaded)
switchMap(({ from, to }) => {
return httpClient.get<Flight[]>('www.angulararchitects.io', {
params: new HttpParams().set('from', from).set('to', to),
});
}),
map((flights) => actions.flightsLoaded({ flights }))
);
},
})
Expand Down
111 changes: 56 additions & 55 deletions libs/ngrx-toolkit/src/lib/with-redux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,73 +4,69 @@ import {
EmptyFeatureResult,
SignalStoreFeatureResult,
} from '@ngrx/signals/src/signal-store-models';
import { StateSignal } from '@ngrx/signals/src/state-signal';

type Action = { type: string };
/** Actions **/

type Actions = Record<string, Action>;
type Payload = Record<string, unknown>;

type State = Record<string, unknown>;
type ActionFn<
Type extends string = string,
ActionPayload extends Payload = Payload
> = ((payload: ActionPayload) => ActionPayload & { type: Type }) & {
type: Type;
};

type Payload = Record<string, unknown>;
type ActionFns = Record<string, ActionFn>;

type ActionsSpec = Record<string, Payload>;

type ActionsCreator<Spec extends ActionsSpec> = Extract<
keyof Spec,
'private' | 'public'
> extends never
? {
[ActionName in keyof Spec]: Spec[ActionName] & {
type: ActionName & string;
};
}
: {
[ActionName in keyof Spec['private']]: Spec['private'][ActionName] & {
type: ActionName & string;
};
} & {
[ActionName in keyof Spec['public']]: Spec['public'][ActionName] & {
type: ActionName & string;
};
};

type PublicActions<Spec extends ActionsSpec> = Extract<
keyof Spec,
'private' | 'public'
> extends never
? {
[ActionName in keyof Spec]: Spec[ActionName] & {
type: ActionName & string;
};
}
: {
[ActionName in keyof Spec['public']]: Spec['public'][ActionName] & {
type: ActionName & string;
};
};
type ActionFnCreator<Spec extends ActionsSpec> = {
[ActionName in keyof Spec]: ((
payload: Spec[ActionName]
) => Spec[ActionName] & { type: ActionName }) & { type: ActionName & string };
};

type ActionFnPayload<Action> = Action extends (payload: infer Payload) => void
? Payload
: never;

type ActionFnsCreator<Spec extends ActionsSpec> = Spec extends {
private: Record<string, Payload>;
public: Record<string, Payload>;
}
? ActionFnCreator<Spec['private']> & ActionFnCreator<Spec['public']>
: ActionFnCreator<Spec>;

type PublicActionFns<Spec extends ActionsSpec> = Spec extends {
public: Record<string, Payload>;
}
? ActionFnCreator<Spec['public']>
: ActionFnCreator<Spec>;

export function payload<Type extends Payload>(): Type {
return {} as Type;
}

export const noPayload = {};

export declare function createActions<Spec extends ActionsSpec>(
spec: Spec
): ActionsCreator<Spec>;
/** Reducer **/

type ReducerFn<A extends Action = Action> = (
action: A,
reducerFn: (action: A, state: State) => State
type ReducerFactory<StateActionFns extends ActionFns, State> = (
actions: StateActionFns,
on: <ReducerAction>(
action: ReducerAction,
reducerFn: (action: ActionFnPayload<ReducerAction>, state: State) => void
) => void
) => void;

type ReducerFactory<A extends Actions> = (on: ReducerFn, actions: A) => void;
/** Effect **/

type EffectsFactory<StateActions extends Actions> = (
actions: StateActions,
forAction: <EffectAction extends Action>(
type EffectsFactory<StateActionFns extends ActionFns> = (
actions: StateActionFns,
create: <EffectAction>(
action: EffectAction
) => Observable<EffectAction>
) => Observable<ActionFnPayload<EffectAction>>
) => void;

/**
Expand All @@ -83,16 +79,21 @@ type EffectsFactory<StateActions extends Actions> = (
* actions are passed to reducer and effects, but it is also possible to use other actions.
* effects provide forAction and do not return anything. that is important because effects should stay inaccessible
*/
export declare function withRedux<
export function withRedux<
Spec extends ActionsSpec,
Input extends SignalStoreFeatureResult,
StoreActions extends ActionsCreator<Spec> = ActionsCreator<Spec>,
PublicStoreActions extends PublicActions<Spec> = PublicActions<Spec>
StateActionFns extends ActionFnsCreator<Spec> = ActionFnsCreator<Spec>,
PublicStoreActionFns extends PublicActionFns<Spec> = PublicActionFns<Spec>
>(redux: {
actions: Spec;
reducer: ReducerFactory<StoreActions>;
effects: EffectsFactory<StoreActions>;
reducer: ReducerFactory<StateActionFns, StateSignal<Input['state']>>;
effects: EffectsFactory<StateActionFns>;
}): SignalStoreFeature<
Input,
EmptyFeatureResult & { methods: { actions: () => PublicStoreActions } }
>;
EmptyFeatureResult & { methods: PublicStoreActionFns }
> {
return (store) => {
const methods: PublicStoreActionFns = {} as unknown as PublicStoreActionFns;
return { ...store, methods };
};
}

0 comments on commit aee6def

Please sign in to comment.