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(signal-slice): pass initial state streams to source functions #486

Merged

Conversation

joshuamorony
Copy link
Contributor

@joshuamorony joshuamorony commented Aug 15, 2024

Implementation of proposal discussed here: #485

This PR is non-breaking, it adds onto the state object passed to sources but it does not change existing functionality

This adds the ability to use streams of the signal slices own state within sources (e.g. if there is an initial state property of myVal then a myVal$ stream will be available on the state object passed to a source), which allows a signalSlice to react to its own state changing without needing to use external subjects, or without using an actionSource which will lead to a more imperative style of code.

Here is an example of plain RxJS handling a pagination scenario in a way that is cleanly declarative (but perhaps not the most practical):

  pageNumber$ = new BehaviorSubject(1);
  itemFilter$ = new BehaviorSubject('');

  request$ = combineLatest([this.pageNumber$, this.itemFilter$]).pipe(
    switchMap(([pageNumber, itemFilter]) =>
      this.getPage(pageNumber, itemFilter).pipe(
        materialize(),
        filter((notification) => notification.kind !== 'C'),
      ),
    ),
    share(),
  );

  data$ = this.request$.pipe(
    filter((notification) => notification.kind === 'N'),
    dematerialize(),
  );

  error$ = this.request$.pipe(
    filter((notification) => notification.kind === 'E'),
    switchMap((notification) => of(notification.error)),
  );

My goal was to allow this style of code to be followed reasonably faithfully using only features of signalSlice. With the change in this PR, we are able to write code like this:

  private initialState = {
    pageNumber: 1,
    itemFilter: '',
    loading: true,
    error: null as string | null,
    data: null as string | null,
  };

  public state = signalSlice({
    initialState: this.initialState,
    sources: [
      (state) =>
        combineLatest([state.pageNumber$, state.itemFilter$]).pipe(
          switchMap(([pageNumber, itemFilter]) =>
            this.getPage(pageNumber, itemFilter).pipe(
              map((data) => ({ data, loading: false })),
              startWith({ loading: true }),
              catchError((error) => of({ error })),
            ),
          ),
        ),
    ],
    actionSources: {
      offsetPage: (state, action$: Observable<number>) =>
        action$.pipe(
          map((offset) => ({ pageNumber: state().pageNumber + offset })),
        ),
      setFilter: (_, action$: Observable<string>) =>
        action$.pipe(map((itemFilter) => ({ itemFilter }))),
    },
  });

Note specifically that no external subjects are required here. Our action sources update the state values, and our pre-defined sources can react to those changes via the pageNumber$ and itemFilter$ streams that have been added.

The discussion linked also discussed potentially creating automatic setter action sources for the initial state, so that you could update them without needing to manually define an actionSource as I have done above. This PR does not include that change, if we end up deciding on doing that I will open a separate PR for that.

@nartc nartc merged commit 5342f5a into ngxtension:main Oct 8, 2024
1 check passed
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.

2 participants