Skip to content

Latest commit

 

History

History
282 lines (214 loc) · 11.6 KB

hooks.md

File metadata and controls

282 lines (214 loc) · 11.6 KB
id title
hooks
Hooks

Hooks are supported in @types/react from v16.8 up.

useState

Type inference works very well most of the time:

const [val, toggle] = React.useState(false);
// `val` is inferred to be a boolean
// `toggle` only takes booleans

See also the Using Inferred Types section if you need to use a complex type that you've relied on inference for.

However, many hooks are initialized with null-ish default values, and you may wonder how to provide types. Explicitly declare the type, and use a union type:

const [user, setUser] = React.useState<IUser | null>(null);

// later...
setUser(newUser);

useReducer

You can use Discriminated Unions for reducer actions. Don't forget to define the return type of reducer, otherwise TypeScript will infer it.

const initialState = { count: 0 };

type ACTIONTYPE =
  | { type: "increment"; payload: number }
  | { type: "decrement"; payload: string };

function reducer(state: typeof initialState, action: ACTIONTYPE) {
  switch (action.type) {
    case "increment":
      return { count: state.count + action.payload };
    case "decrement":
      return { count: state.count - Number(action.payload) };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = React.useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "decrement", payload: "5" })}>
        -
      </button>
      <button onClick={() => dispatch({ type: "increment", payload: 5 })}>
        +
      </button>
    </>
  );
}

View in the TypeScript Playground

Usage with `Reducer` from `redux`

In case you use the redux library to write reducer function, It provides a convenient helper of the format Reducer<State, Action> which takes care of the return type for you.

So the above reducer example becomes:

import { Reducer } from 'redux';

export function reducer: Reducer<AppState, Action>() {}

useEffect

When using useEffect, take care not to return anything other than a function or undefined, otherwise both TypeScript and React will yell at you. This can be subtle when using arrow functions:

function DelayedEffect(props: { timerMs: number }) {
  const { timerMs } = props;

  useEffect(
    () =>
      setTimeout(() => {
        /* do stuff */
      }, timerMs),
    [timerMs]
  );
  // bad example! setTimeout implicitly returns a number
  // because the arrow function body isn't wrapped in curly braces
  return null;
}
Solution to the above example
function DelayedEffect(props: { timerMs: number }) {
  const { timerMs } = props;

  useEffect(() => {
    setTimeout(() => {
      /* do stuff */
    }, timerMs);
  }, [timerMs]);
  // better; use the void keyword to make sure you return undefined
  return null;
}

useRef

When using useRef, you have two options when creating a ref container that does not have an initial value:

const ref1 = useRef<HTMLElement>(null!);
const ref2 = useRef<HTMLElement>(null);
const ref3 = useRef<HTMLElement | null>(null);

You can see the difference in this playground, thanks to this discussion with @rajivpunjabi.

The first option will bypass nullchecks on ref1.current, and is intended to be passed in to built-in ref attributes that React will manage (because React handles setting the current value for you).

What is the ! at the end of null!?

null! is a non-null assertion operator (the !). It asserts that any expression before it is not null or undefined, so if you have useRef<HTMLElement>(null!) it means that you're instantiating the ref with a current value of null but lying to TypeScript that it's not null.

function MyComponent() {
  const ref1 = useRef<HTMLDivElement>(null!);
  useEffect(() => {
    doSomethingWith(ref1.current);
    // TypeScript won't require null-check e.g. ref1 && ref1.current
  });
  return <div ref={ref1}> etc </div>;
}

The second option will infer a RefObject instead of a MutableRefObject. This means there will be a type error ifyou try to assign to ref2.current.

The third option will make ref3.current mutable, and is intended for "instance variables" that you manage yourself.

function TextInputWithFocusButton() {
  // initialise with null, but tell TypeScript we are looking for an HTMLInputElement
  const inputEl = React.useRef<HTMLInputElement>(null);
  const onButtonClick = () => {
    // strict null checks need us to check if inputEl and current exist.
    // but once current exists, it is of type HTMLInputElement, thus it
    // has the method focus! ✅
    if (inputEl && inputEl.current) {
      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>
    </>
  );
}

View in the TypeScript Playground

example from Stefan Baumgartner

useImperativeHandle

we dont have much here, but this is from a discussion in our issues. Please contribute if you have anything to add!

type ListProps<ItemType> = {
  items: ItemType[];
  innerRef?: React.Ref<{ scrollToItem(item: ItemType): void }>;
};

function List<ItemType>(props: ListProps<ItemType>) {
  useImperativeHandle(props.innerRef, () => ({
    scrollToItem() {},
  }));
  return null;
}

Custom Hooks

If you are returning an array in your Custom Hook, you will want to avoid type inference as TypeScript will infer a union type (when you actually want different types in each position of the array). Instead, use TS 3.4 const assertions:

export function useLoading() {
  const [isLoading, setState] = React.useState(false);
  const load = (aPromise: Promise<any>) => {
    setState(true);
    return aPromise.finally(() => setState(false));
  };
  return [isLoading, load] as const; // infers [boolean, typeof load] instead of (boolean | typeof load)[]
}

View in the TypeScript Playground

This way, when you destructure you actually get the right types based on destructure position.

Alternative: Asserting a tuple return type

If you are having trouble with const assertions, you can also assert or define the function return types:

export function useLoading() {
  const [isLoading, setState] = React.useState(false);
  const load = (aPromise: Promise<any>) => {
    setState(true);
    return aPromise.finally(() => setState(false));
  };
  return [isLoading, load] as [
    boolean,
    (aPromise: Promise<any>) => Promise<any>
  ];
}

A helper function that automatically types tuples can also be helpful if you write a lot of custom hooks:

function tuplify<T extends any[]>(...elements: T) {
  return elements;
}

function useArray() {
  const numberValue = useRef(3).current;
  const functionValue = useRef(() => {}).current;
  return [numberValue, functionValue]; // type is (number | (() => void))[]
}

function useTuple() {
  const numberValue = useRef(3).current;
  const functionValue = useRef(() => {}).current;
  return tuplify(numberValue, functionValue); // type is [number, () => void]
}

Note that the React team recommends that custom hooks that return more than two values should use proper objects instead of tuples, however.

More Hooks + TypeScript reading:

If you are writing a React Hooks library, don't forget that you should also expose your types for users to use.

Example React Hooks + TypeScript Libraries:

Something to add? File an issue.