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

TypeScript createAsyncThunk optional or no args #489

Closed
stephen-dahl opened this issue Apr 8, 2020 · 29 comments
Closed

TypeScript createAsyncThunk optional or no args #489

stephen-dahl opened this issue Apr 8, 2020 · 29 comments

Comments

@stephen-dahl
Copy link

stephen-dahl commented Apr 8, 2020

I am trying to make an api call with an optional parameter however typescript complains

export const usersLoad = createAsyncThunk(
	'users/load',
	(filters: UserFilters = {}) => Api.get<User[]>('/api/users/search', filters)
);
...
dispatch(usersLoad());

Expected 1 arguments, but got 0

I have a similar issue with this action

export const usersLoadMore = createAsyncThunk(
	'users/loadMore',
	(_: SyntheticEvent | void, thunkApi) => {
		const state = thunkApi.getState() as RootState;
		const { filters, data } = state.users;
		return Api.get<User[]>('/api/users/search', {
			...filters,
			offset: data.length,
		});
	}
);

here I don't use the arg but I had to give it the SyntheticEvent or it complained when passing it to onClick, and I had to give it void to make it work when I call it directly. I would think that I would not have to set it to anything since I am not using it.

@phryneas
Copy link
Member

phryneas commented Apr 8, 2020

Yeah, that's not possible at the moment.
I will look into this in the near future, but in the meantime you can do something like

const _usersLoad = createAsyncThunk(
	'users/load',
	(filters: UserFilters) => Api.get<User[]>('/api/users/search', filters)
);

export const usersLoad = (filters: UserFilters = {}) => _usersLoad(filters);

I admit it's a bit hacky, but it should do as a workaround for now.

@stephen-dahl
Copy link
Author

ya i'm working around it.
still new to typescript but it looks like the typing returns a function with the signature
(arg: ThunkArg) => .... making arg required

@phryneas
Copy link
Member

Yes. As I said:

that's not possible at the moment.
I will look into this in the near future

We simply didn't anticipate this use case and none of our beta testers had it either, so we'll have to add it.

@markerikson
Copy link
Collaborator

Should be resolved by https://github.com/reduxjs/redux-toolkit/releases/tag/v1.3.5 .

@drydenwilliams
Copy link

drydenwilliams commented Mar 25, 2021

@markerikson please could you show an example of an optional argument? That like looks like it just cancels it no? Also it seem using createAsyncThunk seems to be more boilerplate in components? I seem to have:

// someThunk.ts
export const fetchSomeThunk = createAsyncThunk(
  'app/fetchSomeThunk',
  async (arg: IMyArg) => {
    return axios.get(url)
  }
)

and then in a component:

// myComponent.tsx
const someThunkCall = async () => {
    try {
      const someThunkAction = await dispatch(someThunk(''))
      unwrapResult(someThunkAction)
    } catch (error) {
      Bugsnag.notify(error)
    }
}

where as I quite liked:

// someThunkOld.tsx
export const someOldSchoolActionCreator = (): Thunk => async (dispatch) => {
  try {
    const resp = await axios.get(url)
    dispatch(someThunkSuccess(resp.data))
  } catch (err) {
    console.error(err)
  }
}

as it's all together in one place, limited duplication. It is nice having someThunkCall as you can directly update state but it's a bit of a weird uni-directional flow, a bit back and forth? What's the implications, if any, of the someOldSchoolActionCreator way?

@markerikson
Copy link
Collaborator

Uh... sorry, I'm really not sure what you're asking there, in either part of that comment. Can you clarify what you're asking?

@drydenwilliams
Copy link

drydenwilliams commented Mar 25, 2021

You mentioned: Should be resolved by https://github.com/reduxjs/redux-toolkit/releases/tag/v1.3.5 . but I don't really see how that example relates to the title issue of: optional or no args. Just wondered if you can show an example of a createAsyncThunk with "option or no args" in it?

And what's the benefit of using fetchSomeThunk in comparison to someOldSchoolActionCreator??

@markerikson
Copy link
Collaborator

markerikson commented Mar 25, 2021

I would assume that the very first comment in this thread is the "optional arg" that's being discussed:

export const usersLoad = createAsyncThunk(
	'users/load',
	(filters: UserFilters = {}) => Api.get<User[]>('/api/users/search', filters)
);

Here, the one argument to the payload creator is filters: UserFilters, but since a default value has been provided, TS makes that optional: filters?: UserFilters.

That means it should be legal to dispatch this thunk either with a parameter passed in:

dispatch(usersLoad(someFiltersValue));

or without any parameters:

dispatch(usersLoad());

