React V16.8 added hooks, which provide the ability to access state and React lifecycle in function components. These functions can be reused across application components to share logic.

Previously in React, we could only share logic with Higher Order Components and Render Props. The advent of hooks provides a new idea and an easier way to reuse code, making our components DRY(Don’t repeat Yourself).

Today I want to talk about how to combine hooks with Typescript code and how to add types to official hooks or our own.

The type definitions in this article come from @types/ React. Some examples come from the react-typescript-Cheatsheet, from which you can see more complete examples. Other examples come from the official documentation.

Changes to the way function components are written

Previously Function Components in React were called Stateless Function Components because they had no state. With hooks, function components can also access the state and React lifecycle. To make the distinction, we can no longer write our component’s type as react. SFC, but as react. FC or react. FunctionComponent.

import * as React from 'react'

interface IProps {
  / /... Props interface
}

// Now we have to write this
const MyNewComponent: React.FC<IProps> = (props) = >{... };// The old way of writing
const MyOldComponent: React.SFC<IProps> = (props) = >{... };Copy the code

By declaring component types as FC, TypeScript allows us to handle children and defaultProps correctly. In addition, it provides the types of context, propTypes, contextTypes, defaultProps, displayName, etc.

As follows:

interface FunctionComponent<P = {}> { (props: P & { children? : ReactNode }, context? : any): ReactElement |null; propTypes? : WeakValidationMap<P>; contextTypes? : ValidationMap<any>; defaultProps? : Partial<P>; displayName? : string; }Copy the code

I don’t think there is any need for defaultProps. Since functions can write default values for parameters, there is no need to introduce a new attribute.

An overview of the Hooks

As I said before, hooks are nothing new, they are just simple functions that allow us to manage state, use lifecycles, and access React mechanisms like contexts. A Hook is an enhancement to a function component and can only be used in a function component:

import * as React from 'react'

const FunctionComponent: React.FC = () => {
  const [count, setCount] = React.useState(0) //useState hook
}
Copy the code

React comes with 10 hooks. Three are commonly used, and the others are mostly used for edge cases. As follows:

The commonly used Hooks

  • useState
  • useEffect
  • useContext

