Skip to content

Support optional AbortSginal argument when dispatch thunk action #3924

@chigia001

Description

@chigia001

In our codebase, we have some thunk that trigger another thunk.

Example:

const getEmployer = createAsyncThunk('getEmployer', () => {
  // retrieve employer of from current user
})

const getEmployee = createAsyncThunk('getEmployee', ({id}) => {
  // retrieve employee of from employer's id
})

const getColleagues = createAsyncThunk('getColleagues', ({}, {dispatch, signal}) => {
  if (signal.aborted) {
   return;
  }
  const employerThunk = dispatch(getEmployer())

  signal.addEventListener('abort', () => {
    employerThunk.abort()
  })

  const employer = await employerThunk.unwrap()

  if (signal.aborted) {
   return;
  }
  
  const employeeThunk = dispatch(getEmployee({id: employer.id}))

  signal.addEventListener('abort', () => {
    employeeThunk.abort()
  })

  return employeeThunk.unwrap()
})

useEffect(() => {
  const colleagueThunk = dispatch(getColleagues())

  setTimeout(() => {
    cooleagueThunk.abort()
  }, 1000)
}, [])

to successfully abort those inner thunk we need to add a hand full of code

if possible, can we pass the AbortSignal somehow to the action creator itsel and then RTK handle can handle them?

const getEmployer = createAsyncThunk('getEmployer', () => {
  // retrieve employer of from current user
})

const getEmployee = createAsyncThunk('getEmployee', ({id}) => {
  // retrieve employee of from employer's id
})

const getColleagues = createAsyncThunk('getColleagues', ({}, {dispatch, signal}) => {
  if (signal.aborted) {
   return;
  }

  // passing signal from parent thunk
  // even better if rtk can detect that signal already aborted and omit the thunk 
  // like how condition behave.
  const employer = dispatch(getEmployer(undefined, {signal})).unwrap()

  if (signal.aborted) {
   return;
  }

  // passing signal from parent thunk  
  return dispatch(getEmployee({id: employer.id}, {signal})).unwrap()
})

useEffect(() => {
  const timeout = AbortSginal.timeout(1000);

  // passing signal from parent code.
  dispatch(getColleagues(undefined, {signal}));
}, [])

I think this change align with the ecosystem as most API that support AbortSginal/AbortController allow receive AbortSginal from invoke method.

My main concern with above suggestion is that the interface is a little bit wonky for action creator without argument.

Another interface we can explore is withMeta property, similar to the pending, rejected, fullfilled action type reference. But now the meta like signal will be alway be the first argument, the action creator argument will be the 2nd argument if they need. It also allow caller to pass meta information to reducer.

const getEmployer = createAsyncThunk('getEmployer', () => {
  // retrieve employer of from current user
})

const getEmployee = createAsyncThunk('getEmployee', ({id}) => {
  // retrieve employee of from employer's id
})

const getColleagues = createAsyncThunk('getColleagues', ({}, {dispatch, signal}) => {
  // asumpt rtk already handle Canceling Before Execution with signal
  const employer = dispatch(getEmployer.withMeta({signal})).unwrap()
  return dispatch(getEmployee.withMeta({signal}, {id: employer.id})).unwrap()
})

useEffect(() => {
  const timeout = AbortSginal.timeout(1000);

  // passing signal from parent code.
  dispatch(getColleagues.withMeta({signal}));
}, [])

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions