Destructuring react hook results in: This expression is not callable - javascript

I'm creating a custom hook and want to return an object and two functions when the hook is called. I do not want to return it as return {body, setProperty, setBody}, since I might call the hook multiple times within the same component and need different names for the variables.
I'd want to call it just as useState where I can destructure it as an array const [firstBody, setFirstBodyProp, setFirstBody] = useJSONState({/*Some code*/}), but when I try to return it as such return [body, setProperty, setBody], I get the following error when calling it from a component:
This expression is not callable.
Type 'jsonType' has no call signatures.ts(2349)
My Code:
type jsonType = {
[property: string]: any
}
const useJSONState = (json: jsonType) => {
const [ body, setBody ] = useState(json)
function setProp(property: string, value: any){
let newBody = {...body}
newBody[property] = value
setBody(newBody)
}
return [body, setProp, setBody]
}
export default useJSONState

The reason for the error is TypeScript inference. Your hook is returning an array of body,setProp,setBody but TypeScript infers the type to be jsonType[], which is the type of body(the first element in the array). To solve this error, you have to specify the return type of the hook explicitly.
export const useJSONState = (
json: jsonType
): [
jsonType,
(property: string, value: any) => void,
React.Dispatch<React.SetStateAction<jsonType>>
] => {
// .....
return [body, setProp, setBody];
}

Related

React - pass prop that needs to be awaited for

