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: withRedux: global action dispatching / inter-store communication #3

Open
rainerhahnekamp opened this issue Dec 21, 2023 · 3 comments

Comments

@rainerhahnekamp
Copy link
Collaborator

rainerhahnekamp commented Dec 21, 2023

Update 1 (21.12 (17.01)): Self-dispatching external actions

Use Case

withRedux integrates the Redux pattern into a signalStore. Actions are currently methods of the store instance which means we don't have a global dispatching mechanism.

Example:

const FlightStore = signalStore({
  withEntities<Flight>(),
  withRedux({
    actions: {
      search: payload<{from: string, to: string}>()
    },
    // ...
  })
})

const BookingStore = signalStore({
  withEntities<Booking>(),
  withRedux({
    actions: {
      bookFlight: payload<{from: string, to: string}>()
    },
    // ...
  })
})

It is not possible for bookingStore to have a reducer on FlightStore::search. It would be possible though to dispatch search via an effect or withMethods:

const BookingStore = signalStore({
  withEntities<Booking>(),
  withRedux({
    actions: {
      bookFlight: payload<{from: string, to: string}>()
    },
    effect(actions, create) {
      const flightStore = inject(FlightStore);
      return {
        bookFlight$: create(actions.bookFlight).pipe(tap(() => flightStore.search({from: 'London', to: 'Vienna'})))
      }
    }
  })
})

That works as long as we have only global stores. As soon as a global store, wants to listen to actions from al local one, the DI will fail.

Proposed Approach:

Here's a design which would introduce global actions for global and local SignalStores.

We require two new features:

  1. Global (self-dispatching) Actions
  2. reducer option to consume "instance-only" dispatched actions or global ones.

To reference actions without a store's instance, we need to be able to externalize them. That is exactly what we have with the Global Store:

export const flightActions = createActions('flights', {search: payload<{from: string, to: string}>()}) 

const FlightStore = signalStore({
  withEntities<Flight>(),
  withRedux({
    actions: flightActions
    // ...
  })
})

// ...

If another Signal Store wants to react to that action, it can do:

withRedux({
  reducer(, on) {
    on(flightActions.search, (state, {from, to}) => patchState(state, {loading: true}));
  },
  // ...
})

It will still be possible to define actions inside withRedux::actions.

External actions are self-dispatching. They do not require a "DispatcherService", like the Store in the Global Store:

withRedux({
  effect(actions, create) {
    return {
      bookFlight$: create(actions.bookFlight).pipe(tap(() => flightActions.search({from: 'London', to: 'Vienna'})))
    }
  }
})

In contrast to the global store, Signal Stores can be provided multiple times. That means we have more than one instance.

There are use cases, where a "local Signal Store" only needs to consume actions dispatched by its instance. The on method will get an optional option to

const FlightStore = signalStore({
  withEntities<Flight>(),
  withRedux({
    actions: {
      searched: payload<{flights: Flight[]}>()
    },
    reducer(actions, on) {
      on(actions.searched, (state, {flights}) => patchState(state, {flights}), {globalActions: false})
    }
    // ...
  })
})

Actions have to be unique per Store class.

Abandoned (simpler) Approach: storeBound actions & inject in reducer

const BookingStore = signalStore({
  withEntities<Booking>(),
  withRedux({
    actions: {
      bookFlight: payload<{from: string, to: string}>()
    },
    reducer(actions, on) {
      const filghtStore = inject(FlightStore);
      on(flightStore.search, (state) => patchState(state, {loading: true}));
    },
    effect(actions, create) {
      const flightStore = inject(FlightStore);
      return {
        bookFlight$: create(actions.bookFlight).pipe(tap(() => flightStore.search({from: 'London', to: 'Vienna'})))
      }
    }
  })
})
@rosostolato
Copy link

I prefer this approach and have a question: if a store has multiple instances, it means you need to add the dispatcher to the instance provider, right? How would you do that?

@rainerhahnekamp
Copy link
Collaborator Author

For compontent provided stores, it would look like this:

const FlightStore = signalStore({
  withEntities<Flight>(),
  withRedux({
    actions: {
      searched: payload<{flights: Flight[]}>()
    },
    reducer(actions, on) {
      on(actions.searched, (state, {flights}) => patchState(state, {flights}), {globalActions: false})
    }
    // ...
  })
})

const flightStore = new FlightStore();
flightStore.searched({flights: []});

So you would have to have access to the store instance in order to dispatch a local-managed action.

@markostanimirovic
Copy link

Leaving my suggestion here: https://x.com/MarkoStDev/status/1753556633089704059?s=20

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

No branches or pull requests

3 participants