What typescript type do I use with useRef() hook when setting current manually? - reactjs

How can I use a React ref as a mutable instance, with Typescript? The current property appears to be typed as read-only.
I am using React + Typescript to develop a library that interacts with input fields that are NOT rendered by React. I want to capture a reference to the HTML element and then bind React events to it.
const inputRef = useRef<HTMLInputElement>();
const { elementId, handler } = props;
// Bind change handler on mount/ unmount
useEffect(() => {
inputRef.current = document.getElementById(elementId);
if (inputRef.current === null) {
throw new Exception(`Input with ID attribute ${elementId} not found`);
}
handler(inputRef.current.value);
const callback = debounce((e) => {
eventHandler(e, handler);
}, 200);
inputRef.current.addEventListener('keypress', callback, true);
return () => {
inputRef.current.removeEventListener('keypress', callback, true);
};
});
It generates compiler errors: semantic error TS2540: Cannot assign to 'current' because it is a read-only property.
I also tried const inputRef = useRef<{ current: HTMLInputElement }>(); This lead to this compiler error:
Type 'HTMLElement | null' is not assignable to type '{ current: HTMLInputElement; } | undefined'.
Type 'null' is not assignable to type '{ current: HTMLInputElement; } | undefined'.

Yeah, this is a quirk of how the typings are written:
function useRef<T>(initialValue: T): MutableRefObject<T>;
function useRef<T>(initialValue: T|null): RefObject<T>;
If the initial value includes null, but the specified type param doesn't, it'll be treated as an immutable RefObject.
When you do useRef<HTMLInputElement>(null), you're hitting that case, since T is specified as HTMLInputElement, and null is inferred as HTMLInputElement | null.
You can fix this by doing:
useRef<HTMLInputElement | null>(null)
Then T is HTMLInputElement | null, which matches the type of the first argument, so you hit the first override and get a mutable ref instead.

as key.
You can use it like this for input component.
const inputRef = useRef() as MutableRefObject<HTMLInputElement>;

I came to this question by searching how to type useRef with Typescript when used with setTimeout or setInterval. The accepted answer helped me solve that.
You can declare your timeout/interval like this
const myTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)
And to clear it and set it again, you do it as usual:
const handleChange = () => {
if (myTimeout.current) {
clearTimeout(myTimeout.current)
}
myTimeout.current = setTimeout(() => {
doSomething()
}, 500)
}
The typing will work both if you're running in Node or in a Browser.

Related

How to extend a ref type for forwardRef in react and typescript to allow multiple refs

