React + TypeScript 常用類(lèi)型匯總

本文適合對(duì)TypeScript感興趣的小伙伴閱讀~

歡迎關(guān)注前端早茶,與廣東靚仔攜手共同進(jìn)階~

作者:廣東靚仔
一、前言
在React項(xiàng)目開(kāi)發(fā)中,寫(xiě)出優(yōu)雅的、更有意義的typescript代碼,是我們一直追求的。本文廣東靚仔帶小伙伴們一起來(lái)看看React項(xiàng)目實(shí)際開(kāi)發(fā)中用到的一些常用類(lèi)型示例。
目錄搶先看:
基本prop類(lèi)型示例
有用的 React Prop 類(lèi)型示例
函數(shù)組件
類(lèi)組件
form和event
Context
forwardRef/createRef
有用的hooks
HOC
Linting
二、基本prop類(lèi)型示例
常規(guī)的程序中使用的 TypeScript 類(lèi)型列表:
type AppProps = {
  message: string;
  count: number;
  disabled: boolean;
 /** 一個(gè)類(lèi)型的數(shù)組!*/
  names: string[];
  /** 用于指定精確字符串值的字符串文字,使用聯(lián)合類(lèi)型將它們連接在一起 */
  status: "waiting" | "success";
  /** 任何對(duì)象,只要你不使用它的屬性(不常見(jiàn),但用作占位符)*/
  obj: object;
  obj2: {}; // 和 `object` 差不多,和 `Object` 完全一樣  
  /** 具有任意數(shù)量屬性的對(duì)象 (PREFERRED) */
  obj3: {
    id: string;
    title: string;
  };
  /** 對(duì)象數(shù)組?。ǔR?jiàn)的) */
  objArr: {
    id: string;
    title: string;
  }[];
  /** 具有任意數(shù)量的相同類(lèi)型屬性的 dict 對(duì)象 */
  dict1: {
    [key: string]: MyTypeHere;
  };
  dict2: Record<string, MyTypeHere>; // 相當(dāng)于 dict1   
  /** 任何函數(shù),只要你不調(diào)用它(不推薦) */
  onSomething: Function;
   /** 不接受或不返回任何內(nèi)容的函數(shù)(非常常見(jiàn)) */
  onClick: () => void;
   /** 帶有命名props的函數(shù)(非常常見(jiàn)) */
  onChange: (id: number) => void;
   /** 接受事件的函數(shù)類(lèi)型語(yǔ)法(非常常見(jiàn)) */
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
  /** 接受事件的替代函數(shù)類(lèi)型語(yǔ)法(非常常見(jiàn)) */
  onClick(event: React.MouseEvent<HTMLButtonElement>): void;
  /** 一個(gè)可選的props(非常常見(jiàn)!) */
  optional?: OptionalType;
};
三、有用的 React Prop 類(lèi)型示例
export declare interface AppProps {
  children?: React.ReactNode; // 最好,接受 React 可以渲染的所有內(nèi)容  
  childrenElement: JSX.Element; // 單個(gè) React 元素  
  style?: React.CSSProperties; // 傳遞樣式props
  onChange?: React.FormEventHandler<HTMLInputElement>; // 形成事件!泛型參數(shù)是 event.target 的類(lèi)型  
  props: Props & React.ComponentPropsWithoutRef<"button">; // 模擬按鈕元素的所有 props 并明確不轉(zhuǎn)發(fā)其 ref    
  props2: Props & React.ComponentPropsWithRef<MyButtonWithForwardRef>; // 模擬 MyButtonForwardedRef 的所有 props 并顯式轉(zhuǎn)發(fā)其 ref
}


type還是interface?

這是一個(gè)有用的經(jīng)驗(yàn)法則:
在創(chuàng)作庫(kù)或第 3 方環(huán)境類(lèi)型定義時(shí),始終用于公共 API 的定義,因?yàn)檫@允許使用者在缺少某些定義時(shí)通過(guò)聲明合并來(lái)擴(kuò)展它們。

考慮為您的 React 組件 Props 和 State 使用,以保持一致性并且因?yàn)樗艿礁嘞拗啤?br>
四、函數(shù)組件
這些可以寫(xiě)成普通函數(shù),接受一個(gè)props參數(shù)并返回一個(gè) JSX 元素。
type AppProps = {
  message: string;
}; /* 如果導(dǎo)出使用 `interface` 以便消費(fèi)者可以擴(kuò)展 */
// 聲明函數(shù)組件的最簡(jiǎn)單方法;推斷返回類(lèi)型。
const App = ({ message }: AppProps) => <div>{message}</div>;
// 您可以選擇注釋返回類(lèi)型,這樣如果您不小心返回了其他類(lèi)型,則會(huì)引發(fā)錯(cuò)誤
const App = ({ message }: AppProps): JSX.Element => <div>{message}</div>;
// 你也可以?xún)?nèi)聯(lián)類(lèi)型聲明;消除了命名props類(lèi)型,但看起來(lái)重復(fù)
const App = ({ message }: { message: string }) => <div>{message}</div>;
hook
useState
類(lèi)型推斷對(duì)于簡(jiǎn)單值非常有效:
const [state, setState] = useState(false);
// `state` 被推斷為布爾值
// `setState` 只接受布爾值
許多鉤子都是用 null-ish 默認(rèn)值初始化的,你可能想知道如何提供類(lèi)型。

顯式聲明類(lèi)型,并使用聯(lián)合類(lèi)型:

const [user, setUser] = useState<User | null>(null);
setUser(newUser);
如果狀態(tài)在設(shè)置后不久初始化并且始終具有以下值,還可以使用類(lèi)型斷言:
const [user, setUser] = useState<User>({} as User);
setUser(newUser);
useReducer
您可以將有區(qū)別的聯(lián)合用于 reducer 操作。不要忘記定義reducer的返回類(lèi)型,否則TypeScript會(huì)推斷出來(lái)。
import { useReducer } from "react";

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] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "decrement", payload: "5" })}>
        -
      </button>
      <button onClick={() => dispatch({ type: "increment", payload: 5 })}>
        +
      </button>
    </>
  );
}

useEffect / useLayoutEffect
useEffect和都useLayoutEffect用于執(zhí)行副作用并返回一個(gè)可選的清理函數(shù),這意味著如果它們不處理返回值,則不需要類(lèi)型。

使用 時(shí)useEffect,注意不要返回除函數(shù) or 以外的任何東西undefined,否則 TypeScript 和 React 都會(huì)提示你。

這在使用箭頭函數(shù)時(shí)可能很微妙:

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

  useEffect(
    () =>
      setTimeout(() => {
        /* do stuff */
      }, timerMs),
    [timerMs]
  );
  // 反面例子!setTimeout 隱式返回一個(gè)數(shù)字
  // 因?yàn)榧^函數(shù)體沒(méi)有用大括號(hào)括起來(lái)
  return null;
}

useRef
在 TypeScript 中,返回一個(gè)只讀或可變useRef的引用,取決于您的類(lèi)型參數(shù)是否完全覆蓋初始值。選擇一個(gè)適合您的用例。

1、DOM 元素 ref
訪問(wèn) DOM 元素:
僅提供元素類(lèi)型作為參數(shù),并null用作初始值。.current在這種情況下,返回的引用將具有由 React 管理的只讀引用TypeScript 期望將此 ref 提供給元素的ref prop:

function Foo() {
  // - 如果可能,請(qǐng)盡可能具體。例如,HTMLDivElement
  // 比 HTMLElement 好,也比 Element 好得多。
  // - 從技術(shù)上講,這會(huì)返回 RefObject<HTMLDivElement>
  const divRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    // 注意 ref.current 可能為空。這是意料之中的
    // 有條件地渲染被引用的元素,或者你可能忘記分配它
    if (!divRef.current) throw Error("divRef is not assigned");

    // 現(xiàn)在 divRef.current 肯定是 HTMLDivElement
    doSomethingWith(divRef.current);
  });
   // 將 ref 賦予一個(gè)元素,以便 React 可以管理它
  return <div ref={divRef}>etc</div>;
}
如果確定divRef.current永遠(yuǎn)不會(huì)為空,也可以使用非空斷言運(yùn)算符!:
const divRef = useRef<HTMLDivElement>(null!);
// 無(wú)需檢查是否為空
doSomethingWith(divRef.current);
2、可變值 ref
要具有可變值:提供您想要的類(lèi)型,并確保初始值完全屬于該類(lèi)型:
function Foo() {
  // 從技術(shù)上講,這將返回 MutableRefObject<number | 空>
  const intervalRef = useRef<number | null>(null);

  // 你自己管理 ref(這就是為什么它被稱(chēng)為 MutableRefObject?。?br>  useEffect(() => {
    intervalRef.current = setInterval(...);
    return () => clearInterval(intervalRef.current);
  }, []);

  // ref 不會(huì)傳遞給任何元素的 "ref" 屬性
  return <button onClick={/* clearInterval the ref */}>Cancel timer</button>;
}

