TypeScript React createContext() default value - reactjs

I been kinda stumped for a couple hours trying to figure out what to set as my default value on my createContext function. This is my code.
// PetTypeProvider.tsx
import { useState, createContext, useContext } from 'react';
const PetTypeContext = createContext('');
const UpdatePetTypeContext = createContext((event:React.MouseEvent<HTMLElement>) => event);
export function usePetType() {
return useContext(PetTypeContext)
}
export function useUpdatePetType() {
return useContext(UpdatePetTypeContext)
}
interface PetTypeProviderProps {
children: JSX.Element|JSX.Element[];
}
export const PetTypeProvider: React.FC<PetTypeProviderProps> = ({children}) => {
const [petType, setPetType] = useState('Dogs');
const togglePet = (event:React.MouseEvent<HTMLElement>) => setPetType(event.currentTarget.innerText);
return (
<PetTypeContext.Provider value={petType}>
<UpdatePetTypeContext.Provider value={togglePet}>
{children}
</UpdatePetTypeContext.Provider>
</PetTypeContext.Provider>
);
};
On my <UpdatePetTypeContext.Provider> I set the value to my toggle function, which switches the pet type to which ever is selected.
const togglePet = (event:React.MouseEvent<HTMLElement>) => setPetType(event.currentTarget.innerText);
TS compiler is yelling at me with this
Type '(event: React.MouseEvent<HTMLElement>) => void' is not assignable to type '(event: React.MouseEvent<HTMLElement>) => React.MouseEvent<HTMLElement, MouseEvent>'.
Type 'void' is not assignable to type 'MouseEvent<HTMLElement, MouseEvent>'.ts(2322)
index.d.ts(329, 9): The expected type comes from property 'value' which is declared here on type 'IntrinsicAttributes & ProviderProps<(event: MouseEvent<HTMLElement, MouseEvent>) => MouseEvent<HTMLElement, MouseEvent>>'
Thanks for taking the time to read my issue
I have tried setting the to const PetTypeContext = createContext(MouseEvent); which didn't work. I honestly just need the correct default value and data type for TS and I am just lost. The code works, but TS compiler doesn't like it since no default value is given.

Try the following
const UpdatePetTypeContext = createContext((event:React.MouseEvent<HTMLElement>) => {});
You want to have a function that returns nothing, not a function that returns the event itself.

I'm pretty sure you don't want to return event from the handler
change:
(event:React.MouseEvent<HTMLElement>) => event
to:
(event:React.MouseEvent<HTMLElement>) => {}

Related

Add paramater to redux useSelector hook

I am learning Typescript and I have created a smalle app with
create-react-app my-app --template redux-typescript
That has created a hook to type the useSelector
import { TypedUseSelectorHook,useSelector } from 'react-redux';
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
With the useAppSelector i want to select something from the store with a parameter
const favouriteList = useAppSelector((listKey) => selectFavouriteList(listKey));
Without Typescript I thought I could solve this with currying.
const selectFavouriteList = (state: RootState) => (listKey: string) => {
return state.favourites.lists[listKey]
}
But TypesScript returns this error, it expects the store as parameter.
Argument of type 'DefaultRootState' is not assignable to parameter of type '{ favourites: favouriteListState; }'.
Property 'favourites' is missing in type 'DefaultRootState' but required in type '{ favourites: favouriteListState; }'.ts(2345)
store.ts(8, 9): 'favourites' is declared here.
Does anyone has a suggestion how to sole this :-)?
You would do it this way:
const favouriteList = useAppSelector((state) => selectFavouriteList(state, listKey));
where listKey is a local varaible.
If you want to curry, do it the other way round:
const selectFavouriteList = (listKey: string) => (state: RootState) => {
return state.favourites.lists[listKey]
}
const favouriteList = useAppSelector(selectFavouriteList(listKey));

