Elegant writing of TS in React

Welcome to pay attention to the front-end morning tea and advance together with Guangdong liangzai

Front end morning tea focuses on the front end, walks together, and keeps up with the development pace of the industry~

 

preface

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

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.

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 type */
  dict1: {
    [key: string]: MyTypeHere;
  };
  dict2: Record<string, MyTypeHere>; // Basically and dict1 Same, used TS Built in Record Type.
}

 

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. Strings are not considered children
  children4: React.ReactChild[]; // A little better, but I didn't think about it null
  children: React.ReactNode; // ✅ Include all children situation
  functionChildren: (name: string) => React.ReactNode; // ✅ return React Function of node
  style?: React.CSSProperties; // ✅ Inline is recommended style When using
  // ✅ Recommended native button All labels come with props type
  // You can also pass in a component at the location of the generic type and extract the name of the component Props type
  props: React.ComponentProps<"button">;
  // ✅ It is recommended to use the method in the previous step to further extract the native onClick Function type 
  // The first parameter of the function is automatically inferred as React Click event type
  onClickButton: React.ComponentProps<"button">["onClick"]
}

 

Functional component

The simplest:

interface AppProps = { message: string };

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

 

Including children:

Use # react FC # built in types will not only include the # 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 starts to support 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 can also ensure 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 above TS 3.7).

// ✅ ok
const name = user?.name

 

useReducer

Need to use Discriminated Unions To label 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. 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 be automatically matched and inferred.

So:

  • When the , type , you write matches , increment , TS will automatically infer that the corresponding , payload , should be of , string , type.
  • When the , type , you write matches , increment , then , 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 the async function, the async function will return a Promise by default, which will lead to an error in TS.

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 the 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 a value, so when you use {inputel current. When focus(), TS will not give an error.

However, this syntax 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 , instead of the native , ref. otherwise, the use of , forwardRef , will cause complex types.

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

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

 

Combined with the knowledge of "useRef", 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 many interesting ideas and use react A complex example of forwardref.

Customize Hook

If you want to return an array to users in the form of useState, 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, etc

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));
  };
  // ✅ Yes 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 directly mark the type as "HTMLButtonElement".

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

 

Welcome to pay attention to the front-end morning tea and advance together with Guangdong liangzai

Front end morning tea focuses on the front end, walks together, and keeps up with the development pace of the industry~

 

Added by RClapham on Wed, 29 Dec 2021 02:11:03 +0200