Introduced as part of React 16.8.0, Hooks sought to simplify the library for newcomers, and address common pain-points for the experienced React developer. There were three key motivations behind React Hooks:
- Adding the ability to use state without the need for writing a class
- Making it easier to reuse stateful logic across components
- Decreasing the complexity of individual components by increasing modularity
Fortunately for the TypeScript developer, the React team included first-class support for types as part of the same release! Meaning that TypeScript’s powerful static type system is our best mate when using Hooks.
In this introductory article, I’ll offer a flavour of how leveraging types & type safety can make life easier when writing components via a few of React’s Basic Hooks. We will be taking a look at the following Hooks:
useState
useEffect
useMemo
Following-up this article, we’ll delve into some additional Hooks provided by the React team, before wrapping up by discussing how powerful composing multiple Hooks to create your own Custom Hooks can be - make sure to stay tuned for those!
useState<T>
This hook allows us to define some local state for our component, initialising it with a default value, and also provides us a function to make updates to the state.
import React, {FC, useState} from 'react';
const Counter: FC = () => {
const [count, setCount] = useState(0);
return (
<>
<h1>Your count is {count}</h1>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
</>
);
}
In the above example, I’ve written a simple component that increments and decrements a counter, displaying its current count value.
Within the component, I utilise the useState
Hook to declare some local state for the counter’s value - with an initial value of zero - and a method to make changes to the state which is called when I click either button.
TypeScript is able to infer the type of count
from the supplied initial value. Type safety gives us some extra peace of mind, as we know for certain that count
will always represent a number
. If I were to attempt to call setCount
with anything other than a number
then the code will not compile.
However, with TypeScript & React Hook’s support for generics, we don’t have to rely on type inference here. This can come in handy when using more complex types to represent state within our components.
interface User {
name: string,
age: number
}
// Below, the type of the initial state is inferred from the format of the input.
const [user, setUser] = useState<User>({name: "Alice", age: 21});
// This works fine! The compiler expects that name is of type 'User', which is inferred from the input
setUser({name: "Bob", age: 42});
// Error! The compiler expects a 'User' but you have provided a 'string'
setUser("Charlie");
Additionally, we don’t have to pass a value explicitly to useState
, we can also pass it a function that returns your desired state. Let’s have a look at its type definition:
useState<T>(initialState: T | (() => T))
This can come in really handy. For instance, if we have some complex method used to fetch data which we’d like to store as a piece of local state within a component:
function complexGetter(): User {
// do complex operation that needs more than 1 line
return result;
}
const UserCard: FC<OwnProps> = () => {
const [user, setUser] = useState(complexGetter);
return (
{/* display user data */}
)
}
useEffect
This hook is used to manage any side effects within the component, such as external API calls or adding listeners to DOM elements.
useEffect
takes two arguments:
- A callback function which may return an optional callback function that will be called during cleanup, and;
- An optional second argument of type
any[]
, a set of dependencies used to trigger the effect when their values update.
Effects serve to give the user similar functionality to calling the lifecycle methods componentDidMount
, componentDidUpdate
, and componentWillUnmount
in a pre-Hooks world - but in a much more concise way.
The effect is called when the component mounts, or updates. Subsequently, if supplied, the cleanup function is called during an unmount.
Additionally, the effect will listen for updates to any pieces of state passed in its optional second parameter. This ensures it is only called when any of the relevant state updates - this prevents redundant calls to the effect, and therefore reduces extraneous re-renders of the component.
import React, {FC, useState, useEffect} from 'react';
const Counter: FC = () => {
const [count, setCount] = useState(0);
useEffect(() => {
document.title(`Your count is {count}`);
}, [count]);
return (
<>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
</>
);
}
Above, I’ve made a change to the counter component from the useState
example, where instead of displaying the current count in a heading I update the title of the page every time the count updates. Hence, a side-effect of updating the count’s value is to update the page’s title.
I’ve provided count
within the list of dependencies, this ensures that the effect is only called when the value of the count has been updated.
Additionally, as mentioned above you can return a callback from your effects which are used when cleaning up your components.
useEffect(() => {
const handleClick = () => setCount(count + 1);
window.addEventListener('click', handleClick);
return () => window.removeEventListener('click', handleClick);
}, [count, setCount]);
In this example, we add a click event listener to the browser window, which updates the count each time the user clicks on the page. We need to remove the listener later when cleaning up the component, this is done via returning a callback to do so.
TypeScript’s added type safety prevents the user from returning anything other than undefined
, or a function that returns undefined
from an effect. Let’s have a look at the type definition used for the callback argument, for a clearer picture…
type EffectCallback = () => (void | (() => void | undefined));
If the user attempts to return a value from an effect, then the code will not compile. For instance, if I attempt to return the count from an effect, then the compiler will give me an error:
useEffect(() => {
document.title(`Your count is {count}`);
// Error! useEffect can only return 'undefined' or a 'function'.
return count;
}, [count]);
useMemo<T>
This hook memoizes the value returned from a function call, meaning it will only be recomputed if the input values change, as opposed to each and every time the component updates. Most often we use this hook to reduce the number of executions of more computationally heavy function calls.
Functionally, this hook is pretty similar to useEffect
. Where we pass it a callback to some function, such that we only want it to execute when the inputs update. However, in this case instead of just having some side effect, we want to return a value.
Hence, the useMemo
hook takes two arguments; a callback function to compute your memoized value, and a dependency array of type any[]
used to trigger re-computations as they are updated.
import React, {FC, useMemo} from 'react';
interface Vector2 {
x: number,
y: number
}
interface ExpensiveValueProps {
a: Vector2[],
b: Vector2[]
}
function someExpensiveComputation(a: Vector2[], b: Vector2[]): Vector2[] {
// do something really complex
}
const ExpensiveValueCalculator: FC<ExpensiveValueProps> = ({a, b}) => {
const expensiveValue = useMemo(() => someExpensiveCalculation(a, b), [a, b]);
return (
<div>Your expensive value is {expensiveValue}</div>
);
}
In the example above, I’ve defined a component displaying the result of some expensive calculation, such that it takes two Vector2
arrays as input from the component’s props and returns a Vector2
array. I’ve utilised the useMemo
hook to prevent unnecessary computations of its value between renders of the component, as it will only be re-computed if the values of a
or b
change.
Type safety, and type inference are our friends here when we’re using TypeScript. As in the definition of someExpensiveFunction
we tell the compiler to only accept the types that we expect, and we tell it what type the function will return. If we attempt to call our function with the wrong types, or use its returned value in unexpected ways, then the compiler will catch us.
const scalar = 42;
// Error! someExpensiveCalculation expects 'x' to be a 'Vector2[]' but you've provided a 'number'
const expensiveValue = useMemo(() => someExpensiveCalculation(scalar, y), [scalar, y]);
Additionally, as with useState
you can use generics to be more specific with what type you’d like your memoized value return to be. We can add an explicit return type of Vector2[]
to our above example like so:
const expensiveValue = useMemo<Vector2[]>(() => someExpensiveCalculation(a, b), [a, b]);
This prevents the user from calling a function that returns anything other than the desired type for expensiveValue
which is Vector2[]
.
Wrapping Up
Hooks are really cool, and I think that anyone who plans to start a React project ought to be using them. They can be used to reduce boilerplate, lower the barrier to entry for less-experienced front-end developers, and can make our code more organised and easier to reason about.
When we add TypeScript to the mix, they become even more powerful as we leverage its support for generics, its powerful type inference system, and the type safety it provides. The best part, in my opinion, is how low the overhead is - as we often have to write little to no extra code to reap these benefits.
Make sure to keep an eye out for Part 2, where we’ll be having a look at another handful of useful hooks provided by the React team - checking out how combining them with TypeScript can help improve your code and make writing components simpler.