The issue was that the original typings in 1.3.4 and earlier did not allow the second option to work correctly with TypeScript, and so it would yell at you if you didn't provide a value. As of 1.3.5, dispatch(usersLoad()) should work okay with the TS compiler.

I'm still not entirely sure what you're asking about using createAsyncThunk vs writing a thunk by hand. Overall, the point of createAsyncThunk is to abstract the standard "dispatch actions based on a promise" pattern. If you need to dispatch actions in that pattern, createAsyncThunk is there to simplify it for you. If you don't need that pattern, then don't use createAsyncThunk.

I'll note that your couple thunk examples there are doing different things, so I'm not sure why you're trying to compare them as if they're identical.

@drydenwilliams
Copy link

drydenwilliams commented Mar 25, 2021

Thanks for the explanation, good clarification. Would there be a nice way to reuse someThunkCall? As what happens if I want to use it in multiple components?

@markerikson
Copy link
Collaborator

Sorry, really having trouble understanding what you're trying to describe here :( Can you give specific examples?

@drydenwilliams
Copy link

drydenwilliams commented Mar 25, 2021

Ok no worries, so let's say I had an async thunk like this:

// app/thunks
export const fetchSomeThunk = createAsyncThunk(
  'app/fetchSomeThunk',
  async (arg: IMyArg) => {
    return axios.get(url)
  }
)

and I wanted to use/call it in two components: ComponentA and ComponentB

// ComponentA.tsx and ComponentB.tsx
const someThunkCall = async () => {
    try {
      const someThunkAction = await dispatch(someThunk(''))
      unwrapResult(someThunkAction)
    } catch (error) {
      Bugsnag.notify(error)
    }
}

so I don't have to duplicate someThunkCall in both components, would I simply pass dispatch in to someThunkCall as an argument?

@markerikson
Copy link
Collaborator

I guess? This is ultimately you creating your own abstraction for additional logic in a component after dispatching.

@drydenwilliams
Copy link

drydenwilliams commented Mar 25, 2021

Hmm, this feel extremely standard to me. Have you've tried to build a real-world app with toolkit? (don't mean that in a rude way) but calling action creators from different components seems quite standard, or maybe I've been doing it wrong since working with redux (which there is a high chance)

@markerikson
Copy link
Collaborator

markerikson commented Mar 25, 2021

Yes, I've worked on some "real" apps. I've just never needed to spend much time checking thunk results in components, or sending off error logs to bug reporting services, etc.

My point is that what you're asking about here is really about your own abstractions, in your app. This isn't anything that is a bug, or requires API changes, or anything like that. It's your app, I don't know what your requirements are, and I don't know what use cases you're trying to solve. You're always welcome to come up with your own abstractions and use them. So, I'm not sure what advice you're looking for from me atm.

@drydenwilliams
Copy link

drydenwilliams commented Mar 25, 2021

That's ok, just trying to understand how to use it better in future 👍 no bug, thanks for the help; really appreciate it. P.s sorry for the "real" world comment, I didn't it to come across so rudely.

@phryneas
Copy link
Member

Adding to this, your example above could also be written:

// myComponent.tsx
const someThunkCall = async () => {
    try {
-      const someThunkAction = await dispatch(someThunk(''))
-      unwrapResult(someThunkAction)
+      dispatch(someThunk('')).then(unwrapResult)
    } catch (error) {
      Bugsnag.notify(error)
    }
}

in the next version of RTK we are probably adding a helper so it can also be written as

// myComponent.tsx
const someThunkCall = async () => {
    try {
-      dispatch(someThunk('')).then(unwrapResult)
+      dispatch(someThunk('')).unwrap()
    } catch (error) {
      Bugsnag.notify(error)
    }
}

which is even shorter

But in the end, it is your own decision to even do all this handling in your component. You could also just have a middleware:

const BugsnagMiddleware = api => next => action => {
  if (isRejectedWithValue(action)) {
    Bugsnag.notify(action.payload)
  } else if (isRejected(action)) {
    Bugsnag.notify(action.error)
  }
  return next(action)
}

From that point on all your components would become

// myComponent.tsx
const someThunkCall = () => dispatch(someThunk(''))

Doing this in the component is your decision in the first place and yes that will always mean more code.

@drydenwilliams
Copy link

drydenwilliams commented Mar 25, 2021

Sorry @markerikson two further questions.

  1. Is this a correct way to use something from state in createAsyncThunk:
