RTK Query UseLazyQuery and Typescript: how to get the correct type from hook argument - reactjs

I need to create a react hook that accepts a RTK Query UseLazyQuery function (from any endpoint), apply a debounce to it and return the trigger function and results.
Everything is working as expected but I can't manage to get the correct type to the data returned by it.
As "UseLazyQuery" type requires a type for its generic type and I don't know what type it'll be, I set it to "any", but it makes the data to be typed as "any" too.
I need help to get/extract the correct type from the given "UseLazyQuery" function given to the hook and make the returned data have the correct type.
export default function useSearch<T extends UseLazyQuery<any>>(lazyQueryFn: T) {
const [ trigger, results ] = lazyQueryFn()
const getParamsAndTrigger = async (
params: ISearchParamsRequest,
filterParams?: (params: ISearchParamsRequest) => ISearchParamsRequest
) => {
await trigger(filterParams ? filterParams(params) : params, true)
}
const debouncedTrigger = useDebouncedFn(getParamsAndTrigger, 1500)
return [debouncedTrigger, { ...results }] as const
}
The "results" const returned by "lazyQueryFn" has the type "UseQueryStateDefaultResult". I need it to have the right data type.

I had a similar situation this days and I came with this approach which fixed it and should help you too:
interface ResponseData<V> {
items: V[];
cursor: string;
}
export const useGetCursorPaginatedData = <
V,
T extends UseLazyQuery<QueryDefinition<any, any, any, ResponseData<V>, any>>
>(
lazyQuery: T
) => {
const [trigger] = lazyQuery();
const { onTriggerAction, isLoading, error } = useAsyncAction();
const [items, setItems] = useState<V[]>([]);
const [newCursor, setNewCursor] = useState<string | undefined>();
const getData = async (isRefresh?: boolean) => {
const tempList = isRefresh ? [] : items;
const { data } = await trigger(isRefresh ? undefined : newCursor);
setItems([...tempList, ...data?.items]);
setNewCursor(data?.cursor);
};
const onLoadMore = useCallback(() => {
getData();
}, [newCursor]);
const onRefresh = () => {
onTriggerAction(async () => getData(true));
};
useEffect(() => {
onTriggerAction(getData);
}, []);
return {
items,
isLoading,
isError: error != null,
onLoadMore,
onRefresh
};
};
And this is how to use it:
const {
items: dailyTweets,
isLoading,
isError,
onLoadMore,
onRefresh
} = useGetCursorPaginatedData<DailyTweet, typeof useLazyGetDailyTweetsQuery>(
useLazyGetDailyTweetsQuery
);
Basically what you need extra is to define an interface for the QueryDefinition Response, in my case ResponseData and add one more generic type to function definition that is the type of the returned data in your response.
I hope this will help you.

Related

How can I pass data from custom hook to component?

I have custom hook which pass parameter data with fetched data to component Settings. There I have hook setData11 in useEffect and I want to set in that hook data passed from useTable but I get error
Argument of type 'Data | Column[] | undefined' is not assignable to parameter of type 'SetStateAction<DataTable[] | undefined>'.
Type 'Data' is not assignable to type 'SetStateAction<DataTable[] | undefined>'.
Type 'Data' is missing the following properties from type 'DataTable[]': length, pop, push, concat, and 29 more.ts(2345)
interface DataTable {
[id: string | number]: string | number;
}
interface Data {
[category: string]: DataTable[];
}
const urls: string[] = [
"https://jsonplaceholder.typicode.com/posts",
"https://jsonplaceholder.typicode.com/comments",
"https://jsonplaceholder.typicode.com/albums",
"https://jsonplaceholder.typicode.com/photos",
"https://jsonplaceholder.typicode.com/todos",
];
const useTable = (idurl: number, actualcategory: string) => {
const [data, setData] = useState<Data>();
const [columns, setColumns] = useState<Column[]>();
const loadDatabase = () => {
fetch(urls[idurl])
.then((response) => response.json())
.then((response) => {
setData({
...data,
new: response.filter((t: DataTable[], i: number) => {
return i > 50 && i < 100 && t;
}),
removed: response.filter((t: DataTable[], i: number) => {
return i > 100 && i < 150 && t;
}),
postponed: response.filter((t: DataTable[], i: number) => {
return i > 50 && i < 100 && t;
}),
});
const objcolumn: Column[] = Object.keys(response[0]).map(
(t: string) => {
let d: Column = { col: { title: t, disp: true } };
return d;
}
);
setColumns(objcolumn);
});
};
useEffect(() => {
loadDatabase();
alert(JSON.stringify(data));
}, []);
return [data, columns];
};
export { useTable };
const Settings = () => {
const [data, columns, checkall, changeDatabase] = useTable(1, "new");
const[ data1, setData1]=useState<DataTable[]>()
useEffect(() => {
setData1(data)
}, []);
return <></>
}
It's not clear to me what this code is supposed to do, so it's difficult to give you a specific answer.
Your example contains several mistakes.
The first one can be confusing. When you return an array from your custom hook, its type will be inferred as Array<Data | Column[]>. If you want to get a signature similar to useState, you should use as const to tell TypeScript that this array contains a specific number of elements in a specific order and cannot change.
But even after you specify as const, TypeScript will complain about two things:
The number of elements that you are trying to unpack from useTable should correspond to the two that you are returning (possibly just a typo). So it's should be const [data, columns] = useTable(...)
The type Data | undefined and DataTable[] | undefined are incompatible, which is entirely true. I'm not entirely sure what your end goal is. If you want to use a specific category as the data1 state, you should pass some attribute of data
const useTable = (idurl: number, actualcategory: string) => {
...
return [data, columns] as const; // use "as const"
};
// --------------
const Settings = () => {
// Remove extra parameters
const [data, columns] = useTable(1, "new");
const [data1, setData1] = useState<DataTable[]>()
useEffect(() => {
// data also can be undefined, so you need to use ?. syntax
setData1(data?.SOME_CATEGORY)
}, []);
return <></>
}
thanks for answer :)
here is resolution of the problem
https://fettblog.eu/typescript-react-typeing-custom-hooks/