自定義hook
如果你在自定義 Hook 中返回一個(gè)數(shù)組,你會(huì)想要避免類(lèi)型推斷,因?yàn)?TypeScript 會(huì)推斷一個(gè)聯(lián)合類(lèi)型(當(dāng)你實(shí)際上想要在數(shù)組的每個(gè)位置使用不同的類(lèi)型時(shí))
import { useState } from "react";
export function useLoading() {
  const [isLoading, setState] = useState(false);
  const load = (aPromise: Promise<any>) => {
    setState(true);
    return aPromise.finally(() => setState(false));
  };
  return [isLoading, load] as const;  // 推斷 [boolean, typeof load] 而不是 (boolean | typeof load)[]
}
五、類(lèi)組件
在TypeScript 中,React.Component是一個(gè)泛型類(lèi)型(aka React.Component<PropType, StateType>),因此希望為它提供(可選)prop 和 state 類(lèi)型參數(shù):
type MyProps = {
  // 使用 `interface` 也可以
  message: string;
};

type MyState = {
  count: number; // 像這樣  
};

class App extends React.Component<MyProps, MyState> {
  state: MyState = {
    // 可選的第二個(gè)注解,用于更好的類(lèi)型推斷
    count: 0,
  };
  render() {
    return (
      <div>
        {this.props.message} {this.state.count}
      </div>
    );
  }

}
Tips: 可以導(dǎo)出/導(dǎo)入/擴(kuò)展這些類(lèi)型/接口以供重用。
類(lèi)方法:像往常一樣做,要記住函數(shù)的任何參數(shù)也需要輸入:
class App extends React.Component<{ message: string }, { count: number }> {
  state = { count: 0 };
  render() {
    return (
      <div onClick={() => this.increment(1)}>
        {this.props.message} {this.state.count}
      </div>
    );
  }
  increment = (amt: number) => {
    this.setState((state) => ({
      count: state.count + amt,
    }));
  };
}
類(lèi)屬性:如果需要聲明類(lèi)屬性以供以后使用,只需將其聲明為state,但無(wú)需賦值:
class App extends React.Component<{
  message: string;
}> {
  pointer: number; // 像這樣
  componentDidMount() {
    this.pointer = 3;
  }
  render() {
    return (
      <div>
        {this.props.message} and {this.pointer}
      </div>
    );
  }
}

getDerivedStateFromProps
派生狀態(tài)可以使用鉤子來(lái)實(shí)現(xiàn),這也可以幫助設(shè)置memoization。

以下是可以注釋的幾種方法getDerivedStateFromProps
1、如果已顯式鍵入派生狀態(tài)并希望確保 from 的返回值getDerivedStateFromProps符合它。
class Comp extends React.Component<Props, State> {
  static getDerivedStateFromProps(
    props: Props,
    state: State
  ): Partial<State> | null {
    //
  }
}
2、希望函數(shù)的返回值確定的狀態(tài)時(shí)。
class Comp extends React.Component<
  Props,
  ReturnType<typeof Comp["getDerivedStateFromProps"]>

> {
  static getDerivedStateFromProps(props: Props) {}

}
3、想要具有其他狀態(tài)字段和記憶的派生狀態(tài)時(shí)
type CustomValue = any;
interface Props {
  propA: CustomValue;
}
interface DefinedState {
  otherStateField: string;
}
type State = DefinedState & ReturnType<typeof transformPropsToState>;
function transformPropsToState(props: Props) {
  return {
    savedPropA: props.propA, // 保存以備memoization
    derivedState: props.propA,
  };
}
class Comp extends React.PureComponent<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = {
      otherStateField: "123",
      ...transformPropsToState(props),
    };
  }
  static getDerivedStateFromProps(props: Props, state: State) {
    if (isEqual(props.propA, state.savedPropA)) return null;
    return transformPropsToState(props);
  }
}
六、form和event
如果需要單獨(dú)定義事件處理程序,IDE 工具在這里真的很方便,因?yàn)?@type 定義帶有豐富的類(lèi)型。輸入要查找的內(nèi)容,通常自動(dòng)完成功能會(huì)為您提供幫助。onChange這是表單事件的樣子

type State = {
  text: string;
};

class App extends React.Component<Props, State> {
  state = {
    text: "",
  };

