This post is just a list of snippets for using React hooks with TypeScript (since I always forget).
useState
useState
equals this.state
for class components. It makes it super simple to add state to a
function component by returning a variable and its associated setter function.
import React, { useState } from "react";
// our components props accept a number for the initial value
const Counter: React.FC<{}> = () => {
// since we pass a number here, clicks is going to be a number.
// setClicks is a function that accepts either a number or a function
// returning a number
const [clicks, setClicks] = useState(0);
return (
<>
<p>Clicks: {clicks}</p>
<button onClick={() => setClicks(clicks + 1)}>+</button>
<button onClick={() => setClicks(clicks - 1)}>-</button>
</>
);
};
useEffect
For creating side effects in function components, the React developers created the appropriately
named useEffect
function. It can be used for adding event listeners, fetching data, you name it.
It replaces class component's lifecycle methods componentDidUpdate
, componentDidMount
and
componentWillUnmount
.
The function accepts two parameters:
- A side effect function. It will be called without any parameters
- An array of values. These are the dependencies for the function. If not provided, React will run
the side effect every time the component updates. If provided, React will only call the side
effect if one of the array's elements has changed. There is also another special case: Passing
an empty array (
[]
) will make the side effect run only oncomponentDidMount
andcomponentDidUnmount
.
// Standard use case.
const [name, setName] = useState("Pascal");
useEffect(() => {
document.title = `Hello ${name}`;
}, [name]);
Your effect function can also return a clean-up function. This will be called then the component is unmounted. Typescript will also check that your effect function returns another function (if it returns anything at all):
useEffect(() => {
const handler = () => {
document.title = window.width;
};
window.addEventListener("resize", handler);
// ❌ won't compile
return true;
// ✅ compiles
return () => {
window.removeEventListener("resize", handler);
};
});
useContext
useContext
allows you to access context values from
anywhere in your component tree. This is the equivalent to Context.Consumer
in class components.
TypeScript's type inferrence really works its magic here, you don't need to specify any typings
for this:
import React, { createContext, useContext } from "react";
// our context sets a property of type string
export const LanguageContext = createContext({ lang: "en" });
const Display = () => {
// lang will be of type string
const { lang } = useContext(LanguageContext);
return (
<>
<p>Your selected language: {lang}</p>
</>
);
};
useRef
useRef
is the new way of setting refences to DOM elements in function components. It is quite
simple to use with TypeScript, especially since optional
chaining
landed:
import React, { useRef } from "react";
function TextInputWithFocusButton() {
// it's common to initialise refs with null
const inputEl = useRef(null);
const onButtonClick = () => {
// inputEl might be null (we initialized it with null) and
// current might be undefined. Both should not be the case
inputEl?.current?.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
Since TypeScript doesn’t know which element we want to refer to, things like current
and focus()
will also probably be null
. We can make this a lot easier for us and for TypeScript, when we know
which type of element we want to reference. This also helps us to not mix up element types in the
end:
function TextInputWithFocusButton() {
// initialise with null, but tell TypeScript we are
// looking for an HTMLInputElement
const inputEl = useRef<HTMLInputElement>(null);
const onButtonClick = () => {
// If current exists, it is of type HTMLInputElement, thus the
// focus method is available ✅
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>
</>
);
}
useMemo
and useCallback
useMemo
is used for memoization. Say you have a computation-heavy function and you only want to
run it when its parameters change. You can then use useMemo
which will automatically memoize the
result of the function's execution and only executes it again when the parameters change:
/\*\*
- Needs to browse through every pixel of an image. ➡ SLOW
\*/
function getHistogram(image: ImageData): number[] {
// details not really necessary right now
...
return histogram;
}
function Histogram() {
...
/\*
- We don't want to run this method all the time, that's why we save
- the histogram and only update it if imageData (from a state or somewhere)
- changes.
-
- If you provide correct return types for your function or type inference is
- strong enough, your memoized value has the same type.
- In that case, our histogram is an array of numbers
\*/
const histogram = useMemo(() => getHistogram(imageData), [imageData]);
}
You usually won't have to provide any types here since the React typings are pretty good.
useCallback
is very similar to useMemo
. It returns a callback function instead of a value. Its
typings are really strong too:
const memoCallback = useCallback(
(a: number) => {
// doSomething
},
[a]
);
// ❌ won't compile, as the callback needs a number
memoCallback();
// ✅ compiles
memoCallback(3);
useReducer
Are you using Redux? Then you already know what useReducer
is: It's basically the core of state
management libraries baked into a hook:
type ActionType = {
type: "reset" | "decrement" | "increment";
};
const initialState = { count: 0 };
// We only need to set the type here ...
function reducer(state, action: ActionType) {
switch (action.type) {
// ... to make sure that we don't have any other strings here ...
case "reset":
return initialState;
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
return state;
}
}
function Counter({ initialCount = 0 }) {
const [state, dispatch] = useReducer(reducer, { count: initialCount });
return (
<>
Count: {state.count}
{/* and can dispatch certain events here */}
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
</>
);
}