id | title |
---|---|
hooks |
Hooks |
Hooks are supported in @types/react
from v16.8 up.
Type inference works very well most of the time:
const [val, toggle] = React.useState(false);
// `val` is inferred to be a boolean
// `toggle` only takes booleans
See also the Using Inferred Types section if you need to use a complex type that you've relied on inference for.
However, many hooks are initialized with null-ish default values, and you may wonder how to provide types. Explicitly declare the type, and use a union type:
const [user, setUser] = React.useState<IUser | null>(null);
// later...
setUser(newUser);
You can use Discriminated Unions for reducer actions. Don't forget to define the return type of reducer, otherwise TypeScript will infer it.
const initialState = { count: 0 };
type ACTIONTYPE =
| { type: "increment"; payload: number }
| { type: "decrement"; payload: string };
function reducer(state: typeof initialState, action: ACTIONTYPE) {
switch (action.type) {
case "increment":
return { count: state.count + action.payload };
case "decrement":
return { count: state.count - Number(action.payload) };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "decrement", payload: "5" })}>
-
</button>
<button onClick={() => dispatch({ type: "increment", payload: 5 })}>
+
</button>
</>
);
}
View in the TypeScript Playground
Usage with `Reducer` from `redux`
In case you use the redux library to write reducer function, It provides a convenient helper of the format Reducer<State, Action>
which takes care of the return type for you.
So the above reducer example becomes:
import { Reducer } from 'redux';
export function reducer: Reducer<AppState, Action>() {}
When using useEffect
, take care not to return anything other than a function or undefined
, otherwise both TypeScript and React will yell at you. This can be subtle when using arrow functions:
function DelayedEffect(props: { timerMs: number }) {
const { timerMs } = props;
useEffect(
() =>
setTimeout(() => {
/* do stuff */
}, timerMs),
[timerMs]
);
// bad example! setTimeout implicitly returns a number
// because the arrow function body isn't wrapped in curly braces
return null;
}
Solution to the above example
function DelayedEffect(props: { timerMs: number }) {
const { timerMs } = props;
useEffect(() => {
setTimeout(() => {
/* do stuff */
}, timerMs);
}, [timerMs]);
// better; use the void keyword to make sure you return undefined
return null;
}
When using useRef
, you have two options when creating a ref container that does not have an initial value:
const ref1 = useRef<HTMLElement>(null!);
const ref2 = useRef<HTMLElement>(null);
const ref3 = useRef<HTMLElement | null>(null);
You can see the difference in this playground, thanks to this discussion with @rajivpunjabi.
The first option will bypass nullchecks on ref1.current
, and is intended to be passed in to built-in ref
attributes that React will manage (because React handles setting the current
value for you).
What is the !
at the end of null!
?
null!
is a non-null assertion operator (the !
). It asserts that any expression before it is not null
or undefined
, so if you have useRef<HTMLElement>(null!)
it means that you're instantiating the ref with a current value of null
but lying to TypeScript that it's not null
.
function MyComponent() {
const ref1 = useRef<HTMLDivElement>(null!);
useEffect(() => {
doSomethingWith(ref1.current);
// TypeScript won't require null-check e.g. ref1 && ref1.current
});
return <div ref={ref1}> etc </div>;
}
The second option will infer a RefObject
instead of a MutableRefObject
. This means there will be a type error ifyou try to assign to ref2.current
.
The third option will make ref3.current
mutable, and is intended for "instance variables" that you manage yourself.
function TextInputWithFocusButton() {
// initialise with null, but tell TypeScript we are looking for an HTMLInputElement
const inputEl = React.useRef<HTMLInputElement>(null);
const onButtonClick = () => {
// strict null checks need us to check if inputEl and current exist.
// but once current exists, it is of type HTMLInputElement, thus it
// has the method focus! ✅
if (inputEl && inputEl.current) {
inputEl.current.focus();
}
};
return (
<>
{/* in addition, inputEl only can be used with input elements. Yay! */}
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
View in the TypeScript Playground
example from Stefan Baumgartner
we dont have much here, but this is from a discussion in our issues. Please contribute if you have anything to add!
type ListProps<ItemType> = {
items: ItemType[];
innerRef?: React.Ref<{ scrollToItem(item: ItemType): void }>;
};
function List<ItemType>(props: ListProps<ItemType>) {
useImperativeHandle(props.innerRef, () => ({
scrollToItem() {},
}));
return null;
}
If you are returning an array in your Custom Hook, you will want to avoid type inference as TypeScript will infer a union type (when you actually want different types in each position of the array). Instead, use TS 3.4 const assertions:
export function useLoading() {
const [isLoading, setState] = React.useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load] as const; // infers [boolean, typeof load] instead of (boolean | typeof load)[]
}
View in the TypeScript Playground
This way, when you destructure you actually get the right types based on destructure position.
Alternative: Asserting a tuple return type
If you are having trouble with const assertions, you can also assert or define the function return types:
export function useLoading() {
const [isLoading, setState] = React.useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load] as [
boolean,
(aPromise: Promise<any>) => Promise<any>
];
}
A helper function that automatically types tuples can also be helpful if you write a lot of custom hooks:
function tuplify<T extends any[]>(...elements: T) {
return elements;
}
function useArray() {
const numberValue = useRef(3).current;
const functionValue = useRef(() => {}).current;
return [numberValue, functionValue]; // type is (number | (() => void))[]
}
function useTuple() {
const numberValue = useRef(3).current;
const functionValue = useRef(() => {}).current;
return tuplify(numberValue, functionValue); // type is [number, () => void]
}
Note that the React team recommends that custom hooks that return more than two values should use proper objects instead of tuples, however.
- https://medium.com/@jrwebdev/react-hooks-in-typescript-88fce7001d0d
- https://fettblog.eu/typescript-react/hooks/#useref
If you are writing a React Hooks library, don't forget that you should also expose your types for users to use.