How to correctly set react useState using generics - reactjs

Core question - where am I messing up my useState
const [data, setData] = useState<IAssetGetManyResponseDto<IAssetResponseDto>>();
so that the data object I'm sending down as props is undefined?
My (functioning) api call looks like this
export const getAllAssets = async (
): Promise<IAssetGetManyResponseDto<IAssetResponseDto>> => {
console.log("get all assets ran in api.ts!")
const response = await amsApiClient.get(ASSET_ENDPOINT(),
{
params: {
limit: 1000
}
}
);
console.log("logging response data "+JSON.stringify(response.data))
return response.data;
};
Here are the types being used:
export interface IAssetGetManyResponseDto<T> {
items: T[];
totalCount: number;
}
export interface IAssetResponseDto {
id: string | null;
name: string | null;
type: string | null;
url: string | null;
// metadata: IMetadataObj| null;
tags: Array<string> | null;
thumbnailImageURL: string | null;
createdAt: number | null;
updatedAt: number | null;
}
And here is the component that makes the call, to show context
export const AssetManagementTable = () => {
const [data, setData] = useState<IAssetGetManyResponseDto<IAssetResponseDto>>();
const getAssets = async () => {
console.log("getAssets ran!")
const assets = await getAllAssets();
console.log("logging data from asset managment table " + data)
console.log("logging assets from asset managment table " + assets)
setData(assets)
}
useEffect(() => {
getAssets()
}, []);
return (
<div>
<NewAssetTable items={data} />
</div>
);
};
Elsewhere in my app, when I pass props down and refer to props.items.map((item: any). => {... I'm getting undefined.
More specifically, TypeError: Cannot read properties of undefined (reading 'map')
Am I messing up the argument to useState by improperly specifying types?
Thanks!

Looks like type definitions for React.useState are little unsafe. They don't augment return type with undefined for variant with no arguments.
So, if you told that state will always contain IAssetGetManyResponseDto<IAssetResponseDto>, TS compiler believes it.
Hover on data to see it has IAssetGetManyResponseDto<IAssetResponseDto> while it should be IAssetGetManyResponseDto<IAssetResponseDto> | undefined.
Workaround:
useState<IAssetGetManyResponseDto<IAssetResponseDto> | undefined>()
useState<IAssetGetManyResponseDto<IAssetResponseDto>>(someFunctionalMockOfYourData)

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/

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

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.

How can I create an interface for API data in typescript?

So I'm getting data from TMDb API(movies) and my App.js looks like this:
const App = () => {
const [movies, setMovies] = useState<MovieType[]>([]); //state for setting movies to api data
useEffect(() =>{
fetchMovies();
}, [])
async function fetchMovies() { //function for fetching data
try{
let apikey = '{api_key}';
let url: string = 'https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=';
url = url + apikey;
const response = await axios.get<MovieResults[]>(url);
setMovies(response.data.results); //and here I get an error: Property 'results' does not exist on type 'MovieResults[]'
}catch(e){
alert(e);
console.log(movies);
}
}
return (
<div className="App">
<Header/>
<Hero movies={movies}/>
</div>
);
}
My types and interfaces:
export type MovieType = {
vote_average: string,
title: string,
tagline: string,
date: string,
img: File,
};
export type MovieResults = {
results: MovieType[],
};
export interface MovieProps {
movies: MovieType[],
};
As you can see, I have types and interfaces, I created MovieType for passing props, and MovieResults for getting results from API. But I have an error when I try to set movies to api data: Property 'results' does not exist on type 'MovieResults[]'
It's weird, because I actually have "results" property in MovieResults.
I think you meant to do axios.get<MovieResults>. If you do axios.get<MovieResults[]>, it will expect an array of MovieResults which I think is not what you want.
If you use MovieResults[] as type, you are basically expecting the data to be the following
[
{ results: ... },
{ results: ... },
{ results: ... },
.
.
.
]
So, you are trying to read results from an array, which is not defined.
But, if you use MovieResults as type, the format of data will be the following
{
results: ...
}
Here, you are reading results from an object.

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.

Error when destructuring values from useContext in TypeScript

I'm a bit new to TypeScript, but I LOVE it.
When I try to destructure my Context in React using useContext I get some errors that I don't know why I'm getting.
The error: Property 'nftContract' does not exist on type 'Web3 | null'.ts(2339)
Context File
export interface Web3 {
gameContract: Contract | string;
nftContract: Contract | string;
provider: Provider | string;
account: string;
etherBalance: string;
setWeb3?: React.Dispatch<React.SetStateAction<Web3>>;
}
export const Web3Context = createContext<Web3 | null>(null);
export const Web3Provider: React.FC = ({ children }) => {
const [
{ gameContract, nftContract, provider, account, etherBalance },
setWeb3
] = useState<Web3>({
gameContract: '',
nftContract: '',
provider: '',
account: '',
etherBalance: ''
});
return (
<Web3Context.Provider
value={{
provider,
gameContract,
nftContract,
account,
etherBalance,
setWeb3
}}
>
{children}
</Web3Context.Provider>
);
File consuming Context (Context has been wrapped properly in the main index/app file.)
const MintNFTForm = () => {
// Property 'nftContract' does not exist on type 'Web3 | null'.ts(2339)
const { nftContract } = useContext(Web3Context);
}
Anything helps,thank you!
Destructure the value only after it has been checked for not being null:
const MintNFTForm = () => {
const ctxt = useContext(Web3Context);
if (ctxt == null) return <div>No context yet</div>;
// `ctxt` is now guaranteed to not be `null`, safe to destructure
const { nftContract } = ctxt;
}
The code is initializing the context's value to null and declaring that its type can be null so any code referencing it must account for that case:
export const Web3Context = createContext<Web3 | null>(null);

Resources