   // 在 = 的右側(cè)輸入
  onChange = (e: React.FormEvent<HTMLInputElement>): void => {
    this.setState({ text: e.currentTarget.value });
  };
  render() {
    return (
      <div>
        <input type="text" value={this.state.text} onChange={this.onChange} />
      </div>
    );
  }
}
React.FormEvent<>除了使用and鍵入?yún)?shù)和返回值void,您還可以將類(lèi)型應(yīng)用于事件處理程序本身
// 在 = 的左側(cè)輸入
  onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
    this.setState({text: e.currentTarget.value})
  }
鍵入 onSubmit,在表單中包含不受控制的組件

如果不太關(guān)心事件的類(lèi)型,可以使用 React.SyntheticEvent。

如果目標(biāo)表單具有想要訪問(wèn)的自定義命名輸入,可以使用類(lèi)型斷言:

<form
  ref={formRef}
  onSubmit={(e: React.SyntheticEvent) => {
    e.preventDefault();
    const target = e.target as typeof e.target & {
      email: { value: string };
      password: { value: string };
    };
    const email = target.email.value; // 類(lèi)型檢查!
    const password = target.password.value; // 類(lèi)型檢查!
    // ...
  }}
>
  <div>
    <label>
      Email:
      <input type="email" name="email" />
    </label>
  </div>
  <div>
    <label>
      Password:
      <input type="password" name="password" />
    </label>
  </div>
  <div>
    <input type="submit" value="Log in" />
  </div>
</form>






事件類(lèi)型列表


七、Context
基本示例
import { createContext } from "react";

interface AppContextInterface {
  name: string;
  author: string;
  url: string;
}

const AppCtx = createContext<AppContextInterface | null>(null);

// 應(yīng)用程序中的提供程序

const sampleAppContext: AppContextInterface = {
  name: "Using React Context in a Typescript App",
  author: "thehappybug",
  url: "http://www.example.com",
};

export const App = () => (
  <AppCtx.Provider value={sampleAppContext}>...</AppCtx.Provider>
);

// 在你的應(yīng)用中使用
import { useContext } from "react";

export const PostInfo = () => {
  const appContext = useContext(AppCtx);
  return (
    <div>
      Name: {appContext.name}, Author: {appContext.author}, Url:{" "}
      {appContext.url}
    </div>
  );
};
擴(kuò)展示例
使用createContext空對(duì)象作為默認(rèn)值
interface ContextState {
  // 使用上下文設(shè)置你想要處理的狀態(tài)類(lèi)型,例如
  name: string | null;
}
// 設(shè)置一個(gè)空對(duì)象為默認(rèn)狀態(tài)
const Context = createContext({} as ContextState);
// 像在 JavaScript 中一樣設(shè)置上下文提供程序
使用createContext 和 context getters來(lái)制作 a createCtx with no ,但無(wú)需檢查:
import { createContext, useContext } from "react";

const currentUserContext = createContext<string | undefined>(undefined);

function EnthusasticGreeting() {
  const currentUser = useContext(currentUserContext);
  return <div>HELLO {currentUser!.toUpperCase()}!</div>;
}

function App() {
  return (
    <currentUserContext.Provider value="Anders">
      <EnthusasticGreeting />
    </currentUserContext.Provider>
  );
}
注意我們需要的顯式類(lèi)型參數(shù),因?yàn)槲覀儧](méi)有默認(rèn)string值:
const currentUserContext = createContext<string | undefined>(undefined);
//                                             ^^^^^^^^^^^^^^^^^^
連同非空斷言告訴 TypeScript currentUser肯定會(huì)在那里:
return <div>HELLO {currentUser!.toUpperCase()}!</div>;
//      
這是不幸的,因?yàn)槲覀冎郎院笤谖覀兊膽?yīng)用程序中,a Provider將填充上下文。

有幾個(gè)解決方案:

1、可以通過(guò)斷言非空來(lái)解決這個(gè)問(wèn)題:

const currentUserContext = createContext<string>(undefined!);    
2、我們可以編寫(xiě)一個(gè)名為的輔助函數(shù)createCtx來(lái)防止訪問(wèn)Context未提供值的 a。通過(guò)這樣做,API 相反,我們不必提供默認(rèn)值,也不必檢查:
import { createContext, useContext } from "react";

