🔖 TypeScript memo: how to use it perfectly in React?

preface

For a long time, there are many small partners around ssh who are confused about how to use TS in React. They begin to hate TS and feel that various inexplicable problems reduce the efficiency of development.

In fact, if it is used skillfully, TS only takes a little more time to write types during the first development, which will play a magical role in subsequent maintenance and reconstruction. It is highly recommended to use it for long-term maintenance projects.

In fact, I always knew that there was a good memo in the English version. I wanted to directly recommend it to my friends. However, many people have a headache about English, and the Chinese version of it is actually this scene:

In that case, do it yourself. Some examples in the original English version are extended and summarized into this memo.

Pre Foundation

The prerequisites for reading this article are:

  • Familiar with the use of React.
  • Familiar with type knowledge in TypeScript.
  • This article will focus on using React Hook as an example. Of course, most types of knowledge are common.

In other words, this article focuses on "the combination of React and TypeScript", rather than the basic knowledge. You can learn the basic knowledge by reading the documents.

Also recommend to see me Advanced advanced guide for early intermediate front end The chapters of React and TypeScript in this article will not be repeated here.

tool

Choose the debugging tool you prefer.

Component Props

Let's first look at several types commonly used to define Props:

Foundation type

type BasicProps = {
  message: string;
  count: number;
  disabled: boolean;
  /** Array type */
  names: string[];
  /** Use "union type" to limit to the following two "string literal" types */
  status: "waiting" | "success";
};

object type

type ObjectOrArrayProps = {
  /** If you don't need to use specific attributes, you can vaguely specify an object ❌  Not recommended */
  obj: object;
  obj2: {}; // ditto
  /** Object type with specific properties ✅  recommend */
  obj3: {
    id: string;
    title: string;
  };
  /** Object array 😁  Commonly used */
  objArr: {
    id: string;
    title: string;
  }[];
  /** key Can be any string, and the value is limited to MyTypeHere */
  dict1: {
    [key: string]: MyTypeHere;
  };
  dict2: Record<string, MyTypeHere>; // Basically the same as dict1, it uses the built-in Record type of TS.
}

Function type

type FunctionProps = {
  /** Arbitrary function type ❌  It is not recommended to specify parameters and return value types */
  onSomething: Function;
  /** Functions without parameters do not need to return values 😁  Commonly used */
  onClick: () => void;
  /** Parameters with function 😁  Very common */
  onChange: (id: number) => void;
  /** Another function syntax parameter is the button event of React 😁  Very common */
  onClick(event: React.MouseEvent<HTMLButtonElement>): void;
  /** Optional parameter type 😁  Very common */
  optional?: OptionalType;
}

React related types

export declare interface AppProps {
  children1: JSX.Element; // ❌  Not recommended. Arrays are not considered
  children2: JSX.Element | JSX.Element[]; // ❌  Not recommended. The string children is not considered
  children4: React.ReactChild[]; // A little better, but I didn't consider null
  children: React.ReactNode; // ✅  Including all children
  functionChildren: (name: string) => React.ReactNode; // ✅  Function that returns the React node
  style?: React.CSSProperties; // ✅  Recommended for inline style
  // ✅  All props types of native button tags are recommended
  // You can also pass in the component at the location of the generic type and extract the Props type of the component
  props: React.ComponentProps<"button">;
  // ✅  It is recommended to use the method in the previous step to further extract the native onClick function type 
  // At this time, the first parameter of the function will be automatically inferred as the click event type of React
  onClickButton: React.ComponentProps<"button">["onClick"]
}

Functional component

Simplest:

interface AppProps = { message: string };

const App = ({ message }: AppProps) => <div>{message}</div>;

Including children:

Use react FC built-in types will not only include AppProps defined by you, but also automatically add a children type and other types that will appear on other components:

// Equivalent to
AppProps & { 
  children: React.ReactNode 
  propTypes?: WeakValidationMap<P>;
  contextTypes?: ValidationMap<any>;
  defaultProps?: Partial<P>;
  displayName?: string;
}

// use
interface AppProps = { message: string };

const App: React.FC<AppProps> = ({ message, children }) => {
  return (
    <>
     {children}
     <div>{message}</div>
    </>
  )
};

Hooks

@The types/react package supports Hooks in versions above 16.8.

useState

If your default value can specify the type, you don't need to declare the type manually, and you can leave it to TS for automatic inference:

// val: boolean
const [val, toggle] = React.useState(false);

toggle(false)
toggle(true)

If the initial value is null or undefined, manually pass in the type you want through generics.

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

// later...
setUser(newUser);

This also ensures that when you directly access the property on user, you will be prompted that it may be null.

This error can be avoided through the optional chaining syntax (supported by TS 3.7 and above).