Senior Hook

  • useReducer`
  • useCallback
  • useMemo
  • useRef
  • useImperativeHandle
  • useLayoutEffect
  • useDebugValue

Each hook is powerful, and they can be combined to implement custom hooks that provide even more power. By implementing custom hooks, we can extract some logic into reusable functions that we can later introduce into our components. The only thing to note is that there are certain rules to follow when using hooks. I’ve talked a little bit about why these rules exist, but we’ll talk about them separately.

useState

UseState allows us to use in function components the capabilities of this.state similar to those of class components. This hook returns an array containing the current state value and a function that updates the state. When the state is updated, it triggers a re-rendering of the component. The usage is as follows:

import * as React from 'react';

const MyComponent: React.FC = () => {
  const [count, setCount] = React.useState(0);
  return (
    <div onClick={() => setCount(count + 1)}>
      {count}
    </div>
  );
};
Copy the code

The state can be any JavaScript type, but in the example above we used number. The set state function is a pure function that specifies how to update the state and always returns a value of the same type.

UseState can infer the type of the initial and return values from the type of the value we provide to the function. For complex states, useState

can be used to specify the type. The following example shows a user object that can be null.

import * as React from 'react'; interface IUser { username: string; email: string; password: string; } const ComplexState = ({ initialUserData }) => { const [user, setUser] = React.useState<IUser | null>(initialUserData); if (! user) { // do something else when our user is null } return ( <form> <input value={user.username} onChange={e => setUser({... user, username: e.target.value})} /> <input value={user.email} onChange={e => setUser({... user, email: e.target.value})} /> <input value={user.password} onChange={e => setUser({... user, password: e.target.value})} /> </form> ); }Copy the code

The official type definition is as follows:

function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
type Dispatch<A> = (value: A) => void;
type SetStateAction<S> = S | ((prevState: S) => S);
Copy the code

useEffect

UseEffect can be used to manage what we call Side effects in function components such as API requests and use of the React lifecycle. UseEffect can take a callback as an argument, and this callback can return a cleanup function. The callback is executed at times like componentDidMount and componentDidUpdate, and the cleanup function is executed at times like componentWillUnmount.

useEffect(() => {
  const subscription = props.source.beginAPIPoll();
  return () => {
    // Clean up the subscription
    subscription.cancelAPIPoll();
  };
});
Copy the code

By default, useEffect is executed on every render, but it also has an optional second parameter that allows useEffect to be executed on a value update or on the first render. This optional argument is an array that reexecutes every time any value in the array is updated. If the array is empty, useEffect is executed only once, at the time of the first rendering. Refer to the official documentation for more detailed information.

When using this hook, we can only return undefined or another function. React and TypeScript will both report an error if we return a value. If we use an arrow function as a callback, we need to make sure that no value is returned implicitly. For example, setTimeout returns an integer in the browser:

function DelayedEffect(props: { timerMs: number }) { const { timerMs } = props; UseEffect (() => setTimeout(() => {/* do stuff */}, timerMs), [timerMs]))Copy the code

The second argument to useEffect is a read-only array that can contain a value of type any-any [].

Since useEffect takes function as an argument and only returns function or undefined, the type definition is clear:

function useEffect(effect: EffectCallback, deps? : DependencyList): void; // The first argument, `effect` type EffectCallback = () => (void | (() => void | undefined)); // The second argument, `deps? ` type DependencyList = ReadonlyArray<any>;Copy the code

useContext

UseContext allows us to use the React context in function components. Context allows us to access global state in any component without having to pass data down one layer at a time.

The useContext function takes a Context object and returns the current Context value. When the provider updates, this Hook triggers a re-rendering with the latest context value.

import { createContext, useContext } from 'react';
props ITheme {
  backgroundColor: string;
  color: string;
}
// The standard way to create context. It takes an initial value object
const ThemeContext = createContext<ITheme>({
  backgroundColor: 'black',
  color: 'white',
})
// Accessing context in a child component
const themeContext = useContext<ITheme>(ThemeContext);
Copy the code

The createContext function creates a context object. The Context object contains a Provider component, and all components that want to access the Context need to be in the Provider’s subcomponent tree. Redux, like Redux’s component, allows you to access global state through context. Those of you who are interested in context and want to know more about it go here: official documentation.

UseContext has the following types:

function useContext<T>(context: Context<T>): T; interface Context<T> { Provider: Provider<T>; Consumer: Consumer<T>; displayName? : string; }Copy the code

useReducer

For complex states, we can also use the useReducer function instead of useState.

const [state, dispatch] = useReducer(reducer, initialState, init); |
Copy the code

This is very similar to Redux. The useReducer takes three arguments and returns the state object and the Dispatch function. Reducer is a function of the form (state, action) => newState, initialState is a JavaScript object, and the init parameter is a function that allows us to lazily load the initialState, like this: init(initialState).

It looks confusing, but let’s look at a concrete example. Use useReducer to rewrite the above counter using useState as follows:

import * as React from 'react';

enum ActionType {
  Increment = 'increment',
  Decrement = 'decrement',
}

interface IState {
  count: number;
}

interface IAction {
  type: ActionType;
  payload: {
    count: number; 
  };
}

const initialState: IState = {count: 0};

const reducer: React.Reducer<IState, IAction> = (state, action) => {
  switch (action.type) {
    case ActionType.Increment:
      return {count: state.count + action.payload.count};
    case ActionType.Decrement:
      return {count: state.count - action.payload.count};
    default:
      throw new Error();
  }
}

const ComplexState = () => {
  const [state, dispatch] = React.useReducer<React.Reducer<IState, IAction>>(reducer, initialState);

  return (
    <div>
      <div>Count: {state.count}</div>
      <button onClick={
        () => dispatch({type: ActionType.Increment, payload: { count: 1 } })
      }>+</button>
      <button onClick={
        () => dispatch({type: ActionType.Decrement, payload: { count: 1 }})
      }>-</button>
    </div>  
  );
Copy the code

The useReducer function can use the following types:

type Dispatch<A> = (value: A) => void; type Reducer<S, A> = (prevState: S, action: A) => S; type ReducerState<R extends Reducer<any, any>> = R extends Reducer<infer S, any> ? S : never; type ReducerAction<R extends Reducer<any, any>> = R extends Reducer<any, infer A> ? A : never; function useReducer<R extends Reducer<any, any>, I>( reducer: R, initializerArg: I & ReducerState<R>, initializer: (arg: I & ReducerState<R>) => ReducerState<R> ): [ReducerState<R>, Dispatch<ReducerAction<R>>]; function useReducer<R extends Reducer<any, any>, I>( reducer: R, initializerArg: I, initializer: (arg: I) => ReducerState<R> ): [ReducerState<R>, Dispatch<ReducerAction<R>>]; function useReducer<R extends Reducer<any, any>>( reducer: R, initialState: ReducerState<R>, initializer? : undefined ): [ReducerState<R>, Dispatch<ReducerAction<R>>];Copy the code

useCallback

UseCallbackhook returns a cached callback. The hook function takes two arguments: the first argument is an inline callback function, and the second argument is an array. The values in this array will be referenced by the callback and accessed in the order they were in the array.

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
Copy the code

UseCallback will return the cached version of the callback and then update the returned callback only if the value in the array changes. This hook can be used to avoid meaningless rendering when we pass a callback from a child component. Because this callback is executed only when the value in the array changes, we can use it to optimize our component. We can think of this hook as a shouldComponentUpdate lifecycle function substitute in function components.

UseCallbackTypeScript is defined as follows:

function useCallback<T extends (... args: any[]) => any>(callback: T, deps: DependencyList): T;Copy the code

useMemo

UseMemohook is similar to useCallback, except that it returns a value. It takes a function as its first argument, and again, an array as its second argument. It then returns a cached value that is recalculated when the value in the array is updated. This allows us to avoid complex calculations during rendering.

 const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); |
Copy the code

UseMemo allows us to evaluate any type of value. The following example shows a typed cached number:

const computeExpensiveValue = (end: number) => { let result = 0; for (let i = 0; i < end * 1000000; i++) { for (let j = 0; i < end * 1000; j++) { result = result + i - j; } } return result; }; const MyComponent = ({ end = 0 }) => { const memoizedNumber = React.useMemo<number>(computeExpensiveValue(end)) return (  <DisplayResult result={memoizedNumber} /> ); }Copy the code

UseMemo types are defined as follows:

function useMemo<T>(factory: () => T, deps: DependencyList): T;
Copy the code

DependencyList is allowed to contain values of type ANY without any special restrictions.

useRef

UseRefhook allows us to create a ref to access the attributes of a bottom node. It comes in handy when we need to access the value of an element or derive some information relative to the DOM (such as the position of the slide).

const refContainer = useRef(initialValue);
Copy the code

Previously we used createRef(), which always returns a new ref every time we render. UseRef ‘will now always return the same ref after creation, which will definitely improve performance.

The hook returns a ref object (of type MutableRefObject) whose.current property is initialized with the initialValue passed in. The returned object will exist throughout the life of the component, and the value of ref can be updated by setting it to the ref property of a React element.

function TextInputWithFocusButton() {
  // The type of our ref is an input element
  const inputEl = useRef<HTMLInputElement>(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
Copy the code

The useRef type is defined as follows:

function useRef<T>(initialValue: T): MutableRefObject<T>;

interface MutableRefObject<T> {
  current: T;
}
Copy the code

useImperativeHandle

| useImperativeHandle(ref, createHandle, [inputs]) |
Copy the code

The useImperativeHandle hook function takes three arguments:

  1. React ref
  2. createHandlefunction
  3. optionaldepsArrays are used to expose passescreateHandleThe value of the

UseImperativeHandle is rarely used because refs are generally avoided. This hook is used to define a modifiable ref object exposed to the parent component. UseImperativeHandle is used with the forwardRef:

function FancyInput(props, ref) { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); }})); return <input ref={inputRef} ... / >; } FancyInput = React.forwardRef(FancyInput); // You can now get a ref directly to the DOM button: const fancyInputRef = React.createRef(); <FancyInput ref={fancyInputRef}>Click me! </FancyInput>;Copy the code

A second ref parameter (FancyInput (props, ref * * * *)) only used in our [forwardRef] (https://reactjs.org/docs/forwarding-refs.html) (function), (reactjs.org/docs/forwar…

In this example, the rendering < FancyInput ref = {fancyInputRef} / > father component will be able to call the fancyInputRef. Current. The focus ().

UseImperativeHandle is defined as follows:

function useImperativeHandle<T, R extends T>(ref: Ref<T>|undefined, init: () => R, deps? : DependencyList): void;Copy the code

useLayoutEffect

UseLayoutEffect is similar to useEffect except that it is only used for DOM related side effects. It allows you to read values from the DOM and rerender them synchronously before the browser has a chance to redraw.

Use **useEffect**** hook whenever possible and avoid **useLayoutEffect** if not necessary.

The useLayoutEffect type definition is similar to useEffect:

function useLayoutEffect(effect: EffectCallback, deps? : DependencyList): void;Copy the code

useDebugValue

UseDebugValue is used to debug our custom hooks. It allows us to display a label to our custom hooks in React Dev Tools.

useDebugValue(value)
Copy the code

This example shows how to debug a custom hook using useDebugValue hook.

function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); / /... // display a value in DevTools near the hook // e.g. "FriendStatus: Online" useDebugValue(isOnline? 'Online' : 'Offline'); return isOnline; }Copy the code

Custom Hooks

The ability to define our own hooks is where the React update shines. In React we used Higher Order Components to share logic with Render Props. This resulted in our component tree becoming bloated and some code that was difficult to read and understand. Moreover, they are implemented using class components, which can cause some problems that are difficult to optimize.

Custom hooks allow us to combine React core hooks into our own functions, abstracting some of the component logic. Custom hook functions can easily share logic and import like any other JavaScript function. React hooks are no different from React hooks and follow the same rules.

Let’s customize a hook using the official documentation example and add our TypeScript type. This custom hook uses useState and useEffect to manage a user’s online status. We will assume that we have a ChatAPI available to access our friends’ online status.

For custom hooks, we should follow the rule that our function is a hook by prefixing it with the use prefix. Call this hook useFriendStatus. Let’s look at the code and explain it later:

import React, { useState, useEffect } from 'react';

type Hook = (friendID: number) => boolean

interface IStatus {
  id: number;
  isOnline: boolean;
}

const useFriendStatus: Hook = (friendID) => {
  const [isOnline, setIsOnline] = useState<boolean | null>(null);

  function handleStatusChange(status: IStatus) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}
Copy the code

UseFriendStatushook accepts a friendID from which we can access the user’s online status. We use the useState function and give an initial value of NULL. Rename the status value to isOnline and change the Boolean value to setIsOnline. This state is relatively simple, and TypeScript can infer the value of the state and the type of the update function.

We also have to have a handleStatusChange function. This function has a status parameter that contains an isOnline value. We call the setIsOnline function to update the status value. Status cannot be inferred, so we create an interface type for it.

The useEffecthook callback registers the API to check a friend’s online status and returns a cleanup function to unregister the component if it is unmounted. The handleStatusChange function is executed when the online state changes. Once the state is updated, it is passed to the component that is using the hook, causing it to re-render.

Our hook can be used in any function component because we have added a type definition to it, and any component that uses it gets its type definition by default.

import * as React from 'react'; import useFriendStatus from './useFriendStatus'; interface IUser { id: number; username: string; } const FriendsListItem ({user}) => {const isOnline = useFriendStatus(user.id); return ( <li> <span style={{ backgroundColor: isOnline ? 'green' : 'red }} /> <span> {user.username} </span> <li> ); };Copy the code

Now any component that wants to retrieve a user’s online status can use this hook directly to extend the functionality.

interface FunctionComponent<P = {}> { (props: P & { children? : ReactNode }, context? : any): ReactElement | null; propTypes? : WeakValidationMap<P>; contextTypes? : ValidationMap<any>; defaultProps? : Partial<P>; displayName? : string; }Copy the code

Well, once you understand some of these type definitions, you should be able to use hooks in typescript. They’re just simple functions, right?