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.

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.

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.

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:

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 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:

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 componentDidUpate, 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 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:

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 reduer 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:

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:

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.

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 new ref 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.

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 the forwardRef 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:

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.

The TypeScript definition of useLayoutEffect is nearly identical to useEffect:

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.

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 with 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