/**
* 創(chuàng)建上下文和提供者的助手,沒(méi)有預(yù)先的默認(rèn)值,并且
* 無(wú)需一直檢查未定義。
*/
function createCtx<A extends {} | null>() {
  const ctx = createContext<A | undefined>(undefined);
  function useCtx() {
    const c = useContext(ctx);
    if (c === undefined)
      throw new Error("useCtx must be inside a Provider with a value");
    return c;
  }
  return [useCtx, ctx.Provider] as const; // 'as const' 使 TypeScript 推斷出一個(gè)元組
}

// 用法:
// 我們?nèi)匀恍枰付ㄒ粋€(gè)類(lèi)型,但沒(méi)有默認(rèn)值!
export const [useCurrentUserName, CurrentUserProvider] = createCtx<string>();

function EnthusasticGreeting() {
  const currentUser = useCurrentUserName();
  return <div>HELLO {currentUser.toUpperCase()}!</div>;
}

function App() {
  return (
    <CurrentUserProvider value="Anders">
      <EnthusasticGreeting />
    </CurrentUserProvider>
  );
}   
3、可以更進(jìn)一步,使用createContext和context getters結(jié)合這個(gè)想法。
import { createContext, useContext } from "react";

/**
* 創(chuàng)建上下文和提供者的助手,沒(méi)有預(yù)先的默認(rèn)值,并且
* 無(wú)需一直檢查未定義。
*/
function createCtx<A extends {} | null>() {
  const ctx = createContext<A | undefined>(undefined);
  function useCtx() {
    const c = useContext(ctx);
    if (c === undefined)
      throw new Error("useCtx must be inside a Provider with a value");
    return c;
  }
  return [useCtx, ctx.Provider] as const; // 'as const' 使 TypeScript 推斷出一個(gè)元組
}

// 用法

export const [useCtx, SettingProvider] = createCtx<string>();  // 指定類(lèi)型,但不需要預(yù)先指定值
export function App() {
  const key = useCustomHook("key"); // 從鉤子中獲取值,必須在組件中
  return (
    <SettingProvider value={key}>
      <Component />
    </SettingProvider>
  );
}
export function Component() {
  const key = useCtx(); // 仍然可以在沒(méi)有空檢查的情況下使用!
  return <div>{key}</div>;
}
4、使用createContext and useContext制作一個(gè)createCtx with  unstated-like 上下文設(shè)置器:
import {
  createContext,
  Dispatch,
  PropsWithChildren,
  SetStateAction,
  useState,
} from "react";

export function createCtx<A>(defaultValue: A) {
  type UpdateType = Dispatch<SetStateAction<typeof defaultValue>>;
  const defaultUpdate: UpdateType = () => defaultValue;
  const ctx = createContext({
    state: defaultValue,
    update: defaultUpdate,
  });

  function Provider(props: PropsWithChildren<{}>) {
    const [state, update] = useState(defaultValue);
    return <ctx.Provider value={{ state, update }} {...props} />;
  }
  return [ctx, Provider] as const;  // 或者,[typeof ctx, typeof Provider]
}

// 用法
import { useContext } from "react";

const [ctx, TextProvider] = createCtx("someText");
export const TextContext = ctx;
export function App() {
  return (
    <TextProvider>
      <Component />
    </TextProvider>
  );
}
export function Component() {
  const { state, update } = useContext(TextContext);
  return (
    <label>
      {state}
      <input type="text" onChange={(e) => update(e.target.value)} />
    </label>
  );
}
八、forwardRef/createRef
檢查Hooks 部分的useRef.

createRef:

import { createRef, PureComponent } from "react";

class CssThemeProvider extends PureComponent<Props> {
  private rootRef = createRef<HTMLDivElement>(); // 像這樣
  render() {
    return <div ref={this.rootRef}>{this.props.children}</div>;
  }
}
forwardRef:
import { forwardRef, ReactNode } from "react";

interface Props {
  children?: ReactNode;
  type: "submit" | "button";
}
export type Ref = HTMLButtonElement;

export const FancyButton = forwardRef<Ref, Props>((props, ref) => (
  <button ref={ref} className="MyClassName" type={props.type}>
    {props.children}
  </button>
));
通用 forwardRefs
1 - Wrapper component
type ClickableListProps<T> = {
  items: T[];
  onSelect: (item: T) => void;
  mRef?: React.Ref<HTMLUListElement> | null;
};