This code validates under typescript:
type Ref = HTMLTableElement;
const Table = forwardRef<Ref, shapeTable>(({filtered, handleSort, paginationOptions, summary, onMount}, refTable) => {
useEffect(() => {
if(refTable && typeof refTable !== 'function' && refTable.current) {
const tableWidth:string = window.getComputedStyle(refTable.current).getPropertyValue("width");
onMount(tableWidth);
}
}, [refTable, onMount]);
This code does not validate under typescript:
When I put the type refTable inside an object as below, and I update the Ref type to allow for that, it does not validate. How can I fix this? I need to be able to pass more than one ref into forwardRef. I have done this previously without using typescript and it works. So something about forwardRef seems to insist only one ref type can pass. Unless its a simpler matter of updating my Ref type:
Type '((instance: Ref | null) => void) | MutableRefObject | null' is not assignable to type 'Ref'. Type 'null' is not assignable to type 'Ref'.
type Ref = {
refTable: HTMLTableElement
};
const Table = forwardRef<Ref, shapeTable>(({filtered, handleSort, paginationOptions, summary, onMount}, ref) => {
const {refTable}:Ref = ref;
useEffect(() => {
if(refTable && typeof refTable !== 'function' && refTable.current) {
const tableWidth:string = window.getComputedStyle(refTable.current).getPropertyValue("width");
onMount(tableWidth);
}
}, [refTable, onMount]);
You are confused about which properties are on which objects. In the second example you have a ref to an object with a refTable property. If your ref is a ref object (not a callback ref) then it would look like:
ref = {
current: {
refTable: HTMLTableElement,
}
}
So you need to look for current on the ref first and then look for refTable on ref.current.
Revised code:
const Table = forwardRef<Ref, shapeTable>(({ filtered, handleSort, paginationOptions, summary, onMount }, ref) => {
useEffect(() => {
if (ref && typeof ref !== 'function' && ref.current) {
const table = ref.current.refTable;
const tableWidth = window.getComputedStyle(table).getPropertyValue("width");
onMount(tableWidth);
}
}, [ref, onMount]);
...
Typescript Playground Link
Be aware that using refs in useEffect dependencies is not recommended.

How to pass a type down to a React Context Consumer in Typescript

I want to create a React Context and pass state and setState properties useState Hook down to a Consumer.
I have a Step and a Context types:
type Step = {
step: string;
};
type Context = {
step: Step;
setStep: Dispatch<SetStateAction<Step>>;
};
I create a context:
export const StepContext = createContext<Context | undefined>(undefined);
I create a StepProvider and a hook:
const StepProvider = ({ children }: { children: ReactNode }) => {
const [step, setStep] = useState<Step>({ step: 'one' });
return <StepContext.Provider value={{ step, setStep }}>{children}</StepContext.Provider>;
};
I create a StepConsumer:
const StepConsumer = () => {
const { step, setStep } = useContext(StepContext);
return <div>{...}</div>;
};
But in:
const { step } = useContext(StepContext)
step and setStep return the following error TS2339: Property 'step' does not exist on type 'Context | undefined'
Am I not passing the types the right way?
Thanks
You're syntax is correct but usage of the context is not thorough enough to ensure the value is not undefined when deconstructing.
The error itself is checked by the strictNullChecks compiler option, your code will work in it's current state if strictNullChecks: false is set in your tsconfig.
However, to keep that option enabled you'll need to make sure the context value is not undefined before deconstructing or using the StepContext values, Typescript won't let you access it without checking first.
If you change the deconstruction to a simple assignment and then check the context is not undefined the error will go away.
const stepContext = React.useContext(StepContext);
if (typeof stepContext !== "undefined") {
stepContext.setStep("foo")
}
// simplified
if (stepContext) {
stepContext.setStep("foo")
}
// simplified further
stepContext?.setStep("foo");
Additionally, with strictNullChecks: false you're also able to define the context type without the undefined type value.
export const StepContext = createContext<Context>(undefined);

Typescript React prop types with two possibilities

I am trying to learn and implement Typescript in my new React project. I have a component ToggleButton that I want to use on two occasions, sorting and filtering products. Each toggle option has it's own button, but depending on if it is a sorting or filtering option there is a slight difference
type OrderBy = {
value: ProductOrderBy
children: ReactNode
onClick: (arg0: ProductOrderBy) => void
}
type SizeFilter = {
value?: ProductSize
children: ReactNode
onClick: (arg0?: ProductSize) => void
}
const ToggleButton = ({ value, children, onClick }: OrderBy | SizeFilter) => {
const handleClick = () => onClick(value)
return <Button onClick={handleClick}>{children}</Button>
}
As you can see, in case it's a filter option, the value is optional. Because the value is used as a parameter in the callback, also the callback parameter is optional.
I expected that the union | would state that the props have to match either the OrderBy or SizeFilter type. But in the intellisense of my IDE it looks like both types are merged.
My code works as expected, but my IDE tells me on the onClick(value) the following: Argument of type 'ProductOrderBy | ProductSize | undefined' is not assignable to parameter of type 'never'.
I can think of many workarounds to get around this problem, but as I'm still trying to learn Typescript I like to know why my typing isn't working as expected...?
const ToggleButton = ({ value, children, onClick }: OrderBy | SizeFilter) => {
if(!value) {
return null;
}
const handleClick = () => onClick(value)
return <Button onClick={handleClick}>{children}</Button>
}
This is because of SizeFilter where value can be undefined.

React Typescript destructure providerValue

After a suggestion of a member on my previous post on how to pass setState to children of context API, I have this code:
export interface IShowProviderProps {
shows: IShow[];
setShows: (currentShows: IShow[], shows: IShow[]) => void;
sort: string;
setSort: (sort: string) => void;
query: string;
setQuery: (sort: string) => void;
showType: string;
setShowType: (sort: "movie" | "tv") => void;
page: number;
setPage: (page: number) => void;
}
export const ShowContext = createContext<IShowProviderProps | null>(null);
export const ShowProvider = ({ children }: Props): JSX.Element => {
const [shows, setShows] = useState<IShow[]>([]);
const [sort, setSort] = useState<string>("popularity.desc");
const [query, setQuery] = useState<string>("");
const [showType, setShowType] = useState<"movie" | "tv">("movie");
const [page, setPage] = useState<number>(1);
const providerValue: IShowProviderProps = {
shows,
setShows,
sort,
setSort,
query,
setQuery,
showType,
setShowType,
page,
setPage
};
return <ShowContext.Provider value={providerValue}>{children}</ShowContext.Provider>;
export const useShows = () => useContext(ShowContext);
When I use context to children and I try something like this
const {shows, setShows} = useShows();
I get error:
Property 'shows' does not exist on type IShowProviderProps | null
And I have to do this:
const providerValues = useShows();
and then
providerValues?.shows (without ? I get possibly null)
what I have to do? thanks
I don't work with TypeScript, so my answer might be not very accurate, but this line
const ShowContext = React.createContext<IShowProviderProps>({} as IShowProviderProps);
fixes the issue. The problem is that since providerValues can be null, TS compiler assumes that null must have shows, which it obviously doesn't.
There is nothing wrong with the logic. I assume IShowProviderProps | null was a workaround to keep TS compiler silent when you passed null as an initial value.
I dropped all your code in a codesandbox, check this out -- it's all working.
BTW I believe setShows: (currentShows: IShow[], shows: IShow[]) => void; is a wrong signature for setShows. It takes the only argument which is either an IShow or IShow => IShow, but not two.
The issue here is that your Context's value type is a union between IShowProviderProps and null:
/* The "IShowProviderProps | null" means that the value can be of type
IShowProviderProps or type null */
export const ShowContext = createContext<IShowProviderProps | null>(null);
TypeScript will therefore consider the value of your Context as potentially being null and, will by default, report a compile error when you attempt to access fields on what is (potenntially) a null value:
/* Obtain context value - note that the value type is for this is
IShowProviderProps | null, which means TypeScript sees showsValue
as being potentially null */
const showsValue = useShows();
/* TypeScript won't allow access to shows, setShows, on a nullable value */
const {shows, setShows} = showsValue;
One solution here would be to simply define the value type of your Context as IShowProviderProps:
/* Removing "| null" causes TypeScript to now consider the value type of
ShowContext to be non-nullable, ie always defined */
export const ShowContext = createContext<IShowProviderProps>(null);
With that change, the need for any "null checking" of the context value at run-time can be discared as TypeScript will deduce that the Context value is always defined as type IShowProviderProps.
For these changes to work, you need to ensure that TypeScripts "strict mode" is disabled. That can be done by ensuring "strict" : false is present in the compilerOptions of your projects tsconfig.js file:
{
"compilerOptions": {
...
"strict": false,
...
},
...
}
Hope that helps!

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