export const usersLoad = createAsyncThunk(
  'users/load',
  (filters: UserFilters = {}, { getState }) => {
    const state = getState()
    return Api.get<User[]>('/api/users/search', {
      headers: {
        Authorization: `Bearer ${state.auth.accessToken}`,
      },
    })
  }
)
  1. what happens if usersLoad doesn't take a filter arg but you still wanted to use getState how do you do this? Would you just pass the part of the state you wanted as the first arg? As currently I get a Expected 0 arguments, but got 1 when I pass in undefined or null as the first arg.

@markerikson
Copy link
Collaborator

@drydenwilliams this issue really isn't a good place to provide support :( Please come by the #redux channel in the Reactiflux Discord if you have further questions.

@drydenwilliams
Copy link

drydenwilliams commented Mar 25, 2021

I've always found this issues a bit more helpful as this was the first result on Google, so I hope it can help others if they find it due to the history? Hope that's ok; I feel like you're support is awesome!

Thanks for your points @phryneas I like the idea of the middleware 👍

@phryneas
Copy link
Member

This should not be the first result on google if searching for that question, because it should never have been asked here - and by now is pretty much completely unrelated to the initial issue discussed in this bug tracker.

Instead, a StackOverflow thread with a useful title and a specific problem & solution should be the first google hit.
Which is only possible if that question is asked & answered over there instead of here.

@drydenwilliams
Copy link

drydenwilliams commented Mar 25, 2021

Not sure these points are "pretty much completely unrelated" as I can't seem to understand how to use "createAsyncThunk optional or no args" which is the issue title?

Sure that might be in the wrong place now and possibly "should never have been asked here" but will be helpful now to other having a similar issue now?

@phryneas
Copy link
Member

They are completely unrelated to the question if there is a way to define an asyncThunk without args or with optional args.

The only thing they have in common is that they are about createAsyncThunk.

@BeigeBadger
Copy link

For anyone who comes across this in the future, I solved the no argument requirement in my TypeScript application by using the _ discard operator as the first parameter to the payloadCreator function - whether this is correct or not is another matter.

An example is below:

export const getAllQuizzes = createAsyncThunk<QuizItem[]>(
    'quizzes/fetchAll',
    async (_, thunkAPI) => {
        thunkAPI.dispatch(pageStackLoading());

        const response = await fetch(`/api/quiz/all`);
        const jsonPayload = await response.json();

        thunkAPI.dispatch(pageStackReceived());

        // Will dispatch the `fulfilled` action
        return jsonPayload;
    }
)

@phryneas
Copy link
Member

Well, it is correct, but _ is not a discard operator. You are simply giving the first parameter the name _, which is a valid variable name in JavaScript.

@BeigeBadger
Copy link

Well, it is correct, but _ is not a discard operator. You are simply giving the first parameter the name _, which is a valid variable name in JavaScript.

Thanks for the correction @phryneas, I'm primarily a C# developer where _ is the discard operator, so I made an assumption there.

Can you please confirm that as in version 1.6.0 of reduxjs/toolkit that the approach I have outline above is the correct way to define a think that takes no arguments i.e. having an argument defined in the payloadCreator function that is unused, simply so that I can access methods such as dispatch via the second argument?

@phryneas
Copy link
Member

phryneas commented Jul 15, 2021

I already said it is correct :)
Arguments in JS are postitional - if you want to access the nth argument, you have to assign names to all argument that come before.
If you are typing the first argument of cAT as "no argument", use void. But that should be the default anyways.

@BeigeBadger
Copy link

Awesome, I will use the void type. Thanks for the quick replies @phryneas!

@REX500
Copy link

REX500 commented Oct 9, 2023

Seems to still be an issue with version 1.9.7 and Typescript version 5.2.2 @phryneas ...
I have a simple function to fetch entries:

type fetchEntries = {
	extended?: boolean
}
export const fetchEntries = createAsyncThunk(
	'entries',
	async (props?: fetchEntries) => {
		const response = await get(

and when I try to call it I get an error:

		dispatch(fetchEntries())  // Expected 1 arguments, but got 0

Workaround is to pass it an empty object {} like so:

		dispatch(fetchEntries({}))

This is not ideal and a fix would be very welcomed 🙌

Fix idea you proposed @phryneas:

const _usersLoad = createAsyncThunk(
	'users/load',
	(filters: UserFilters) => Api.get<User[]>('/api/users/search', filters)
);

export const usersLoad = (filters: UserFilters = {}) => _usersLoad(filters);

could very well work but a more rigid, "official" fix would be better imo.

@EskiMojo14
Copy link
Collaborator

@REX500 unfortunately this is a known bug with TS 5.1 and 5.2: #3758
It's fixed in 5.3.

As a workaround, you can get the proper inference by explicitly setting your parameter to props?: fetchEntries | undefined.

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

7 participants