rect-query: Pass additional key for navigation

I am trying to pass a key for navigation, which specifies to show the query/page after the current query call.
useform.ts
...
...
export const useupdateSurveyForm = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateSurveyForm,
onSuccess: (data) => {
queryClient.invalidateQueries(["searched-public-survey"]);
},
});
};
here added "invalidateQueries(["searched-public-survey"]" directly then the code is working properly, but I want to make this dynamic. like,
queryClient.invalidateQueries(navigationqueryKey);
For that I made some changes
plans.ts
...
...
const {
mutate: updateArchiveSurveyStatus,
navigationqueryKey: "searched-public-survey",
} = useupdateSurveyForm();
...
...
pass "navigationKey: "searched-public-survey", but it shows an error
Property 'searched-public-survey' does not exist on type 'UseMutationResult<any, unknown, SurveyUpdatePayload, unknown>'.
Give me some solution to fix this problem.
I dont know if it is correct to pass the value for the query that way.
The statement from the plain.ts is just the return value. I did not found anything in the docs which would lead to putting in the navigation query key.
If I get you correctly I think what you want to do would look like:
export const useupdateSurveyForm = (key: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateSurveyForm,
onSuccess: (data) => {
queryClient.invalidateQueries([key]);
},
});
};
And the plans.ts would then look like:
const {
mutate: updateArchiveSurveyStatus,
} = useupdateSurveyForm("searched-public-survey");
Maybe that helps. :-)

How to check "Type" user passed in custom hook in React typescript?

I am creating a custom hook to fetch json from api. I am using this hook for different type of data. So i need to set the type whenever i use this hook. Based on the type I use hook should parse data and return the parsed data which should contain data of our desired type.
const useSearch = <T>(url: string) => {
const [data, setData] = useState<T[]>([])
const fetchData = React.useCallback(
(searchText: string): void => {
fetch(url).then(res=>res.json()).then(({ data }) => {
//check if T is Type1, Type2, Type3
// all types parse their data using different functions
//I need to check the type of T
setData(parseData(data))
}).catch(error z=> {
console.log(error)
})
},
[],
)
return [data, fetchData] as const
}
But I am unable to find a way to check the type of T and use the corresponding parsing function.
You could try using type predicates:
type MyDataType = {
foo: 'bar'
}
type MyOtherDataType = {
baz: 'biz'
}
function isMyDataType(obj: MyDataType | MyOtherDataType): obj is MyDataType {
return (obj as MyDataType).foo !== undefined;
}
function isMyOtherDataType(obj: MyDataType | MyOtherDataType): obj is MyOtherDataType {
return (obj as MyOtherDataType).baz !== undefined;
}
// Now your hook...
const useSearch = <T>(url: string) => {
const [data, setData] = useState<T[]>([])
const fetchData = React.useCallback(
(searchText: string): void => {
fetch(url).then(res=>res.json()).then(({ data }) => {
if (isMyDataType(data)) {
// TS will treat data as MyDataType
myUniqueDataHandler(data);
} else if (isMyOtherDataType(data)) {
// TS will treat data as MyOtherDataType
myOtherUniqueDataHandler(data);
}
setData(parseData(data))
}).catch(error z=> {
console.log(error)
})
},
[],
)
return [data, fetchData] as const
}
Alternatively, you could add a "type" field to the data objects that are returned by your API and have an easy check at runtime. Type predicates have the advantage of being type safe (or, at least, as type safe as your predicates); in other words, TS will treat your data as the type you've narrowed to AND Javascript will perform the same check at runtime so you can run your unique handlers.