Typescript complaining about React useState setter `Argument of type 'Dispatch<SetStateAction<never[]>>' is not assignable to parameter of

Presume I have this react component
export default function Restaurants() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetchUsers().then(setUsers);
})
...
and User.ts is
interface User { name: string; }
export function fetchUsers(): Promise<User[]> { return fetch(...).then(r => r.json()) }
TypeScript is throwing Argument of type 'Dispatch<SetStateAction<never[]>>' is not assignable to parameter of type '(value: User[]) => void | PromiseLike<void>'. Types of parameters 'value' and 'value' are incompatible. Type 'IRestaurant[]' is not assignable to type 'SetStateAction<never[]>'. Type 'IRestaurant[]' is not assignable to type 'never[]'. Type 'IRestaurant' is not assignable to type 'never'.ts
I tried const [users, setUsers]: [User[], Function] = useState([]); but that also didn't solve the issue
How can I solve this issue?
You need TS to be able to infer at the time that you call useState what sort of value it will contain.
Since it's going to be an array of Users, use that for the generic:
const [users, setUsers] = useState<User[]>([]);
I'd also highly recommend not ignoring errors - unhandled rejections should be avoided whenever possible.
fetchUsers()
.then(setUsers)
.catch(handleErrors);
You also probably only want to fetch the users once, rather than every time the component mounts.
useEffect(() => {
fetchUsers()
.then(setUsers)
.catch(handleErrors);
}, []);

React Typed Context

I'm trying to use Context API with Typescript and nothing is working properly, I'm very confused, I already checked a lot of articles on the internet and it still can't give me an understanding of this matter.
import { createContext, useState } from 'react';
export const SelectedContext = createContext<number>();
export const SelectedContextProvider = (props) => {
const [selectedId, setId] = useState(null);
const changeValue = (value:number | null) => {
setId(value);
}
return (
<SelectedContext.Provider value={{selectedId, changeValue}}>
{props.children}
</SelectedContext.Provider>
)
}
This is how I'm making my Context. I want it to have id: null as a default value but than when the user choose an option in Select I want to set is as number and make an API call. But right now I must specify default value for context and if I do than typescript shows me an error than type null can't be assigned to a number I want it to be in handleChange fuction.
And my second question is, how am I suppose to say what type my props will be right here if they're react children so it can be an empty array of array of React Nodes, I don't like to specify it as any because there's no use in using typescript in this situation.
<SearchContextProvider>
<CitySearchBar/>
<CitySelect/>
</SearchContextProvider>
Will someone be that kind and explain everything for me? I'm new to typescript and I don't know how to fix anmything :(
So I did like vuongvu suggested and now I'm getting
Type '{ selectedId: number | null; changeValue: (value: number | null) =>
void; }' is not assignable to type 'null'.ts(2322)
index.d.ts(335, 9): The expected type comes from property 'value' which is
declared here on type 'IntrinsicAttributes & ProviderProps<null>'
At my value attribute for context provider
So after major changes I'm getting only one error but it still shows up. It seems that now my provider expects only setter as a value, not both data and setter and I'm getting error like this:
Type '[MySelectedContextType,
Dispatch<SetStateAction<MySelectedContextType>>]' is not assignable to type
'Dispatch<SetStateAction<MySelectedContextType>>'.
Type '[MySelectedContextType,
Dispatch<SetStateAction<MySelectedContextType>>]' provides no match for the
signature '(value: SetStateAction<MySelectedContextType>): void'.ts(2322)
So the another problem I'm getting is when I try to consume this context I get this error:
TypeError: Object is not iterable (cannot read property
Symbol(Symbol.iterator))
And I'm getting it like u said:
const [selectedId, setSelectedId] = useSelectedContext();
The major problem that you have is that your context consists not just of the value, but also of the setter. So your context is not just a number, but it's a number (or null) and a setter, like this:
type SelectedContextType = {
selectedId: number | null,
changeValue: (arg: number | null) => void
}
This is how I typically do context in TypeScript:
import React, { createContext, useState, useContext } from 'react';
// I like the verbosity of creating a context type separate
type SelectedContextType = {
selectedId: number | null,
changeValue: (arg: number | null) => void
}
// This is internal and should be set to SelectedContextType
const SelectedContext = createContext<SelectedContextType | undefined>(undefined);
// This is exported for other things to use and can be cast
export const useSelectedContext = () => useContext(SelectedContext) as SelectedContextType
export const SelectedContextProvider = (props) => {
const [selectedId, setId] = useState<SelectedContextType['selectedId']>(null);
const changeValue: SelectedContextType['changeValue'] = (value) => {
setId(value);
}
return (
<SelectedContext.Provider value={{selectedId, changeValue}}>
{props.children}
</SelectedContext.Provider>
)
}
You can also shorten it a little bit by just passing the setter directly to the context value:
export const SelectedContextProvider = (props) => {
const [selectedId, setId] = useState<SelectedContextType['selectedId']>(null);
return (
<SelectedContext.Provider value={{selectedId, changeValue:setId}}>
{props.children}
</SelectedContext.Provider>
)
}
Essentially, you're just doing a "higher up in the tree" useState, so I'd suggest getting rid of all the boilerplate and just doing this:
import React, { createContext, useState, useContext } from 'react';
// this type is the actual type you are holding in state
type MySelectedContextType = number | null;
// this is what react returns when it calls useState with the type your are holding in state
type SelectedContextType = [
MySelectedContextType,
React.Dispatch<React.SetStateAction<MySelectedContextType>>
];
const SelectedContext = createContext<SelectedContextType | undefined>(undefined);
export const useSelectedContext = () => useContext(SelectedContext) as SelectedContextType;
// look how tiny this component is now? I love small components 😊
export const SelectedContextProvider: React.FC = ({children}) => (
<SelectedContext.Provider value={useState<MySelectedContextType>(null)}>
{children}
</SelectedContext.Provider>
);
And then, where you need to use it
export const SomeOtherComponent: React.FC = () => {
// destructure it just like you would setState
const [selectedId,setSelectedId] = useSelectedContext();
return ( ... )
}
Working stackblitz
type Context = {
selectedId: number | null;
changeValue = (value:number | null) => void;
}
import { createContext, useState } from 'react';
export const SelectedContext = createContext<Context>({} as Context);
export const SelectedContextProvider:React.Fc = ({children}: {children: React.ReactNode}) => {
const [selectedId, setId] = useState(null);
const changeValue = (value:number | null) => {
setId(value);
}
return (
<SelectedContext.Provider value={{selectedId, changeValue}}>
{children}
</SelectedContext.Provider>
)
}
export const useStore = () => React.useContex(SelectedContext);
in order to use it across the whole app:
import {useStore} from './context';
const {selectedId, changeValue} = useStore();
You can specify the state value type by the <> symbol, example:
const [selectedId, setId] = useState<number | null>(null);
Reason for your error, if you do not specify the type when initial a value, typescript will automatically assign the type based on the first value you put in, so in this case:
const [selectedId, setId] = useState(null);
selectedId will have Null type the whole time, so you cannot assign any other types to it later on (like number in your case).

How to properly type define event.target.value?

I'm authoring a node package but I'm having bit of an issue with my typescript definitions. To be more specific I find the definition of event.target.value super confusing
Issue description:
I have the following event handler:
import { ChangeEvent, useState } from 'react'
type FieldEvent = ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
export const useField = <T>(input: T) => {
const [value, setValue] = useState<T>(input)
const handleChange = (event: FieldEvent) => {
const { name, value: eventValue } = event.target
// #ts-expect-error
setValue(eventValue)
}
return [input, handleChange]
}
The expression setValue(eventValue) results in the following error:
Argument of type 'string' is not assignable to parameter of type 'SetStateAction<T>'.
I was a bit surprised by this, given a lot of exported components use different event.target.value. Eg date-picker return Date type, select Object, etc.
Issue investigation
Naturally I went to check the imported ChangeEvent react exports to see if it has correct definitions, but this appears to be correct
interface ChangeEvent<T = Element> extends SyntheticEvent<T> {
target: EventTarget & T;
}
so according to this definition it should inherit the type of the Element that was passed to the SyntheticEvent
so I followed the chain to the HTMLInputElement declaration located in node_modules/typescript/lib/lib.dom.d.ts which is where the crux of the issue lies
interface HTMLInputElement extends HTMLElement {
value: string
//... rest
}
I checked back and it appears all the native <input> elements default to string as their value type, which I guess make sense.
Solving the issue
Obviously this is not ideal, given this does not represent the event.target.value behavior in a lot of the reactjs projects that use third-party-packages (which my package is supposed to support). Consider the following codesandbox
The returned event.target.value is as you'd expect of typeof number
that leads me to the question, should I simply override the ChangeEvent with the following definition?
ChangeEvent<{ value: T, name: string } & HTMLInputElement>
or would this be considered a bad practice? Or is there some better way to go about doing this altogether?
handleChange is not match to required params.
I've tried and it worked:
export default function App() {
const [selected, setSelected] = useState(1);
const handleChange = (e: ChangeEvent<{
name?: string | undefined,
value: unknown | number
}>, child: React.ReactNode) => {
setSelected(e.target.value as number);
};
return (
<Select value={selected} onChange={handleChange}>
<MenuItem value={1}>One</MenuItem>
<MenuItem value={2}>Two</MenuItem>
<MenuItem value={3}>Three</MenuItem>
</Select>
);
}
Alright, I'm not 100% sure if this is the correct approach but it seems to work fine for my use-case, albeit the typing seems a tiny bit odd, but basically I'm overwriting the passed type argument to ChangeEvent and extending it by a union of one the HTML elements.
export type FieldEvent<T> = ChangeEvent<
{ value: T, name?: string } &
(HTMLInputElement | HtmlTextAreaElement | HTMLSelectElement)
>
This overwrites the type definition of the ChangeEvent, then you just need to create a handler function that extends the type argument
export type FieldHanderFunction<T> = (event: FieldEvent<T>) => void
so then inside my hook, it basically comes down to:
const useField<T> = (input: T) => {
const handleChange = (event: FieldEvent<T>) => {
// ...
}
}

Typescript: how to declare a type that includes all types extending a common type?

TLDR: Is there a way in Typescript to declare a type that encompasses all types that extend a given interface?
My specific problem
I am writing a custom React hook that encapsulates logic for deciding whether or not an element is moused over. It is modelled roughly after this hook. It exposes a ref that should be able to take any HTMLElement:
const ref = useRef<HTMLElement>(null);
The problem is, if I try to use this ref on any specific React element, I get an error telling me that this specific element is not quite HTMLElement. For example, if I use it with HTMLDivElement, I get this error: argument of type HTMLElement is not assignable to parameter of type HTMLDivElement.
Here's a simple repro case of the problem above in Typescript playground
Obviously, I wouldn't want to list types of all html elements in my hook. Given that HTMLDivElement extends the HTMLElement type, is there a way of declaring that the type that I am actually after is not strictly HTMLElement, but whatever extends HTMLElement?
React code example
source code of the hook
import { useRef, useState, useEffect } from 'react';
type UseHoverType = [React.RefObject<HTMLElement>, boolean];
export default function useHover(): UseHoverType {
const [isHovering, setIsHovering] = useState(false);
let isTouched = false;
const ref = useRef<HTMLElement>(null); // <-- What should the type be here?
const handleMouseEnter = () => {
if (!isTouched) {
setIsHovering(true);
}
isTouched = false;
};
const handleMouseLeave = () => {
setIsHovering(false);
};
const handleTouch = () => {
isTouched = true;
};
useEffect(() => {
const element = ref.current;
if (element) {
element.addEventListener('mouseenter', handleMouseEnter);
element.addEventListener('mouseleave', handleMouseLeave);
element.addEventListener('touchstart', handleTouch);
return () => {
element.removeEventListener('mouseenter', handleMouseEnter);
element.removeEventListener('mouseleave', handleMouseLeave);
element.removeEventListener('touchend', handleTouch);
};
}
}, [ref.current]);
return [ref, isHovering];
}
which produces type error if used like this:
import useHover from 'path-to-useHover';
const testFunction = () => {
const [hoverRef, isHovered] = useHover();
return (
<div
ref={hoverRef}
>
Stuff
</div>
);
}
Type error in example above will be:
Type 'RefObject<HTMLElement>' is not assignable to type 'string | RefObject<HTMLDivElement> | ((instance: HTMLDivElement | null) => void) | null | undefined'.
Type 'RefObject<HTMLElement>' is not assignable to type 'RefObject<HTMLDivElement>'.
Property 'align' is missing in type 'HTMLElement' but required in type 'HTMLDivElement'.
I think you are mistaken about the direction of the assignment that fails. If you have an interface A, then the type that matches all subclasses of A is just called A. This way, HTMLElement (i.e. is assignable from) any HTML element, e.g. HTMLDivElement.
This means that if you have a bunch of functions, one of them accepts HTMLDivElement, another accepts HTMLLinkElement etc, then there is no real type that you can pass to all of them. It would mean you expect to have an element that is both a div and a link and more.
Edited based on your edits of the question:
If the code you have works fine, and your only problem is that it doesn't compile, then just make your useHover generic, like this:
type UseHoverType<T extends HTMLElement> = [React.RefObject<T>, boolean];
function useHover<T extends HTMLElement>(): UseHoverType<T> {
const ref = useRef<T>(null); // <-- What should the type be here?
...
And then:
const testFunction = () => {
const [hoverRef, isHovered] = useHover<HTMLDivElement>();
Something like this will make your code compile fine, without changing its runtime behaviour. I'm unable to tell if the runtime behaviour right now is as desired.
It works as expected, since HTMLDivElement extends HTMLElement. In your typescirpt playground you mixed it up. I updated it by switching x and y in this playground. You want the function to extend HTMLElement and pass y, which is and HTMLDivElement into it. And that works.

Resources