Im trying to pass down from my component a prop that comes from an async function.
I have the following code:
export const Bar = (props: Props) => {
...
const getValue = async () => {
const { value } = await initValue();
return value;
}
...
return (
<Foo value={getValue()}/> //Error in this line
}
TS throws an error:
Type 'Promise' is missing the following properties from type 'Element': type, props, key ts(2739)
Blockquote
How can I achieve this?
No need to call getValue(), just send it as a function variable then call it where is needed
<Foo value={getValue}/>
In addition, in your case, the best way would be to set the value as a state and then send the value as props.

useQuery used in custom hook returns a response with type useQueryResults<unknown, unknown>

I have the following custom hook called useGetQuery
type Props = Parameters<typeof useQuery>;
const useGetQuery = (...params: Props) => {
const useQueryResults = useQuery(params);
useFocusEffect(
React.useCallback(() => {
useQueryResults.refetch();
}, [useQueryResults])
);
return useQueryResults;
};
Which I then call in useGetScreen like this:
export const useGetScreen = (screenId: string) => {
return useGetQuery(['getScreen', { screenId }], () => getScreen(screenId), { staleTime: 0 });
};
And useGetScreen is called like this
const { data: screen } = useGetScreen('vegetables');
My problem is that in this case, article has a type of unknown and I can't figure out why is this happening. My guess is that I have to somehow type what is being returned by useFocusRefetchQuery. Before I implemented the custom hook, Typescript automatically inferred the return type of useQuery
I don't recall typescript infering anything from the GQL request string, in my experience you need to provide useQuery the expected output and input (variables) types as mentioned here in details. The useQuery signature is as follow:
useQuery<TData = any, TVariables = OperationVariables>
In your case you would have to add both Props and your article type, say Article:
import { gql, QueryHookOptions, useQuery } from '#apollo/client';
const GET_ARTICLE_QUERY = gql`
query getArticle {
article: {
id,
articleId,
articleName,
}
}
`
interface ArticleContent {
__typename: "Article";
id: string;
articleId: string;
articleName: string;
}
interface Article {
article: ArticleContent | null;
}
interface ArticleInput {
articleId: string;
}
const useArticleQuery = (options: QueryHookOptions = {}) => {
return useQuery<Article, ArticleInput>(GET_ARTICLE_QUERY);
}
I think its also a good idea to keep the options (QueryHookOptions) in here so you can use your hook in different contexts.
You can use a tool like Graphql code generator to automatically generate the static types Article and ArticleInput for you (based on your schema and your JS gql requests).
If you want to have a generic hook for several useQuery that all look the same (for example they all use useFocusEffect), you will have to do something like this:
function useFocusRefetchQuery<TData, TVariables>(){
const useQueryResults = useQuery<TData, TVariables>();
// ...
}
This way you can use useFocusRefetchQuery instead of useQuery, but I really think you will have to pass both the input/output types, which means in the previous example you would have:
const useArticleQuery = (options: QueryHookOptions = {}) => {
return useFocusRefetchQuery<Article, ArticleInput>(GET_ARTICLE_QUERY);
}

How to pass "parameter" to react component

I have a quick question, I am attempting to refactor my code in a existing codebase, this codebase uses a render function, however I would like to replace this with a react functional component instead.
However I am getting a error when I am passing in a parameter called "searchResults" which is a list.
Here is the previous non-refactored code
searchResults.sort((a, b) => new Date(b[0].created) - new Date(a[0].created));
const Rows = [];
searchResults.forEach((appResults, i) => {
Rows.push({
cells: [
appResults[0].applicationId,
<>{renderSuccessCount(appResults)}</>,
prettifyDate(appResults[0].created)
],
isOpen: false
});
This is my refactored code
const newRows = [];
searchResults.forEach((appResults, i) => {
newRows.push({
cells: [
appResults[0].applicationId,
<SuccessCount>{appResults}={appResults}</SuccessCount>,
prettifyDate(appResults[0].created)
],
isOpen: false
});
This is the function SuccessCount (in the non refactored code, renderSuccessCount was just renamed to SuccessCount in the refactored version
const SuccessCount = (appResults: AppResult[]) => {
const nSuccesses = appResults.filter((result: AppResult) => result.pulseStatus === SUCCESS).length;
return Something;
};
My question is relatively simple how do I pass appResults into my functional component, my current refactored code gives me this error
TS2740: Type '{ children: any[]; }' is missing the following properties from type 'AppResult[]': length, pop, push, concat, and 26 more.
This is in the linter, how do I get rid of it?
Now that SuccessCount is not a regular function but a react functional component, it'll not receive regular arguments. You'll have to access them via props
const SuccessCount = ({appResults}: {appResults: AppResult[]}) => {
Same as
const SuccessCount = (props) => {
const {appResults} = props; // or const appResults = props.appResults;
}
Usage as functional component
<SuccessCount appResults={appResults} />

Typescript React createContext and useState

I'm new to TS and trying to update code that I inherited from a former employee. I know it's TS related and have tried different types not able to resolve it. Any assistance would be great. Thanks in advance.
Here's the code:
import React from 'react';
export interface HistoryType {
history?: number;
setHistory: (value: number) => void;
}
const HistoryContext = React.createContext<HistoryType | undefined>(undefined);
export const HistoryProvider: React.FC = ({ children }) => {
const [history, setHistory] = React.useState();
return (
<HistoryContext.Provider value={{ history, setHistory}}>
{children}
</HistoryContext.Provider>
);
};
export const useHistoryState = () => {
const context = React.useContext(HistoryContext);
if (context === undefined) {
throw new Error('useHistoryState error');
}
return context;
};
Screenshot of the error:
const [history, setHistory] = React.useState()
Since no type is specified here, typescript has to try to infer it. It sees that undefined is implicitly being passed to use state, so it assumes the state is (and always will be) undefined. So that means that setHistory expects to be passed undefined.
You will need to specify the type yourself to say that it can either be a number, or undefined:
const [history, setHistory] = React.useState<number | undefined>();
Or if it's always supposed to be a number, you can do that too, but you'll need to provide an initial value which is a number:
const [history, setHistory] = React.useState<number>(42);
P.S: This is unrelated to your question, but i notice this code is employing a common mistake: You're creating a brand new object on every render and passing it as the context value.
value={{ history, setHistory }}>
Since there's a new value on every render, it will force anything that's consuming the context to rerender even if history and setHistory did not change. To fix this you should memoize the value so it only gets recomputed when it actually changes:
const value = React.useMemo(() => {
return { history, setHistory };
}, [history]);
return (
<HistoryContext.Provider value={value}>
{children}
</HistoryContext.Provider>
);

Sort an array in React useEffect() with Typescript

I'm setting an array of movie objects in useEffect and want them sorted alphabetically. If possible later I want to add the functionality of sorting them by different properties but for now I just want to get over this TypeScript hump. I have a separate types file where I export my movie interface:
export interface Movie {
comments: string,
imdb: string,
mtc: string,
rtc: string,
smn: string,
title: string
}
Which I then import in my page
import { Movie } from '../types';
I call useEffect, request the list of movies from my MongoDB and then want to sort them.
const Database = () => {
const [movies, setMovies] = React.useState<Movie[]>([]);
const [sortType, setSortType] = React.useState('title');
const history = useHistory();
React.useEffect(() => {
Requests.getMovies()
.then(setMovies)
.catch (err => console.log(err));
const types: Movie = {
title: 'title',
rtc: 'rtc',
imdb: 'imdb',
smn: 'smn',
mtc: 'mtc',
comments: 'comments'
};
const sortArray = (sortProperty: string): sortProperty is keyof typeof types => {
const sorted = [...movies].sort((a: Movie, b: Movie) => b[sortProperty] - a[sortProperty] );
return sorted;
}
sortArray(sortType);
setMovies(sortArray);
}, [sortType])
return (
// Bunch of stuff on the page and then map out the movies like so..
<tbody>
{movies.map((movie, index) => (
<tr key={index} onClick={() => history.push(`/edit/${movie.title}`)}>
<td>{movie.title}</td>
<td>{movie.imdb}</td>
<td>{movie.rtc}</td>
<td>{movie.mtc}</td>
<td>{movie.smn}</td>
<td>{movie.comments}</td>
</tr>
))}
</tbody>
//Bunch of other stuff on the page..
)
}
export default Database;
I get the list of movies to show up no problem but sorting them is hard. I found this and this and this but TypeScript keeps giving me errors. On the return sorted...
Type 'Movie[]' is not assignable to type 'boolean'.
..and on the b[sortProperty] and a[sortProperty].
Element implicitly has an 'any' type because expression of type 'string | number | symbol' can't be used to index type 'Movie'.
No index signature with a parameter of type 'string' was found on type 'Movie'.
How do I get this to work? I thought sort returned the sorted array, but it returns a boolean? Why can't it sort by a property that is defined in the object and also in the interface?
ok here we go, i'll try to explain everything as best as i could.
first of all, you need to split your useEffect into 2 useEffect statement. useEffect hook should only handle one thing.
so your code would look like this:
React.useEffect(() => {
Requests.getMovies()
.then(setMovies)
.catch (err => console.log(err));
), []}
React.useEffect(() => {
// your sorting code
}, [/* dependencies */])
the reasoning behind this is, you don't want unneeded action to trigger for every change in your depencies, i.e. you don't want to fetch each time a dependencies change but only when your component mounts.
as far as the issues with your code, the errors have 2 reasons:
trying to do an arithmetic operation on 2 variables of type string
trying to read property of an object using variable of type string while your object has no generic keys
also do a change operation through a useEffect hook is not the most optimal solution, because you'll need the movies state in your dependencies so you get the latest state, but if you do you'll trigger an infinite loop.
the way i do it, is sort once on component mount using the fetched array and then use an event handler function to trigger to subsequent sorts. this way you would avoid any unintended side effects.
so your code would look like this:
const [movies, setMovies] = React.useState<Movie[]>([]);
const [sortType, setSortType] = React.useState<keyof Movie>("title");
React.useEffect(() => {
Requests.getMovies()
.then(sortMovies)
.catch (err => console.log(err));
), []}
const sortMovies = (moviesParam: Movie[] = movies) => {
const sortArray = [...moviesParam].sort((a: Movie, b: Movie) => {
if (a[sortType] > b[sortType]) return 1;
else if (b[sortType] > a[sortType]) return -1;
return 0;
});
setMovies(sortArray);
}
this code does sort your array alphabetically, and does not cause infinite loop.
also the sortArray function is not actually needed since you actually using the state anyway so it does not add any value to the code.
if you require further explanation on how and why i did those changes please add a comment and i'll update the post.

Categories

Resources