ContextApi, useReducer, & Typescript - calculated value is not accessible on component

Apologies for the somewhat opaque title, but I am having difficulties being more precise here.
So I have a Context/Reducer Logic, where I initialise the context with some values. I then have a reducer Logic on a custom Provider and use useMemo to calculate values. When trying to access one on of those values (that isn't in the state/initialState) on a component typescript gets angry at me and tells me that said value does not exist on State. What is the best way to remedy this warning?
I have the following definition of a Context/Reducer.
interface State {
displaySidebar: boolean
}
const initialState = {
displaySidebar: false
}
type Action =
| {
type: 'OPEN_SIDEBAR'
}
| {
type: 'CLOSE_SIDEBAR'
}
const UIContext = React.createContext<State>(initialState)
UIContext.displayName = 'UIContext'
const uiReducer = (state: State, action: Action): State => {
switch (action.type) {
case 'OPEN_SIDEBAR': {
return {
...state,
displaySidebar: true,
}
}
case 'CLOSE_SIDEBAR': {
return {
...state,
displaySidebar: false,
}
}
}
}
const UIProvider: FC = (props) => {
const [state, dispatch] = React.useReducer(uiReducer, initialState)
const openSidebar = (): void => dispatch({ type: 'OPEN_SIDEBAR' })
const closeSidebar = (): void => dispatch({ type: 'CLOSE_SIDEBAR' })
const value = useMemo(
() => ({
...state,
openSidebar,
closeSidebar,
}),
[state]
)
return <UIContext.Provider value={value} {...props} />
}
export const useUI = () => {
const context = React.useContext(UIContext)
if (context === undefined) {
throw new Error(`useUI must be used within a UIProvider`)
}
return context
}
export const ManagedUIContext: FC = ({ children }) => (
<UIProvider>
<ThemeProvider>{children}</ThemeProvider>
</UIProvider>
)
now when I try to use const {closeSidebar} = useUI() in a component typescript gets angry with me and tells me that Property 'closeSidebar' does not exist on type 'State'. I get that, but I was not able to figure out how to properly add closeSidebar to the React.Context type.
When you create context you tell TS that its type will be State, so it doesn't expect anything else to be there. If you want to add additional fields you can create an intersection type, state + methods, either as a named type of just React.createContext<State & {openSidebar : ()=> void, closeSidebar: ()=> void}>. Note that as your initial state doesn't have methods you either need to make them optional or provide some sort of dummy versions.

How to specify generic type of a function used in useMemo as argument?

I have generic function that is selector creator used by multiple components (according to https://react-redux.js.org/api/hooks#using-memoizing-selectors) as follows:
const makeCharSelector = <Subscriptions extends UseCharSubscriptions>() =>
createSelector<[RootStore, string[]], SelectorState<Subscriptions>>(
[
(state: RootStore) => ({
hasChar: state.user.checked && state.user.authenticated && state.user.data.hasChar,
initialized: state.char.meta.initialized,
}),
(state: RootStore, subscriptions: string[]): CharSelectorState<Subscriptions> => {
const subs = castArray(subscriptions);
return zipObjectDeep(subs, at(state.char, subs)) as CharSelectorState<Subscriptions>;
},
{
path: 1,
argIndex: 2,
},
],
(basic: SelectorState<Subscriptions>, subscriptions: CharSelectorState<Subscriptions>) => {
basic.char = subscriptions;
return basic;
}
);
and then inside a custom hook I am using it like this:
export function useChar<Subscriptions extends UseCharSubscriptions = DefaultSubscriptionsList>(
subscriptions: string[] = defaultSubscriptions
): CharSelectorState<Subscriptions> | null {
const selector = useMemo(makeCharSelector, []);
const state = useSelector((state: RootStore) => selector(state, subscriptions));
but then it wrong infers makeCharSelector Subscriptions generic so I would like to specify it manually by using component Subscriptions.
The problem is that I can't write it like this:
useMemo(makeCharSelector<Subscriptions>, [])
neither by using useMemo generic because it only specifies return type.
So the question is how to properly pass Subscriptions generic from useChar hook to makeCharSelector function?

Resources