Search code examples
javascriptangulartypescriptngrxngrx-effects

Chaining actions with error handling in NgRx?


I have scenario where a user fills out a form, select 3 required files, and on submitting the form i want to use NgRx to chain the actions of uploading the files one at a time, use the returned values of ID's from the server, and in the end create an object using the returned values.

I also need to handle any errors in the file upload, to notify the user and have them, possibly select another file and continue the process.

I am trying to learn the best practice and approach to chaining actions, but have encountered some issues.

I have 3 similar actions for file uploads add_first_file, add_second_file, add_third_file:

export const add_first_file = createAction(
  '[VIEW - CREATE OBJECT] - Add The First File',
  props<{
    file: any;
  }>()
);

export const add_first_file_success = createAction(
  '[VIEW - CREATE OBJECT] - Add The First File Success',
  props<{ created_file_id: string }>()
);

export const add_first_file_failure = createAction(
  '[VIEW - CREATE OBJECT] - Add The First File Failure',
  props<{ error: string }>()
);

My first attempt was to just fire of multiple actions in the first effect, but obviously that is not a valid approach, as also mentioned here

I then tried to create effects that returned the next action file upload, so that in my NgRx Effect:

add_first_file = createEffect(() => {
    return this.actions$.pipe(
      ofType(actions.add_first_file),
      switchMap((_file) =>
        from(
          this.http_service.upload_a_file(
            _file.file
          )
        ).pipe(
          map((_id) => {
            return actions.add_second_file({
                created_file_id: _id,
            });
          }),
          catchError((error: any) => {
            return of(
              actions.add_first_file_failure({
                error: error,
              })
            );
          })
        )
      )
    );
  });

Where i would then assigned the returned values to the object in my store:


  on(actions.add_first_file_success, (state, { created_file_id }) => {
    return {
      ...state,
      created_file_id: created_file_id,
      status: 'success',
    }
  }),


  on(actions.add_first_file_failure, (state, { error }) => {
    return {
      ...state,
      status: 'failed',
    }
  }),

This resulted in an infinite loop, and also creates other issues, on something like file upload errors, where the chain would stop, and the approach will also need to re-upload files that was already successfully uploaded already in order to use the chained actions returned on success.

i also tried having an action that returned an array of actions as i found in use cases for triggering multiple actions in effects, even though it is not best practice:

add_files = createEffect(() => {
    return this.actions$.pipe(
      ofType(actions.form_submit),
      switchMap((_form) => [
        actions.add_first_file(_form.first_file),
        actions.add_second_file(_form.second_file),
        actions.add_third_file(_form.third_file)
      ]
      )
    );
  });

This also doesn't really allow me to handle individual possible errors i believe.

I was hoping someone could help me out with some good examples of how to best use NgRx to chain actions or some other approach, to solve the use case i described.


Solution

  • I don't know your complete use-case, so the answer might be not fully complete but I will try to describe the solution as good as I can.

    Please, keep in mind that while chained actions are not really an antipattern by itself BUT creating a workflow based on chained actions is usually considered as antipattern. From your example it doesn't look like you have a workflow (unless number of files can vary and error handling is more complex) but as I said, you didn't describe the whole story.

    From your description I personally don't see the point to use NGRX at all. NGRX is used for global state management, here it doesn't look like you are changing global state. All you want is to upload a few files sequentially and concatenate the results. From my perspective, file uploading process is the responsibility of your page/component. When it is done or failed, the component will dispatch an action that will change the global state.

    If you are trying to implement an async file upload, when user navigates away, the app uploads files in the queue, retries failed uploads and shows a notification "File A was (not) uploaded", etc then I would use service workers in combination with NGRX but not NGRX effects.