export function ClickableList<T>(props: ClickableListProps<T>) {
  return (
    <ul ref={props.mRef}>
      {props.items.map((item, i) => (
        <li key={i}>
          <button onClick={(el) => props.onSelect(item)}>Select</button>
          {item}
        </li>
      ))}
    </ul>
  );
}
2 - Redeclare forwardRef
// 重新聲明 forwardRef
declare module "react" {
  function forwardRef<T, P = {}>(
    render: (props: P, ref: React.Ref<T>) => React.ReactElement | null
  ): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
}

// 只需像以前一樣編寫(xiě)組件!
import { forwardRef, ForwardedRef } from "react";

interface ClickableListProps<T> {
  items: T[];
  onSelect: (item: T) => void;
}

function ClickableListInner<T>(
  props: ClickableListProps<T>,
  ref: ForwardedRef<HTMLUListElement>
) {
  return (
    <ul ref={ref}>
      {props.items.map((item, i) => (
        <li key={i}>
          <button onClick={(el) => props.onSelect(item)}>Select</button>
          {item}
        </li>
      ))}
    </ul>
  );
}

export const ClickableList = forwardRef(ClickableListInner);






九、有用的hooks
useLocalStorage
import { useState } from "react";

// 用法
function App() {
  // 類(lèi)似于 useState 但第一個(gè) arg 是本地存儲(chǔ)中值的鍵。
  const [name, setName] = useLocalStorage<string>("name", "Bob");

  return (
    <div>
      <input
        type="text"
        placeholder="Enter your name"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
    </div>
  );
}

// Hook
function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((val: T) => T)) => void] {
  // 狀態(tài)來(lái)存儲(chǔ)我們的值
  // 將初始狀態(tài)函數(shù)傳遞給 useState,因此邏輯只執(zhí)行一次
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      // 按鍵從本地存儲(chǔ)中獲取
      const item = window.localStorage.getItem(key);
      // 解析存儲(chǔ)的 json 或者如果沒(méi)有則返回 initialValue
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      // 如果錯(cuò)誤也返回initialValue
      console.log(error);
      return initialValue;
    }
  });

  // 返回 useState 的 setter 函數(shù)的包裝版本,它...
  // ... 將新值保存到 localStorage。
  const setValue = (value: T | ((val: T) => T)) => {
    try {
      // 允許 value 是一個(gè)函數(shù),所以我們有與 useState 相同的 API
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      // 保存狀態(tài)
      setStoredValue(valueToStore);
      // 保存到本地存儲(chǔ)
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      // 更高級(jí)的實(shí)現(xiàn)將處理錯(cuò)誤情況
      console.log(error);
    }
  };

  return [storedValue, setValue];
}
useMedia
import { useState, useEffect } from 'react';

function App() {
  const columnCount = useMedia<number>(
    // 媒體查詢(xún)
    ['(min-width: 1500px)', '(min-width: 1000px)', '(min-width: 600px)'],
    // 列數(shù)(與上述按數(shù)組索引的媒體查詢(xún)有關(guān))
    [5, 4, 3],
    // 默認(rèn)列數(shù)
    2
  );

  // 創(chuàng)建列高數(shù)組(從 0 開(kāi)始)
  let columnHeights = new Array(columnCount).fill(0);

  // 創(chuàng)建包含每列項(xiàng)目的數(shù)組數(shù)組
  let columns = new Array(columnCount).fill().map(() => []) as Array<DataProps[]>;

  (data as DataProps[]).forEach(item => {
    // 獲取最短列的索引
    const shortColumnIndex = columnHeights.indexOf(Math.min(...columnHeights));
    // 添加項(xiàng)目
    columns[shortColumnIndex].push(item);
    // 更新高度
    columnHeights[shortColumnIndex] += item.height;
  });

  // 渲染列和項(xiàng)目
  return (
    <div className="App">
      <div className="columns is-mobile">
        {columns.map(column => (
          <div className="column">
            {column.map(item => (
              <div
                className="image-container"
                style={{
                  // 將圖像容器大小調(diào)整為圖像的縱橫比
                  paddingTop: (item.height / item.width) * 100 + '%'
                }}
              >
                <img src={item.image} alt="" />
              </div>
            ))}
          </div>
        ))}
      </div>
    </div>
  );
}

// Hook
const useMedia = <T>(queries: string[], values: T[], defaultValue: T) => {
   // 包含每個(gè)查詢(xún)的媒體查詢(xún)列表的數(shù)組
  const mediaQueryLists = queries.map(q => window.matchMedia(q));

  // 根據(jù)匹配的媒體查詢(xún)獲取值的函數(shù)
  const getValue = () => {
    // 獲取第一個(gè)匹配的媒體查詢(xún)的索引
    const index = mediaQueryLists.findIndex(mql => mql.matches);
    // 返回相關(guān)值,如果沒(méi)有則返回默認(rèn)值
    return values?.[index] || defaultValue;
  };

  // 匹配值的狀態(tài)和設(shè)置器
  const [value, setValue] = useState<T>(getValue);

  useEffect(
    () => {
      // 事件監(jiān)聽(tīng)回調(diào)
      // 注意:通過(guò)在 useEffect 之外定義 getValue,我們確保它具有 ...
      // ... 鉤子參數(shù)的當(dāng)前值(因?yàn)檫@個(gè)鉤子回調(diào)在掛載時(shí)創(chuàng)建一次)。
      const handler = () => setValue(getValue);
      // 使用上述處理程序?yàn)槊總€(gè)媒體查詢(xún)?cè)O(shè)置一個(gè)偵聽(tīng)器作為回調(diào)。
      mediaQueryLists.forEach(mql => mql.addListener(handler));
      // 在清理時(shí)移除監(jiān)聽(tīng)器
      return () => mediaQueryLists.forEach(mql => mql.removeListener(handler));
    },
    [] // 空數(shù)組確保效果僅在掛載和卸載時(shí)運(yùn)行
  );

  return value;
}
useAsyncTask
// 用法
const task = useAsyncTask(async (data: any) => await myApiRequest(data));
task.run(data);
useEffect(() => {
  console.log(task.status); // 'IDLE' | 'PROCESSING' | 'ERROR' | 'SUCCESS';
}, [task.status]);

// 執(zhí)行

import { useCallback, useState } from "react";

type TStatus = "IDLE" | "PROCESSING" | "ERROR" | "SUCCESS";

function useAsyncTask<T extends any[], R = any>(
  task: (...args: T) => Promise<R>
) {
  const [status, setStatus] = useState<TStatus>("IDLE");
  const [message, setMessage] = useState("");

  const run = useCallback(async (...arg: T) => {
    setStatus("PROCESSING");
    try {
      const resp: R = await task(...arg);
      setStatus("SUCCESS");
      return resp;
    } catch (error) {
      let message = error?.response?.data?.error?.message || error.message;
      setMessage(message);
      setStatus("ERROR");
      throw error;
    }
  }, []);

  const reset = useCallback(() => {
    setMessage("");
    setStatus("IDLE");
  }, []);

  return {
    run,
    status,
    message,
    reset,
  };
}

export default useAsyncTask;
useFetch
export function useFetch(request: RequestInfo, init?: RequestInit) {
  const [response, setResponse] = useState<null | Response>(null);
  const [error, setError] = useState<Error | null>();
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const abortController = new AbortController();
    setIsLoading(true);
    (async () => {
      try {
        const response = await fetch(request, {
          ...init,
          signal: abortController.signal,
        });
        setResponse(await response?.json());
        setIsLoading(false);
      } catch (error) {
        if (isAbortError(error)) {
          return;
        }
        setError(error as any);
        setIsLoading(false);
      }
    })();
    return () => {
      abortController.abort();
    };
  }, [init, request]);

  return { response, error, isLoading };
}

// type guards
function isAbortError(error: any): error is DOMException {
  if (error && error.name === "AbortError") {
    return true;
  }
  return false;
}
十、HOC
一個(gè) HOC 示例

注入props

interface WithThemeProps {
  primaryColor: string;
}
在組件中的使用

在組件的接口上提供可用的props,但在包裝在 HoC 中時(shí)為組件的消費(fèi)者減去。

interface Props extends WithThemeProps {
  children?: React.ReactNode;
}

class MyButton extends React.Component<Props> {
  public render() {
    // 使用主題和其他props渲染元素。
  }

  private someInternalMethod() {
    // 主題值也可在此處作為props使用。
  }
}

export default withTheme(MyButton);
使用組件

現(xiàn)在,在使用組件時(shí),可以省略primaryColor props或覆蓋通過(guò)上下文提供的props。

<MyButton>Hello button</MyButton> // 有效

<MyButton primaryColor="#333">Hello Button</MyButton> // 同樣有效
聲明 HoC

實(shí)際的 HoC。

export function withTheme<T extends WithThemeProps = WithThemeProps>(
  WrappedComponent: React.ComponentType<T>
) {
   // 嘗試為 React 開(kāi)發(fā)工具創(chuàng)建一個(gè)不錯(cuò)的 displayName。
  const displayName =
    WrappedComponent.displayName || WrappedComponent.name || "Component";

  // 創(chuàng)建內(nèi)部組件。這里計(jì)算出來(lái)的 Props 類(lèi)型是魔法發(fā)生的地方。
  const ComponentWithTheme = (props: Omit<T, keyof WithThemeProps>) => {
    // 獲取要注入的props。這可以通過(guò)上下文來(lái)完成。
    const themeProps = useTheme();

    // props隨后出現(xiàn),因此可以覆蓋默認(rèn)值。
    return <WrappedComponent {...themeProps} {...(props as T)} />;
  };

  ComponentWithTheme.displayName = `withTheme(${displayName})`;

  return ComponentWithTheme;
}
這是一個(gè)更高級(jí)的動(dòng)態(tài)高階組件示例,它的一些參數(shù)基于傳入的組件的 props:
// 向組件注入靜態(tài)值,以便始終提供它們
export function inject<TProps, TInjectedKeys extends keyof TProps>(
  Component: React.JSXElementConstructor<TProps>,
  injector: Pick<TProps, TInjectedKeys>
) {
  return function Injected(props: Omit<TProps, TInjectedKeys>) {
    return <Component {...(props as TProps)} {...injector} />;
  };
}
使用forwardRef
對(duì)于“真正的”可重用性,還應(yīng)該考慮為 HOC 公開(kāi)一個(gè) ref。
十一、Linting
yarn add -D @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint
將lint腳本添加到您的package.json:
"scripts": {
    "lint": "eslint 'src/**/*.ts'"
  },
一個(gè)合適的.eslintrc.js
module.exports = {
  env: {
    es6: true,
    node: true,
    jest: true,
  },
  extends: "eslint:recommended",
  parser: "@typescript-eslint/parser",
  plugins: ["@typescript-eslint"],
  parserOptions: {
    ecmaVersion: 2017,
    sourceType: "module",
  },
  rules: {
    indent: ["error", 2],
    "linebreak-style": ["error", "unix"],
    quotes: ["error", "single"],
    "no-console": "warn",
    "no-unused-vars": "off",
    "@typescript-eslint/no-unused-vars": [
      "error",
      { vars: "all", args: "after-used", ignoreRestSiblings: false },
    ],
    "@typescript-eslint/explicit-function-return-type": "warn", // 考慮對(duì)對(duì)象字面量和函數(shù)返回類(lèi)型使用顯式注釋?zhuān)词顾鼈兛梢员煌茢喑鰜?lái)。  
    "no-empty": "warn",
  },

};
更多.eslintrc.json選項(xiàng)需要考慮,可能需要更多應(yīng)用選項(xiàng):
{
  "extends": [
    "airbnb",
    "prettier",
    "prettier/react",
    "plugin:prettier/recommended",
    "plugin:jest/recommended",
    "plugin:unicorn/recommended"
  ],
  "plugins": ["prettier", "jest", "unicorn"],
  "parserOptions": {
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "env": {
    "es6": true,
    "browser": true,
    "jest": true
  },
  "settings": {
    "import/resolver": {
      "node": {
        "extensions": [".js", ".jsx", ".ts", ".tsx"]
      }
    }
  },
  "overrides": [
    {
      "files": ["**/*.ts", "**/*.tsx"],
      "parser": "typescript-eslint-parser",
      "rules": {
        "no-undef": "off"
      }
    }
  ]
}

十二、最后
在我們閱讀完官方文檔后,我們一定會(huì)進(jìn)行更深層次的學(xué)習(xí),比如看下框架底層是如何運(yùn)行的,以及源碼的閱讀。
    這里廣東靚仔給下一些小建議:
在看源碼前,我們先去官方文檔復(fù)習(xí)下框架設(shè)計(jì)理念、源碼分層設(shè)計(jì)
閱讀下框架官方開(kāi)發(fā)人員寫(xiě)的相關(guān)文章
借助框架的調(diào)用棧來(lái)進(jìn)行源碼的閱讀,通過(guò)這個(gè)執(zhí)行流程,我們就完整的對(duì)源碼進(jìn)行了一個(gè)初步的了解
接下來(lái)再對(duì)源碼執(zhí)行過(guò)程中涉及的所有函數(shù)邏輯梳理一遍








作者:廣東靚仔

歡迎關(guān)注:前端早茶