// ✅ ok
const name = user?.name

useReducer

Need to use Discriminated Unions To mark the type of Action.

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>
    </>
  );
}

"Distinguished unions" is generally a union type, in which each type needs to be distinguished by a specific field such as type. When you pass in a specific type, the remaining types payload will automatically match and infer.

So:

  • When the type you write matches the increment, TS will automatically infer that the corresponding payload should be of string type.
  • When the type you write matches increment, the payload should be of type number.

In this way, when you dispatch, enter the corresponding type, and you will be automatically prompted for the remaining parameter types.

useEffect

The main thing to note here is that the return value of the function passed in by useEffect is either a method (cleaning function) or undefined. In other cases, errors will be reported.

A common situation is that our useEffect needs to execute an async function, such as:

// ❌ 
// Type 'Promise<void>' provides no match 
// for the signature '(): void | undefined'
useEffect(async () => {
  const user = await getUser()
  setUser(user)
}, [])

Although there is no explicit return value in async function, async function will return a Promise by default, which will lead to TS error.

It is recommended to rewrite:

useEffect(() => {
  const getUser = async () => {
    const user = await getUser()
    setUser(user)
  }
  getUser()
}, [])

Or use self executing functions? Not recommended, poor readability.

useEffect(() => {
  (async () => {
    const user = await getUser()
    setUser(user)
  })()
}, [])

useRef

This Hook often has no initial value, so you can declare the type of current attribute in the return object:

const ref2 = useRef<HTMLElement>(null);

Take a button scenario as an example:

function TextInputWithFocusButton() {
  const inputEl = React.useRef<HTMLInputElement>(null);
  const onButtonClick = () => {
    if (inputEl && inputEl.current) {
      inputEl.current.focus();
    }
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

When the onButtonClick event is triggered, you can be sure that inputEl is also valuable, because the components are rendered at the same level, but you still have to make redundant non empty judgment.

There's a way around.

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

null! This syntax is a non null assertion. Following a value means that you conclude that it has value, so you use inputel current. When focus(), TS will not give an error.

However, this grammar is dangerous and needs to be used as little as possible.

In most cases, inputel current?. Focus () is a safer option, unless the value really cannot be empty. (for example, it is assigned before use)

useImperativeHandle

It is recommended to use a custom innerRef to replace the native ref, otherwise the type of forwardRef will be very complex.

type ListProps = {
  innerRef?: React.Ref<{ scrollToTop(): void }>
}

function List(props: ListProps) {
  useImperativeHandle(props.innerRef, () => ({
    scrollToTop() { }
  }))
  return null
}

Combined with the knowledge of useRef just now, the usage is as follows:

function Use() {
  const listRef = useRef<{ scrollToTop(): void }>(null!)

  useEffect(() => {
    listRef.current.scrollToTop()
  }, [])

  return (
    <List innerRef={listRef} />
  )
}

It's perfect, isn't it?

It can be debugged online Example of useImperativeHandle.

You can also view this useImperativeHandle discuss Issue , there are a lot of interesting ideas, but also use react A complex example of forwardref.

Customize Hook

If you want to follow the form of useState and return an array to users, you must remember to use as const when appropriate. Mark the return value as a constant and tell TS that the values in the array will not be deleted, change the order, and so on

Otherwise, each of your items will be inferred as a "joint type of all types of possibilities", which will affect users' use.

export function useLoading() {
  const [isLoading, setState] = React.useState(false);
  const load = (aPromise: Promise<any>) => {
    setState(true);
    return aPromise.finally(() => setState(false));
  };
  // ✅  Adding as const will infer [boolean, typeof load]
  // ❌  Otherwise, it will be (boolean | typeof load) []
  return [isLoading, load] as const;[]
}

By the way, if you are writing a library with React Hook, don't forget to export the types to users.

React API

forwardRef

A functional component cannot add ref by default. It does not have its own instance like a class component. This API is generally a functional component used to receive refs from parent components.

Therefore, you need to mark the instance type, that is, what type of value the parent component can get through ref.

type Props = { };
export type Ref = HTMLButtonElement;
export const FancyButton = React.forwardRef<Ref, Props>((props, ref) => (
  <button ref={ref} className="MyClassName">
    {props.children}
  </button>
));

In this example, ref is directly forwarded to button, so you can mark the type as HTMLButtonElement directly.

By calling the parent component in this way, you can get the correct type:

export const App = () => {
  const ref = useRef<HTMLButtonElement>()
  return (
    <FancyButton ref={ref} />
  )
}

Acknowledge

This article is widely used react-typescript-cheatsheets The examples in, plus their own embellishment and example supplement, students with good English can also read the original text to expand their learning.

Added by shanx24 on Wed, 09 Mar 2022 15:47:25 +0200