useTypescript — A Complete Guide to React Hooks and TypeScript
React v16.8 introduced Hooks which provide the ability to extract state and the React lifecycle into functions that can be utilized across components in your app, making it easy to share logic. Hooks have been met with excitement and rapid adoption, and the React team even imagines them replacing class components eventually.
Previously in React, the way to share logic was through Higher Order Components and Render Props. Hooks offer a much simpler method to reuse code and make our components DRY.
This article will show the changes to TypeScript integration with React and how to add types to hooks as well as your own custom hooks.
The type definitions were pulled from @types/react. Some examples were derived from react-typescript-cheatsheet, so refer to it for a comprehensive overview of React and TypeScript. Other examples and the definition of the hooks have been pulled from the official docs.
I built a course to help you master the coding interview >
Changes to Functional Component with TypeScript
Previously function components in React were called “Stateless Function Components”, meaning they were pure functions. With the introduction of hooks, function components can now contain state and access the React lifecycle. To align with this change, we now type our components as React.FC
or React.FunctionComponent
instead of React.SFC
.
By typing our component as an FC
, the React TypeScripts types allow us to handle children
and defaultProps
correctly. In addition, it provides types for context
, propTypes
, contextTypes
, defaultProps
, displayName
.
It corresponds to these props for FC
/ FunctionComponent
:
NOTE: The React team is discussing removing
defaultProps
from function components. It adds unnecessary complexity because default function arguments work equally as well without needing to introduce a new concept beyond standard JavaScript.
Quick Introduction to Hooks
Hooks are simply functions that allow us to manage state, utilize the React lifecycle, and connect to the React internals such as context. Hooks are imported from the react
library and can only be used in function components:
import * as React from 'react'const FunctionComponent: React.FC = () => {
const [count, setCount] = React.useState(0) // The useState hook
}
React includes 10 hooks by default. 3 of the hooks are considered the “basic” or core hooks which you will use most frequently. There are 7 additional “advanced” hooks which are most often utilized for edge cases. Hooks according to the official React docs:
Basic Hooks
Advanced Hook
While hooks alone are useful, they become even more powerful with the ability to combine them into custom hook functions. By building your own hooks, you are able to extract React logic into reusable functions that can simply be imported into our components. The only caveat to hooks is that you must follow some basic rules.
useState with TypeScript
useState
is a hook that allows us to replace this.state
from class components. We execute the hook which returns an array containing the current state value and a function to update the state. When the state is updated, it causes a re-render of the component. The code below shows a simple useState
hook:
The state can be any JavaScript type, and above we made it a number
. The set state function is a pure function that will specify how to update the state and will always return a value of the same type.
With simple functions, useState
can infer the type from the initial value and return value based on the value supplied to the function. For more complex state, the useState<T>
is a generic where we can specify the type. The example below shows a user
object that can be null
.
The official typing is the following:
useEffect with TypeScript
The useEffect
is how we manage side effects such as API calls and also utilize the React lifecycle in function components. useEffect
takes a callback function as its argument, and the callback can return a clean-up function. The callback will execute on componentDidMount
and componentDidUpdate
, and the clean-up function will execute on componentWillUnmount
.
By default useEffect
will be called on every render, but you can also pass an optional second argument which allows you to execute the useEffect
function only if a value changes or only on the initial render. The second optional argument is an array of values that will only re-execute the effect if one of them changes. If the array is empty, it will only call useEffect
on the initial render. For a more comprehensive understanding of how it works, refer to the official docs.
When using hooks, you should only return undefined
or a function
. If you return any other value, both React and TypeScript will give you an error. If you use an arrow function as your callback, you must be careful to ensure that you are not implicitly returning a value. For example, setTimeout
returns an integer in the browser:
The second argument to useEffect
is a read-only array that can contain any
values — any[]
.
Since useEffect
takes a function
as an argument and only returns a function
or undefined
, these types are already declared by the React type system:
useContext with TypeScript
useContext
allows you utilize the React context
which is the global way to manage app state which can be accessed inside any component without needing to pass the values as props
.
The useContext
function accepts a Context
object and returns the current context value. When the provider updates, this Hook will trigger a re-render with the latest context value.
We initialize a context using the createContext
function which builds a context object. The Context
object contains a Provider
component, and all components that want to access its context must be in the child tree of the Provider
. If you have used Redux, this is equivalent to the <Provider store={store} />
component which provides access to the global store through context. For a further explanation of context, refer to the official docs.
The types for useContext
are the following:
useReducer with TypeScript
For more complex state, you have the option to utilize the useReducer
function as an alternative to useState
.
If you have used Redux before, this should be familiar. The useReducer
accepts 3 arguments and returns a state
and dispatch
function. The reducer
is a function of the form (state, action) => newState
. The initialState
is likely a JavaScript object, and the init
argument is a function that allows you to lazy-load the initial state and will execute init(initialState)
.
This is a bit dense, so let’s look at a practical example. We will redo the counter example from the useState
section but replace it with a useReducer
, and then we’ll follow it with a Gist containing the type definitions.
The useReducer
function can utilize the following types:
useCallback with TypeScript
The useCallback
hook returns a memoized callback. This hook function takes 2 arguments: the first argument is an inline callback function and the second argument is an array of values. The array of values will be referenced in the callback function and accessed by the order in which they exist in the array.
useCallback
will return a memoized version of the callback that only changes if one of the inputs in the input array has changed. This hook is utilized when you pass a callback function to a child component. It will prevent unnecessary renders because the callback will only be executed when the values change, allowing you to optimize your components. This hook can be viewed as a similar concept as the shouldComponentUpdate
lifecycle method.
The TypeScript definition of useCallback
is the following:
useMemo with TypeScript
The useMemo
hook takes a “create” function as its first argument and an array of values as the second argument. It returns a memoized value which will only be recomputed if the input values change. This allows you to prevent expensive calculations on renders for computed values.
useMemo
allows you to compute a value of any type. The below example shows how to create a typed memoized number.
The TypeScript definition of useMemo
is the following:
The DependencyList
is allowed to contain values of any
type, and there is no strict requirement from the built-in types or in relation to T
.
useRef with TypeScript
The useRef
hook allows you to create a ref
which gives you access to the properties of an underlying DOM node. This can be used when you need to extract values from the element or derive information about it in relation to the DOM, such as its scroll position.
Previously we used
createRef()
. This function would always return a newref
every render inside a function component. Now,useRef
will always return the same ref after being created which will lead to an improvement in performance.
The hook returns a ref
object (which is type MutableRefObject
) whose .current
property is initialized to the passed argument of initialValue
. The returned object will persist for the full lifetime of the component. The value of the ref
can be updated by setting it using the ref
prop on a React element.
The TypeScript definition for useRef
is the following:
The generic type you pass as the generic T
will correspond to the type of HTML element you will be accessing in current
.
useImperativeHandle with TypeScript
useImperativeHandle
hook function takes 3 arguments — 1. a React ref
, 2. a createHandle
function, and 3. an optional deps
array for arguments exposed to createHandle
.
The useImperativeHandle
is a rarely used function and using refs should be avoided in most cases. This hook is used to customize a mutable ref
object that is exposed to parent components. useImperativeHandle
should be used with forwardRef
:
The second
ref
arugment of a component (FancyInput(props, ref)
) only exists when you use theforwardRef
function, which allows you more easily pass refs to children.
In this example, a parent component that renders <FancyInput ref={fancyInputRef} />
would be able to call fancyInputRef.current.focus()
.
The TypeScript definition of useImperativeHandle
is the following:
useLayoutEffect with TypeScript
The useLayoutEffect
is similar to useEffect
with the difference being that it is solely intended for side effects relating to the DOM. This hook allows you to read values from the DOM and synchronously re-render before the browser has a chance to repaint.
Utilize the useEffect
hook whenever possible and avoid useLayoutEffect
only when absolutely needed to avoid blocking visual updates.
The TypeScript definition of useLayoutEffect
is nearly identical to useEffect
:
useDebugValue with TypeScript
useDebugValue
is a tool used to debug your custom hooks. It allows you to display a label in React Dev Tools for your custom hook functions.
We will build a custom hook below, but this example shows us how we debug inside them using the useDebugValue
hook.
Custom Hooks
The ability to build your own hooks is truly what makes this update to React so impactful. Previously we shared component in React apps using Higher Order Components and Render Props. This causes our component tree to become unwieldy and produces code that is difficult to read and reason about. In addition, these were typically implemented using class components which can cause many problems and prevent optimizations.
A custom hook allows you to combine the core React Hooks into your own function and extract component logic. This custom hook function can easily be shared and imported like any other JavaScript function, and it behaves the same as the core React hooks and must follow the same rules.
To build our custom hook, we will use the example from the React docs and add our TypeScript types. This custom hook will utilize the useState
and useEffect
and will manage the online status of a user. We will assume we have access to a ChatAPI
which we can subscribe to and receive our friend’s online status.
For custom hooks, we should follow the pattern of React’s hooks and prepend our function with the word use
indicating that the function is intended to be a hook. We will name our hook useFriendStatus
. The code is below followed by an explanation.
The useFriendStatus
hook takes a friendID
which allows us to subscribe to the online status of this user. We utilize the useState
function and initialize it to null
. We name our state value isOnline
and have a function setIsOnline
to toggle this boolean. Since we have a simple state, TypeScript can infer the types of our state value and updater function.
Next we have a handleStatusChange
function. This function takes a status
parameter which contains an isOnline
value. We call the setIsOnline
function to update the state value. status
can’t be inferred, so we create a TypeScript interface for it (we will also arbitrarily assume it contains the user ID).
The useEffect
hook’s callback subscribes to the API to check a friend’s status and returns a clean-up function to unsubscribe from the API when the component unmounts. When the online status changes, our subscription executes the handleStatusChange
function. Since this updates our state, it will propagate the result to the component that is using the hook and forces a rerender.
The hook can be imported in any function component. Since we added types to the custom hook, the component using will get the type definitions by default.
This logic is now able to be extended to any component that needs to know the online status of a user