-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
RTKQ feature request: allow auto-binding endpoint parameters from store #4371
Comments
I assume that endpoint paremeter is not always an object though, so maybe the signature would have to be something like function bindParamsFromStore<OuterParams>(getState: GetAppState, callSiteParams: OuterParams): EndpointParam | SkipToken forcing the implementator to provide This would also make the api more powerful (allowing selectors to react to provided params, and allow for auto-selecting nested properties), and would allow to skip when params are not yet loaded in store. |
part of the trouble (at least Typescript wise) is that because your RootState type is partially derived from the API definition, using that RootState type anywhere can result in circular type issues it would also complicate the types a lot, i suspect - having to keep track separately the "full endpoint arg" type, the "derived from state arg" type, and the "provided at call site arg" type |
That is true, but some parts of API definition callbacks already do have access to RootState (for example
Yeah, intuitively this sounds very painful to me, but I'm not familiar with the codebase, so I'm hoping for the best :) Also, based on my previous comment, only "full endpoint arg" and "provided at call site arg" would have to be tracked. This might be much easier, since there would be no particular relation to be codified between those two types. |
Hmm, thinking about this a bit more, I might be able to create an outer wrapper layer which would do the arg bindings for me - something that would roughly look like this: const createEndpointHelper = <CallSiteArgs, Endpoint extends EndpointDefinition>(
endpoint: Endpoint,
transformArgs: (state: RootState, callSiteArgs: CallSiteArgs) => ExtractArgs<Endpoint>
) => {
return {
initiate: (args: CallSiteArgs) => (dispatch, getState) => {
dispatch(endpoint.utils.initiate(transformArgs(getState(), callSiteArgs)))
},
select: (args: CallSiteArgs) => (dispatch, getState) => {
dispatch(endpoint.utils.select(transformArgs(getState(), callSiteArgs)))
},
useQuery: (args: CallSiteArgs, options) => {
const args = useSelector(state => transformArgs(state, callSiteArgs))
return endpoint.useQuery(args, options)
},
useMutation: options => {
const [nativeTrigger, nativeResult] = endpoint.useMutation(options)
return [
// Not really sure how to access current state here, though.
// I guess I might smuggle it outside of a thunk ...
// const dispatch = useDispatch()
// const getState = dispatch((dispatch, getState) => getState)
(param: CallSiteArgs) => nativeTrigger(transformArgs(getState(), param)),
nativeResult
] as const
}
}
} I would just have to change my current code a bit to always trigger endpoints actions from my helpers ... thoughts? Would this complicate some other aspects of working with rtkq I am missing? Off the top of my head, it doesn't seem to be destructive to my current workflow in any way. |
Ok, so I tried to implement my concept from previous comment, and it seems to works quite well so far. But I really struggled for many hours trying to get they types to work. I didn't manage to get the inference of helper method return types to work - for exmaple for the return of So I brute-forced typing all of the returns explicitly, and at this point they seems to be quite convincing (although probably incomplete, even for the implemented methods). But it resulted in some very ugly code imo (a lot of brute-force type assertions, and the readability-to-functionality ratio seems atrocious to me): CODE HEREimport { useDispatch, useSelector } from 'react-redux'
import { ApiEndpointMutation, ApiEndpointQuery } from '@reduxjs/toolkit/dist/query/core/module'
import {
MutationHooks,
QueryHooks,
UseMutation,
UseMutationStateOptions,
UseQuery,
UseQueryStateOptions
} from '@reduxjs/toolkit/dist/query/react/buildHooks'
import { RootState } from '../../reducers'
import { FeedbotDispatch } from '../../store/types'
import { QueryResultSelectorResult } from '@reduxjs/toolkit/dist/query/core/buildSelectors'
import {
MutationActionCreatorResult,
QueryActionCreatorResult
} from '@reduxjs/toolkit/dist/query/core/buildInitiate'
import type { MutationSubState, RequestStatusFlags } from '@reduxjs/toolkit/src/query/core/apiState'
type AnyEndpoint = ApiEndpointQuery<any, any> | ApiEndpointMutation<any, any>
type EndpointArgs<Endpoint extends AnyEndpoint> = Parameters<Endpoint['initiate']>[0]
// I didn't get it to work without re-defining this type from scratch - likely a skill issue
type UseMutationResult<Endpoint extends AnyEndpoint> = MutationSubState<
Endpoint extends ApiEndpointQuery<infer Definition, any> ? Definition : never
> &
RequestStatusFlags & {
originalArgs?: EndpointArgs<Endpoint>
reset: () => void
}
export const createEndpointHelper = {
query: <CallSiteArgs, Endpoint extends ApiEndpointQuery<any, any> & QueryHooks<any>>(
endpoint: Endpoint,
skipCondition: (args: EndpointArgs<Endpoint>) => boolean,
transformArgs: (state: RootState, callSiteArgs: CallSiteArgs) => EndpointArgs<Endpoint>
) => {
type Definition = Endpoint extends ApiEndpointQuery<infer Definition, any> ? Definition : never
return {
initiate: (
callSiteArgs: CallSiteArgs
): ThunkAction<QueryActionCreatorResult<Definition>, any, any, AnyAction> => {
return async (dispatch: FeedbotDispatch, getState: () => RootState): any => {
const args = transformArgs(getState(), callSiteArgs)
if (skipCondition(args)) {
return
}
return dispatch(endpoint.initiate(args))
}
},
select: (callSiteArgs: CallSiteArgs) => {
return (state: RootState) => {
const args = transformArgs(state, callSiteArgs)
return endpoint.select(args)(state as any) as QueryResultSelectorResult<Definition>
}
},
useQuery: (callSiteArgs: CallSiteArgs, options?: UseQueryStateOptions<any, any>) => {
const args = useSelector(state => transformArgs(state as any, callSiteArgs))
const useQuery = endpoint.useQuery as UseQuery<Definition>
// @ts-ignore
return useQuery(args, { skip: skipCondition(args) })
},
useData: (callSiteArgs: CallSiteArgs, options?: UseQueryStateOptions<any, any>) => {
const args = useSelector(state => transformArgs(state as any, callSiteArgs))
const useQuery = endpoint.useQuery as UseQuery<Definition>
// @ts-ignore
return useQuery(args, { skip: skipCondition(args) })?.data
}
}
},
mutation: <CallSiteArgs, Endpoint extends ApiEndpointMutation<any, any> & MutationHooks<any>>(
endpoint: Endpoint,
skipCondition: (args: EndpointArgs<Endpoint>) => boolean,
transformArgs: (state: RootState, callSiteArgs: CallSiteArgs) => EndpointArgs<Endpoint>
) => {
type Definition = Endpoint extends ApiEndpointMutation<infer Definition, any>
? Definition
: never
return {
initiate: (
callSiteArgs: CallSiteArgs
): ThunkAction<MutationActionCreatorResult<Definition>, any, any, AnyAction> => {
return async (dispatch: FeedbotDispatch, getState: () => RootState): any => {
const args = transformArgs(getState(), callSiteArgs)
if (skipCondition(args)) {
return
}
return dispatch(endpoint.initiate(args))
}
},
useMutation: (options?: UseMutationStateOptions<Definition, any>) => {
const dispatch = useDispatch()
const useMutation = endpoint.useMutation as UseMutation<Definition>
const [nativeTrigger, nativeResult] = useMutation(options)
return [
(param: CallSiteArgs) => {
// @ts-ignore
const getState: () => RootState = dispatch((dispatch, getState) => getState)
const args = transformArgs(getState(), param)
nativeTrigger(args as any)
},
nativeResult as UseMutationResult<Endpoint>
] as const
}
}
}
}
// skip utils
export const skipWhenSomeFalsy =
<O extends Object>(...trackedParams: (keyof O)[]) =>
(obj: O): boolean => {
return trackedParams.some(param => !obj[param])
}
export const neverSkip = () => false
// Args transformers
export const doNotInject =
<T>() =>
(state: RootState, value: T) =>
value And I define my endpoint helpers like this (quite like this syntax): const getBotMetadataQueryEndpointHelper = createEndpointHelper.query(
botsApi.endpoints.getBotsMetadata,
neverSkip,
doNotInject<void>()
)
const originalVersionQueryHelper = createEndpointHelper.query(
botsApi.endpoints.getBotVersion,
skipWhenSomeFalsy('id', 'version'),
(state, callSiteArgs: void) => {
return {
id: openBotIdSelector(state)!,
version: originalVersionStringSelector(state)!
}
}
) Any ideas about how to improve the typings are very welcome! |
A vast majority of my queries and mutations contain at least one parameter which maps directly to information already present in redux store, and could be selected by a static selector. I tend to then make hook and thunk wrappers which bind the parameters for me - for example:
And often times analogous thunk+selector wrappers for the same endpoints when I need to work with
.select()
and.initiate()
apis. This creates a lot of boilerplate mixed within more meaningful logic.I would love to have access to an api which would allow me to provide store bindings for a subset of the parameters at endpoint level, so I don't have to manually bind those paremeters up to 3 times for each endpoint (useEndpoint hook + selector + thunk) - something like:
It would help me improve my DX by eliminating all those single-purpose wrappers (injecting parameters to
useXyzMutation
result gets especially verbose and ugly for what it does).Would you please consider this? Or am I missing some already existing solution to my problem?
Thank you 🙏
The text was updated successfully, but these errors were encountered: