React + TypeScript 常用类型汇总(下)

简介: 本文适合对TypeScript感兴趣的小伙伴阅读~

七、Context


基本示例

import { createContext } from "react";
interface AppContextInterface {
  name: string;
  author: string;
  url: string;
}
const AppCtx = createContext<AppContextInterface | null>(null);
// 应用程序中的提供程序
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>
);
// 在你的应用中使用
import { useContext } from "react";
export const PostInfo = () => {
  const appContext = useContext(AppCtx);
  return (
    <div>
      Name: {appContext.name}, Author: {appContext.author}, Url:{" "}
      {appContext.url}
    </div>
  );
};


扩展示例使用createContext空对象作为默认值

interface ContextState {
  // 使用上下文设置你想要处理的状态类型,例如
  name: string | null;
}
// 设置一个空对象为默认状态
const Context = createContext({} as ContextState);
// 像在 JavaScript 中一样设置上下文提供程序


使用createContext 和 context getters来制作 a createCtx with no ,但无需检查:

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


注意我们需要的显式类型参数,因为我们没有默认string值:

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


连同非空断言告诉 TypeScript currentUser肯定会在那里:

return <div>HELLO {currentUser!.toUpperCase()}!</div>;
//


这是不幸的,因为我们知道稍后在我们的应用程序中,a Provider将填充上下文。

有几个解决方案:


1、可以通过断言非空来解决这个问题:

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


2、我们可以编写一个名为的辅助函数createCtx来防止访问Context未提供值的 a。通过这样做,API 相反,我们不必提供默认值,也不必检查:

import { createContext, useContext } from "react";
/**
* 创建上下文和提供者的助手,没有预先的默认值,并且
* 无需一直检查未定义。
*/
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 推断出一个元组 
}
// 用法:
// 我们仍然需要指定一个类型,但没有默认值!
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、可以更进一步,使用createContext和context getters结合这个想法。

import { createContext, useContext } from "react";
/**
* 创建上下文和提供者的助手,没有预先的默认值,并且
* 无需一直检查未定义。
*/
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 推断出一个元组
}
// 用法
export const [useCtx, SettingProvider] = createCtx<string>();  // 指定类型,但不需要预先指定值
export function App() {
  const key = useCustomHook("key"); // 从钩子中获取值,必须在组件中
  return (
    <SettingProvider value={key}>
      <Component />
    </SettingProvider>
  );
}
export function Component() {
  const key = useCtx(); // 仍然可以在没有空检查的情况下使用!
  return <div>{key}</div>;
}


4、使用createContext and useContext制作一个createCtx with  unstated-like 上下文设置器:

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


通用 forwardRefs1 - 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;
}
// 只需像以前一样编写组件!
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() {
  // 类似于 useState 但第一个 arg 是本地存储中值的键。
  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] {
  // 状态来存储我们的值
  // 将初始状态函数传递给 useState,因此逻辑只执行一次
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      // 按键从本地存储中获取
      const item = window.localStorage.getItem(key);
      // 解析存储的 json 或者如果没有则返回 initialValue
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      // 如果错误也返回initialValue
      console.log(error);
      return initialValue;
    }
  });
  // 返回 useState 的 setter 函数的包装版本,它...
  // ... 将新值保存到 localStorage。
  const setValue = (value: T | ((val: T) => T)) => {
    try {
      // 允许 value 是一个函数,所以我们有与 useState 相同的 API
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      // 保存状态
      setStoredValue(valueToStore);
      // 保存到本地存储
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      // 更高级的实现将处理错误情况
      console.log(error);
    }
  };
  return [storedValue, setValue];
}


useMedia

import { useState, useEffect } from 'react';
function App() {
  const columnCount = useMedia<number>(
    // 媒体查询
    ['(min-width: 1500px)', '(min-width: 1000px)', '(min-width: 600px)'],
    // 列数(与上述按数组索引的媒体查询有关)
    [5, 4, 3],
    // 默认列数
    2
  );
  // 创建列高数组(从 0 开始)
  let columnHeights = new Array(columnCount).fill(0);
  // 创建包含每列项目的数组数组
  let columns = new Array(columnCount).fill().map(() => []) as Array<DataProps[]>;
  (data as DataProps[]).forEach(item => {
    // 获取最短列的索引
    const shortColumnIndex = columnHeights.indexOf(Math.min(...columnHeights));
    // 添加项目
    columns[shortColumnIndex].push(item);
    // 更新高度
    columnHeights[shortColumnIndex] += item.height;
  });
  // 渲染列和项目
  return (
    <div className="App">
      <div className="columns is-mobile">
        {columns.map(column => (
          <div className="column">
            {column.map(item => (
              <div
                className="image-container"
                style={{
                  // 将图像容器大小调整为图像的纵横比
                  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) => {
   // 包含每个查询的媒体查询列表的数组
  const mediaQueryLists = queries.map(q => window.matchMedia(q));
  // 根据匹配的媒体查询获取值的函数
  const getValue = () => {
    // 获取第一个匹配的媒体查询的索引
    const index = mediaQueryLists.findIndex(mql => mql.matches);
    // 返回相关值,如果没有则返回默认值
    return values?.[index] || defaultValue;
  };
  // 匹配值的状态和设置器
  const [value, setValue] = useState<T>(getValue);
  useEffect(
    () => {
      // 事件监听回调
      // 注意:通过在 useEffect 之外定义 getValue,我们确保它具有 ...
      // ... 钩子参数的当前值(因为这个钩子回调在挂载时创建一次)。
      const handler = () => setValue(getValue);
      // 使用上述处理程序为每个媒体查询设置一个侦听器作为回调。
      mediaQueryLists.forEach(mql => mql.addListener(handler));
      // 在清理时移除监听器
      return () => mediaQueryLists.forEach(mql => mql.removeListener(handler));
    },
    [] // 空数组确保效果仅在挂载和卸载时运行
  );
  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]);
// 执行
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


一个 HOC 示例

注入props

interface WithThemeProps {
  primaryColor: string;
}


在组件中的使用

在组件的接口上提供可用的props,但在包装在 HoC 中时为组件的消费者减去。

interface Props extends WithThemeProps {
  children?: React.ReactNode;
}
class MyButton extends React.Component<Props> {
  public render() {
    // 使用主题和其他props渲染元素。
  }
  private someInternalMethod() {
    // 主题值也可在此处作为props使用。
  }
}
export default withTheme(MyButton);


使用组件

现在,在使用组件时,可以省略primaryColor props或覆盖通过上下文提供的props。

<MyButton>Hello button</MyButton> // 有效
<MyButton primaryColor="#333">Hello Button</MyButton> // 同样有效


声明 HoC

实际的 HoC。

export function withTheme<T extends WithThemeProps = WithThemeProps>(
  WrappedComponent: React.ComponentType<T>
) {
   // 尝试为 React 开发工具创建一个不错的 displayName。
  const displayName =
    WrappedComponent.displayName || WrappedComponent.name || "Component";
  // 创建内部组件。这里计算出来的 Props 类型是魔法发生的地方。
  const ComponentWithTheme = (props: Omit<T, keyof WithThemeProps>) => {
    // 获取要注入的props。这可以通过上下文来完成。
    const themeProps = useTheme();
    // props随后出现,因此可以覆盖默认值。
    return <WrappedComponent {...themeProps} {...(props as T)} />;
  };
  ComponentWithTheme.displayName = `withTheme(${displayName})`;
  return ComponentWithTheme;
}


这是一个更高级的动态高阶组件示例,它的一些参数基于传入的组件的 props:

// 向组件注入静态值,以便始终提供它们
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对于“真正的”可重用性,还应该考虑为 HOC 公开一个 ref。


十一、Linting


yarn add -D @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint


将lint脚本添加到您的package.json:

"scripts": {
    "lint": "eslint 'src/**/*.ts'"
  },


一个合适的.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", // 考虑对对象字面量和函数返回类型使用显式注释,即使它们可以被推断出来。  
    "no-empty": "warn",
  },
};


更多.eslintrc.json选项需要考虑,可能需要更多应用选项:

{
  "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"
      }
    }
  ]
}


十二、最后


在我们阅读完官方文档后,我们一定会进行更深层次的学习,比如看下框架底层是如何运行的,以及源码的阅读。    


这里广东靓仔给下一些小建议:

  • 在看源码前,我们先去官方文档复习下框架设计理念、源码分层设计
  • 阅读下框架官方开发人员写的相关文章
  • 借助框架的调用栈来进行源码的阅读,通过这个执行流程,我们就完整的对源码进行了一个初步的了解
  • 接下来再对源码执行过程中涉及的所有函数逻辑梳理一遍
相关文章
|
2月前
|
JavaScript 前端开发 安全
深入理解TypeScript:增强JavaScript的类型安全性
【10月更文挑战第8天】深入理解TypeScript:增强JavaScript的类型安全性
64 0
|
2月前
|
JavaScript 前端开发 开发者
深入理解TypeScript:类型系统与实用技巧
【10月更文挑战第8天】深入理解TypeScript:类型系统与实用技巧
|
29天前
|
设计模式 JavaScript 安全
TypeScript性能优化及代码质量提升的重要性、方法与策略,包括合理使用类型注解、减少类型断言、优化模块导入导出、遵循编码规范、加强代码注释等
本文深入探讨了TypeScript性能优化及代码质量提升的重要性、方法与策略,包括合理使用类型注解、减少类型断言、优化模块导入导出、遵循编码规范、加强代码注释等,旨在帮助开发者在保证代码质量的同时,实现高效的性能优化,提升用户体验和项目稳定性。
42 6
|
28天前
|
开发框架 JavaScript 前端开发
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势。通过明确的类型定义,TypeScript 能够在编码阶段发现潜在错误,提高代码质量;支持组件的清晰定义与复用,增强代码的可维护性;与 React、Vue 等框架结合,提供更佳的开发体验;适用于大型项目,优化代码结构和性能。随着 Web 技术的发展,TypeScript 的应用前景广阔,将继续引领 Web 开发的新趋势。
36 2
|
1月前
|
前端开发 安全
如何使用类型参数来创建 React 泛型组件?
通过以上步骤,就可以使用类型参数来创建灵活、可复用且类型安全的React泛型组件,以满足不同的数据类型和业务需求。
|
1月前
|
JavaScript 安全 前端开发
TypeScript类型声明:基础与进阶
通过本文的介绍,我们详细探讨了TypeScript的基础与进阶类型声明。从基本数据类型到复杂的泛型和高级类型,TypeScript提供了丰富的工具来确保代码的类型安全和可维护性。掌握这些类型声明能够帮助开发者编写更加健壮和高效的代码,提高开发效率和代码质量。希望本文能为您在使用TypeScript时提供实用的参考和指导。
38 2
|
1月前
|
JavaScript 开发者
在 Babel 插件中使用 TypeScript 类型
【10月更文挑战第23天】可以在 Babel 插件中更有效地使用 TypeScript 类型,提高插件的开发效率和质量,减少潜在的类型错误。同时,也有助于提升代码的可理解性和可维护性,使插件的功能更易于扩展和升级。
|
1月前
|
前端开发 JavaScript
手敲Webpack 5:React + TypeScript项目脚手架搭建实践
手敲Webpack 5:React + TypeScript项目脚手架搭建实践
|
2月前
|
JavaScript 前端开发
TypeScript【类型别名、泛型】超简洁教程!再也不用看臭又长的TypeScript文档了!
【10月更文挑战第11天】TypeScript【类型别名、泛型】超简洁教程!再也不用看臭又长的TypeScript文档了!
|
2月前
|
JavaScript 前端开发 安全
TypeScript【基础类型】超简洁教程!再也不用看臭又长的TypeScript文档了!
【10月更文挑战第9天】TypeScript【基础类型】超简洁教程!再也不用看臭又长的TypeScript文档了!