用 TypeScript 写 React & Redux - 完全指南

限时加码!20+主流AI编程工具免费用 购周边加赠Coding Plan Lite,Claude Code、Cursor等即刻畅享,学习进阶更高效! 阅读详情

“这个指南是一个最新的摘要,记录了关于如何用TypeScript 函数式风格使用React(以及相关生态)最重要的模式和示例。它会使你的代码在从具体实现中进行类型推导时绝对是类型安全的,这样就能减少来自过度类型声明的信息噪音,并更容易写出易于长期维护的正确类型声明。”

目标

  • 完全的类型安全(支持--strict模式),并且在向应用的下游代码传递时,不会丢失类型信息(比如:缺少类型断言或用 any 来强行使用)
  • 使用高级 TypeScript 语言特性(诸如类型推论控制流分析)来消除类型冗余、使类型声明简明扼要
  • 使用专门的 TypeScript 补充库 来减少类型代码的重复性和复杂度

React、Redux、Typescript 生态系统

  • typesafe-actions - 为 Redux / Flux 架构中 “action-creators” 创造的类型安全实用工具集
  • utility-types - TypeScript 常用泛型集合,能够补充 TS 自带的映射类型和别名 - 把它当成类型复用的 lodash
  • react-redux-typescript-scripts - 开发者工具配置文件,可用于遵循本指南的项目

示例


🌟 - 新内容及更新板块

目录


安装

React & Redux 的类型定义

npm i -D @types/react @types/react-dom @types/react-redux

“react” - @types/react
“react-dom” - @types/react-dom
“redux” - (types included with npm package)*
“react-redux” - @types/react-redux

*提示: 本指南的类型系统适用于 Redux >= v4.x.x。如果想用于 Redux v3.x.x 请查看 这个配置)

⇧ 返回顶部


React - 类型定义速查表

React.FC<Props> | React.FunctionComponent<Props>

表示函数组件的类型

const MyComponent: React.FC<Props> = ...

React.Component<Props, State>

表示class组件的类型

class MyComponent extends React.Component<Props, State> { ...}

React.ComponentType<Props>

表示 (React.FC | React.Component) 集合的类型 - 用于 HOC

const withState = <P extends WrappedComponentProps>(
  WrappedComponent: React.ComponentType<P>,
) => { ...}

React.ComponentProps<typeof XXX>

取得组件 XXX 的 Props 类型(警告:无法用于静态声明的 default props 以及泛型 props)

type MyComponentProps = React.ComponentProps<typeof MyComponent>;

React.ReactElement | JSX.Element

表示 React 中 Element 概念的类型 - 表示一个原生 DOM 组件(比如 <div />)或用户自定义的复合组件 (比如 <MyComponent />

const elementOnly: React.ReactElement = <div /> || <MyComponent />;

React.ReactNode

表示任意类型的 React 节点(相当于 ReactElement (包括 Fragments 和 Portals) + 原始 JS 类型)

const elementOrPrimitive: React.ReactNode = 'string' || 0 || false || null || undefined || <div /> || <MyComponent />;
const Component = ({ children: React.ReactNode }) => ...

React.CSSProperties

表示 JSX 中样式对象的类型 - 实现 css-in-js 风格

const styles: React.CSSProperties = { flexDirection: 'row', ...
const element = <div style={styles} ...

React.HTMLProps<HTMLXXXElement>

表示指定 HTML 元素的类型 - 用于扩展 HTML 元素

const Input: React.FC<Props & React.HTMLProps<HTMLInputElement>> = props => { ... }
<Input about={...} accept={...} alt={...} ... />

React.ReactEventHandler<HTMLXXXElement>

表示 event handler 的泛型类型 - 用于声明 event handlers

const handleChange: React.ReactEventHandler<HTMLInputElement> = (ev) => { ... } 
<input onChange={handleChange} ... />

React.XXXEvent<HTMLXXXElement>

表示更多特殊 event。一些常见的 event 例如:ChangeEvent, FormEvent, FocusEvent, KeyboardEvent, MouseEvent, DragEvent, PointerEvent, WheelEvent, TouchEvent

const handleChange = (ev: React.MouseEvent<HTMLDivElement>) => { ... }
<div onMouseMove={handleChange} ... />

上一段代码中的 React.MouseEvent<HTMLDivElement> 表示鼠标事件的类型,这个事件挂载在 HTMLDivElement 上。

⇧ 返回顶部


React - 类型模式

Function Components - FC

- 计数器组件

import * as React from 'react';
type Props = {
  label: string;
  count: number;
  onIncrement: () => void;
};
export const FCCounter: React.FC<Props> = props => {
  const { label, count, onIncrement } = props;
  const handleIncrement = () => {
    onIncrement();
  };
  return (
    <div>
      <span>
        {label}: {count}
      </span>
      <button type="button" onClick={handleIncrement}>
        {`Increment`}
      </button>
    </div>
  );
};

⟩⟩⟩ demo

⇧ 返回顶部


- 组件的 属性展开

import * as React from 'react';
type Props = {
  className?: string;
  style?: React.CSSProperties;
};
export const FCSpreadAttributes: React.FC<Props> = props => {
  const { children, ...restProps } = props;
  return <div {...restProps}>{children}</div>;
};

⟩⟩⟩ demo

⇧ 返回顶部


Class Components

- 计数器组件 Class 版

import * as React from 'react';
type Props = {
  label: string;
};
type State = {
  count: number;
};
export class ClassCounter extends React.Component<Props, State> {
  readonly state: State = {
    count: 0,
  };
  handleIncrement = () => {
    this.setState({ count: this.state.count + 1 });
  };
  render() {
    const { handleIncrement } = this;
    const { label } = this.props;
    const { count } = this.state;
    return (
      <div>
        <span>
          {label}: {count}
        </span>
        <button type="button" onClick={handleIncrement}>
          {`Increment`}
        </button>
      </div>
    );
  }
}

⟩⟩⟩ demo

⇧ 返回顶部


- Class 组件和 default props

import * as React from 'react';
type Props = {
  label: string;
  initialCount: number;
};
type State = {
  count: number;
};
export class ClassCounterWithDefaultProps extends React.Component<
  Props,
  State
> {
  static defaultProps = {
    initialCount: 0,
  };
  readonly state: State = {
    count: this.props.initialCount,
  };
  handleIncrement = () => {
    this.setState({ count: this.state.count + 1 });
  };
  render() {
    const { handleIncrement } = this;
    const { label } = this.props;
    const { count } = this.state;
    return (
      <div>
        <span>
          {label}: {count}
        </span>
        <button type="button" onClick={handleIncrement}>
          {`Increment`}
        </button>
      </div>
    );
  }
}

⟩⟩⟩ demo

⇧ 返回顶部


泛型组件

  • 易于生成不同类型的变种组件,同时复用公共逻辑
  • 常见的用例是泛型列表组件

- 泛型列表组件

import * as React from 'react';
export interface GenericListProps<T> {
  items: T[];
  itemRenderer: (item: T) => JSX.Element;
}
export class GenericList<T> extends React.Component<GenericListProps<T>, {}> {
  render() {
    const { items, itemRenderer } = this.props;
    return (
      <div>
        {items.map(itemRenderer)}
      </div>
    );
  }
}

⟩⟩⟩ demo

⇧ 返回顶部


Render Props

https://zh-hans.reactjs.org/docs/render-props.html

- Name Provider 组件

将 children 用作 render prop 的简单组件

import * as React from 'react';
interface NameProviderProps {
  children: (state: NameProviderState) => React.ReactNode;
}
interface NameProviderState {
  readonly name: string;
}
export class NameProvider extends React.Component<NameProviderProps, NameProviderState> {
  readonly state: NameProviderState = { name: 'Piotr' };
  render() {
    return this.props.children(this.state);
  }
}

⟩⟩⟩ demo

⇧ 返回顶部


- Mouse Provider 组件

Mouse 组件的例子来源于 Render Props - React 文档

import * as React from 'react';
export interface MouseProviderProps {
  render: (state: MouseProviderState) => React.ReactNode;
}
interface MouseProviderState {
  readonly x: number;
  readonly y: number;
}
export class MouseProvider extends React.Component<MouseProviderProps, MouseProviderState> {
  readonly state: MouseProviderState = { x: 0, y: 0 };
  handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
    this.setState({
      x: event.clientX,
      y: event.clientY,
    });
  };
  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        {/*
          Instead of providing a static representation of what <Mouse> renders,
          use the `render` prop to dynamically determine what to render.
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

⟩⟩⟩ demo

⇧ 返回顶部


高阶组件

https://zh-hans.reactjs.org/docs/higher-order-components.html

- 用 HOC 封装一个组件

给无状态的计数器加上状态

import React from 'react';
import { Diff } from 'utility-types';
// These props will be injected into the base component
interface InjectedProps {
  count: number;
  onIncrement: () => void;
}
export const withState = <BaseProps extends InjectedProps>(
  BaseComponent: React.ComponentType<BaseProps>
) => {
  type HocProps = Diff<BaseProps, InjectedProps> & {
    // here you can extend hoc with new props
    initialCount?: number;
  };
  type HocState = {
    readonly count: number;
  };
  return class Hoc extends React.Component<HocProps, HocState> {
    // Enhance component name for debugging and React-Dev-Tools
    static displayName = `withState(${BaseComponent.name})`;
    // reference to original wrapped component
    static readonly WrappedComponent = BaseComponent;

    readonly state: HocState = {
      count: Number(this.props.initialCount) || 0,
    };
    handleIncrement = () => {
      this.setState({ count: this.state.count + 1 });
    };
    render() {
      const { ...restProps } = this.props;
      const { count } = this.state;
      return (
        <BaseComponent
          count={count} // injected
          onIncrement={this.handleIncrement} // injected
          {...(restProps as BaseProps)}
        />
      );
    }
  };
};
import * as React from 'react';
import { withState } from '../hoc';
import { FCCounter } from '../components';
const FCCounterWithState = withState(FCCounter);
export default () => <FCCounterWithState label={'FCCounterWithState'} />;

⇧ 返回顶部


- 用 HOC 封装组件并注入 props

用 componentDidCatch 给任意组件加上错误处理功能

import React from 'react';
const MISSING_ERROR = 'Error was swallowed during propagation.';
export const withErrorBoundary = <BaseProps extends {}>(
  BaseComponent: React.ComponentType<BaseProps>
) => {
  type HocProps = {
    // here you can extend hoc with new props
  };
  type HocState = {
    readonly error: Error | null | undefined;
  };
  return class Hoc extends React.Component<HocProps, HocState> {
    // Enhance component name for debugging and React-Dev-Tools
    static displayName = `withErrorBoundary(${BaseComponent.name})`;
    // reference to original wrapped component
    static readonly WrappedComponent = BaseComponent;
    readonly state: HocState = {
      error: undefined,
    };
    componentDidCatch(error: Error | null, info: object) {
      this.setState({ error: error || new Error(MISSING_ERROR) });
      this.logErrorToCloud(error, info);
    }
    logErrorToCloud = (error: Error | null, info: object) => {
      // TODO: send error report to service provider
    };
    render() {
      const { children, ...restProps } = this.props;
      const { error } = this.state;
      if (error) {
        return <BaseComponent {...(restProps as BaseProps)} />;
      }
      return children;
    }
  };
};
import React, {useState} from 'react';
import { withErrorBoundary } from '../hoc';
import { ErrorMessage } from '../components';
const ErrorMessageWithErrorBoundary =
  withErrorBoundary(ErrorMessage);
const BrokenComponent = () => {
  throw new Error('I\'m broken! Don\'t render me.');
};
const BrokenButton = () => {
  const [shouldRenderBrokenComponent, setShouldRenderBrokenComponent] =
    useState(false);
  if (shouldRenderBrokenComponent) {
    return <BrokenComponent />;
  }
  return (
    <button
      type="button"
      onClick={() => {
        setShouldRenderBrokenComponent(true);
      }}
    >
      {`Throw nasty error`}
    </button>
  );
};

export default () => (
  <ErrorMessageWithErrorBoundary>
    <BrokenButton />
  </ErrorMessageWithErrorBoundary>
);

⇧ 返回顶部


- 嵌套 HOC - 封装组件,props 注入,连接到 redux 🌟

用 componentDidCatch 给任意组件加上错误处理功能

import { RootState } from 'MyTypes';
import React from 'react';
import { connect } from 'react-redux';
import { Diff } from 'utility-types';
import { countersActions, countersSelectors } from '../features/counters';
// These props will be injected into the base component
interface InjectedProps {
  count: number;
  onIncrement: () => void;
}
export const withConnectedCount = <BaseProps extends InjectedProps>(
  BaseComponent: React.ComponentType<BaseProps>
) => {
  const mapStateToProps = (state: RootState) => ({
    count: countersSelectors.getReduxCounter(state.counters),
  });
  const dispatchProps = {
    onIncrement: countersActions.increment,
  };
  type HocProps = ReturnType<typeof mapStateToProps> &
    typeof dispatchProps & {
      // here you can extend ConnectedHoc with new props
      overrideCount?: number;
    };
  class Hoc extends React.Component<HocProps> {
    // Enhance component name for debugging and React-Dev-Tools
    static displayName = `withConnectedCount(${BaseComponent.name})`;
    // reference to original wrapped component
    static readonly WrappedComponent = BaseComponent;
    render() {
      const { count, onIncrement, overrideCount, ...restProps } = this.props;
      return (
        <BaseComponent
          count={overrideCount || count} // injected
          onIncrement={onIncrement} // injected
          {...(restProps as BaseProps)}
        />
      );
    }
  }
  const ConnectedHoc = connect<
    ReturnType<typeof mapStateToProps>,
    typeof dispatchProps, // use "undefined" if NOT using dispatchProps
    Diff<BaseProps, InjectedProps>,
    RootState
  >(
    mapStateToProps,
    dispatchProps
  )(Hoc);

  return ConnectedHoc;
};
import * as React from 'react';
import { withConnectedCount } from '../hoc';
import { FCCounter } from '../components';
const FCCounterWithConnectedCount = withConnectedCount(FCCounter);
export default () => (
  <FCCounterWithConnectedCount overrideCount={5} label={'FCCounterWithState'} />
);

⇧ 返回顶部


Redux 连接组件

- Redux 版计数器

import Types from 'MyTypes';
import { connect } from 'react-redux';
import { countersActions, countersSelectors } from '../features/counters';
import { FCCounter } from '../components';
const mapStateToProps = (state: Types.RootState) => ({
  count: countersSelectors.getReduxCounter(state.counters),
});
const dispatchProps = {
  onIncrement: countersActions.increment,
};
export const FCCounterConnected = connect(
  mapStateToProps,
  dispatchProps
)(FCCounter);
import * as React from 'react';
import { FCCounterConnected } from '.';
export default () => <FCCounterConnected label={'FCCounterConnected'} />;

⇧ 返回顶部


- Redux 版计数器,带自定义 props

import Types from 'MyTypes';
import { connect } from 'react-redux';
import { countersActions, countersSelectors } from '../features/counters';
import { FCCounter } from '../components';
type OwnProps = {
  initialCount?: number;
};
const mapStateToProps = (state: Types.RootState, ownProps: OwnProps) => ({
  count:
    countersSelectors.getReduxCounter(state.counters) +
    (ownProps.initialCount || 0),
});
const dispatchProps = {
  onIncrement: countersActions.increment,
};
export const FCCounterConnectedOwnProps = connect(
  mapStateToProps,
  dispatchProps
)(FCCounter);
import * as React from 'react';
import { FCCounterConnectedOwnProps } from '.';
export default () => (
  <FCCounterConnectedOwnProps
    label={'FCCounterConnectedOwnProps'}
    initialCount={10}
  />
);

⇧ 返回顶部


- Redux 版计数器,集成 redux-thunk

import Types from 'MyTypes';
import { bindActionCreators, Dispatch } from 'redux';
import { connect } from 'react-redux';
import * as React from 'react';
import { countersActions } from '../features/counters';
// Thunk Action
const incrementWithDelay = () => async (dispatch: Dispatch): Promise<void> => {
  setTimeout(() => dispatch(countersActions.increment()), 1000);
};
const mapStateToProps = (state: Types.RootState) => ({
  count: state.counters.reduxCounter,
});
const mapDispatchToProps = (dispatch: Dispatch<Types.RootAction>) =>
  bindActionCreators(
    {
      onIncrement: incrementWithDelay,
    },
    dispatch
  );
type Props = ReturnType<typeof mapStateToProps> &
  ReturnType<typeof mapDispatchToProps> & {
    label: string;
  };
export const FCCounter: React.FC<Props> = props => {
  const { label, count, onIncrement } = props;
  const handleIncrement = () => {
    // Thunk action is correctly typed as promise
    onIncrement().then(() => {
      // ...
    });
  };
  return (
    <div>
      <span>
        {label}: {count}
      </span>
      <button type="button" onClick={handleIncrement}>
        {`Increment`}
      </button>
    </div>
  );
};
export const FCCounterConnectedBindActionCreators = connect(
  mapStateToProps,
  mapDispatchToProps
)(FCCounter);
import * as React from 'react';
import { FCCounterConnectedBindActionCreators } from '.';
export default () => (
  <FCCounterConnectedBindActionCreators
    label={'FCCounterConnectedBindActionCreators'}
  />
);

⇧ 返回顶部


Context

https://zh-hans.reactjs.org/docs/context.html

ThemeContext

import * as React from 'react';
export type Theme = React.CSSProperties;
type Themes = {
  dark: Theme;
  light: Theme;
};
export const themes: Themes = {
  dark: {
    color: 'black',
    backgroundColor: 'white',
  },
  light: {
    color: 'white',
    backgroundColor: 'black',
  },
};
export type ThemeContextProps = { theme: Theme; toggleTheme?: () => void };
const ThemeContext = React.createContext<ThemeContextProps>({ theme: themes.light });
export default ThemeContext;

⇧ 返回顶部


ThemeProvider

import React from 'react';
import ThemeContext, { themes, Theme } from './theme-context';
import ToggleThemeButton from './theme-consumer';
interface State {
  theme: Theme;
}
export class ThemeProvider extends React.Component<{}, State> {
  readonly state: State = { theme: themes.light };
  toggleTheme = () => {
    this.setState(state => ({
      theme: state.theme === themes.light ? themes.dark : themes.light,
    }));
  }
  render() {
    const { theme } = this.state;
    const { toggleTheme } = this;
    return (
      <ThemeContext.Provider value={{ theme, toggleTheme }}>
        <ToggleThemeButton />
      </ThemeContext.Provider>
    );
  }
}

⇧ 返回顶部


ThemeConsumer

import * as React from 'react';
import ThemeContext from './theme-context';
type Props = {};
export default function ToggleThemeButton(props: Props) {
  return (
    <ThemeContext.Consumer>
      {({ theme, toggleTheme }) => <button style={theme} onClick={toggleTheme} {...props} />}
    </ThemeContext.Consumer>
  );
}

ThemeConsumer Class 版

import * as React from 'react';
import ThemeContext from './theme-context';
type Props = {};
export class ToggleThemeButtonClass extends React.Component<Props> {
  static contextType = ThemeContext;
  context!: React.ContextType<typeof ThemeContext>;
  render() {
    const { theme, toggleTheme } = this.context;
    return (
      <button style={theme} onClick={toggleTheme}>
        Toggle Theme
      </button>
    );
  }
}

Implementation with Hooks

⇧ 返回顶部


Hooks

https://zh-hans.reactjs.org/docs/hooks-intro.html

- useState

https://zh-hans.reactjs.org/docs/hooks-reference.html#usestate

import * as React from 'react';
type Props = { initialCount: number };
export default function Counter({initialCount}: Props) {
  const [count, setCount] = React.useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
    </>
  );
}

⇧ 返回顶部


- useReducer

用于函数组件的状态管理 Hook (类似 Redux)。

import * as React from 'react';
interface State {
  count: number;
}
type Action = { type: 'reset' } | { type: 'increment' } | { type: 'decrement' };
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      throw new Error();
  }
}
interface CounterProps {
  initialCount: number;
}
function Counter({ initialCount }: CounterProps) {
  const [state, dispatch] = React.useReducer(reducer, {
    count: initialCount,
  });
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </>
  );
}
export default Counter;

⇧ 返回顶部


- useContext

https://zh-hans.reactjs.org/docs/hooks-reference.html#usecontext

import * as React from 'react';
import ThemeContext from '../context/theme-context';
type Props = {};
export default function ThemeToggleButton(props: Props) {
  const { theme, toggleTheme } = React.useContext(ThemeContext);
  return (
    <button onClick={toggleTheme} style={theme} >
      Toggle Theme
    </button>
  );
}

⇧ 返回顶部


Redux - 类型模式

Store 配置

创建全局 Store 类型

RootState - 表示根 state 树的类型

可以作为 import,使用 Redux connect 方法连接组件时,能够确保类型安全性

RootAction - 表示所有 action 对象集合的类型

可以作为 import,用于不同层次中(reducers, sagas 或 redux-observables epics)接收和发送 redux actions

import { StateType, ActionType } from 'typesafe-actions';
declare module 'MyTypes' {
  export type Store = StateType<typeof import('./index').default>;
  export type RootAction = ActionType<typeof import('./root-action').default>;
  export type RootState = StateType<ReturnType<typeof import('./root-reducer').default>>;
}
declare module 'typesafe-actions' {
  interface Types {
    RootAction: ActionType<typeof import('./root-action').default>;
  }
}

⇧ 返回顶部


创建 Store

当创建 store 实例时,我们不需要编写任何额外的类型,它会通过类型推断自动建立一个类型安全的 Store 实例

生成的 store 实例中的方法(像 getStatedispatch)将支持类型检查,并能够暴露所有的类型错误。

import { RootAction, RootState, Services } from 'MyTypes';
import { createStore, applyMiddleware } from 'redux';
import { createEpicMiddleware } from 'redux-observable';
import { createBrowserHistory } from 'history';
import { routerMiddleware as createRouterMiddleware } from 'connected-react-router';
import { composeEnhancers } from './utils';
import rootReducer from './root-reducer';
import rootEpic from './root-epic';
import services from '../services';
// browser history
export const history = createBrowserHistory();
export const epicMiddleware = createEpicMiddleware<
  RootAction,
  RootAction,
  RootState,
  Services
>({
  dependencies: services,
});
const routerMiddleware = createRouterMiddleware(history);
// configure middlewares
const middlewares = [epicMiddleware, routerMiddleware];
// compose enhancers
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
// rehydrate state on app start
const initialState = {};
// create store
const store = createStore(rootReducer(history), initialState, enhancer);
epicMiddleware.run(rootEpic);
// export store singleton instance
export default store;

⇧ 返回顶部


Action Creators 🌟

我们将使用成熟的辅助库 typesafe-actions [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4VlvQfHn-1599371537834)(https://img.shields.io/npm/v/typesafe-actions.svg)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g1WSaoHe-1599371537836)(https://img.shields.io/npm/dt/typesafe-actions.svg)] 它被设计成便于使用 TypeScript 来写 Redux

查看这个进阶教程来学习更多:Typesafe-Actions - Tutorial!

下面的方案用一个简单的工厂函数来自动创建类型安全的 action creators。目的是减少重复的 actions 和 creators 类型声明代码,并减少代码维护工作。生成结果是绝对类型安全的 action-creators 及其 actions。

/* eslint-disable */
import { action } from 'typesafe-actions';
import { ADD, INCREMENT } from './constants';
/* SIMPLE API */
export const increment = () => action(INCREMENT);
export const add = (amount: number) => action(ADD, amount);
/* ADVANCED API */
// More flexible allowing to create complex actions more easily
// use can use "action-creator" instance in place of "type constant"
// e.g. case getType(increment): return action.payload;
// This will allow to completely eliminate need for "constants" in your application, more info here:
// https://github.com/piotrwitek/typesafe-actions#constants
import { createAction } from 'typesafe-actions';
import { Todo } from '../todos/models';
export const emptyAction = createAction(INCREMENT)<void>();
export const payloadAction = createAction(ADD)<number>();
export const payloadMetaAction = createAction(ADD)<number, string>();
export const payloadCreatorAction = createAction(
  'TOGGLE_TODO',
  (todo: Todo) => todo.id
)<string>();
import store from '../../store';
import { countersActions as counter } from '../counters';
// store.dispatch(counter.increment(1)); // Error: Expected 0 arguments, but got 1.
store.dispatch(counter.increment()); // OK
// store.dispatch(counter.add()); // Error: Expected 1 arguments, but got 0.
store.dispatch(counter.add(1)); // OK

⇧ 返回顶部


Reducers

拥有 Type 层面不可变性的 State

readonly 修饰符声明 reducer 中 State 的类型,可以获得编译时的不可变性

export type State = {
  readonly counter: number;
  readonly todos: ReadonlyArray<string>;
};

Readonly 修饰符允许初始化,但不允许重新赋值(编译器会提示错误)

export const initialState: State = {
  counter: 0,
}; // OK
initialState.counter = 3; // TS Error: cannot be mutated

这对 JS 中的 数组 很起效,因为用 (push, pop, splice, …) 这样的赋值方法将会报错,但是 (concat, map, slice,…) 这样的不可变方法依然是允许的。

state.todos.push('Learn about tagged union types') // TS Error: Property 'push' does not exist on type 'ReadonlyArray<string>'
const newTodos = state.todos.concat('Learn about tagged union types') // OK
警告 - Readonly 不是递归的

这意味着 readonly 修饰符在对象的嵌套结构中不会向下传递不变性。你需要标记每个层级的每个属性。(译注:Readonly 是浅比较的)

小贴士: 使用 ReadonlyReadonlyArray 映射类型

export type State = Readonly<{
  counterPairs: ReadonlyArray<Readonly<{
    immutableCounter1: number,
    immutableCounter2: number,
  }>>,
}>;
state.counterPairs[0] = { immutableCounter1: 1, immutableCounter2: 1 }; // TS Error: cannot be mutated
state.counterPairs[0].immutableCounter1 = 1; // TS Error: cannot be mutated
state.counterPairs[0].immutableCounter2 = 1; // TS Error: cannot be mutated
解决方案 - Readonly 的递归版本是 DeepReadonly

为了解决上述问题,我们可以使用 DeepReadonly 类型(来自 utility-types)。

import { DeepReadonly } from 'utility-types';
export type State = DeepReadonly<{
  containerObject: {
    innerValue: number,
    numbers: number[],
  }
}>;
state.containerObject = { innerValue: 1 }; // TS Error: cannot be mutated
state.containerObject.innerValue = 1; // TS Error: cannot be mutated
state.containerObject.numbers.push(1); // TS Error: cannot use mutator methods

⇧ 返回顶部


reducer 类型声明

为了理解下一小节,请确保了解 类型推论基于控制流的类型分析 以及 标记联合类型

import { combineReducers } from 'redux';
import { ActionType } from 'typesafe-actions';
import { Todo, TodosFilter } from './models';
import * as actions from './actions';
import { ADD, CHANGE_FILTER, TOGGLE } from './constants';
export type TodosAction = ActionType<typeof actions>;
export type TodosState = Readonly<{
  todos: Todo[];
  todosFilter: TodosFilter;
}>;
const initialState: TodosState = {
  todos: [],
  todosFilter: TodosFilter.All,
};
export default combineReducers<TodosState, TodosAction>({
  todos: (state = initialState.todos, action) => {
    switch (action.type) {
      case ADD:
        return [...state, action.payload];
      case TOGGLE:
        return state.map(item =>
          item.id === action.payload
            ? { ...item, completed: !item.completed }
            : item
        );
      default:
        return state;
    }
  },
  todosFilter: (state = initialState.todosFilter, action) => {
    switch (action.type) {
      case CHANGE_FILTER:
        return action.payload;
      default:
        return state;
    }
  },
});

⇧ 返回顶部


使用 typesafe-actions 进行 reducer 类型声明

请注意,我们不需要在 API 上使用任何泛型类型参数。可以和传统的 reducer 写法进行比较,它们是等价的。

import { combineReducers } from 'redux';
import { createReducer } from 'typesafe-actions';
import { Todo, TodosFilter } from './models';
import { ADD, CHANGE_FILTER, TOGGLE } from './constants';
export type TodosState = Readonly<{
  todos: Todo[];
  todosFilter: TodosFilter;
}>;
const initialState: TodosState = {
  todos: [],
  todosFilter: TodosFilter.All,
};
const todos = createReducer(initialState.todos)
  .handleType(ADD, (state, action) => [...state, action.payload])
  .handleType(TOGGLE, (state, action) =>
    state.map(item =>
      item.id === action.payload
        ? { ...item, completed: !item.completed }
        : item
    )
  );
const todosFilter = createReducer(initialState.todosFilter).handleType(
  CHANGE_FILTER,
  (state, action) => action.payload
);
export default combineReducers({
  todos,
  todosFilter,
});

⇧ 返回顶部


测试 reducer

import {
  todosReducer as reducer,
  todosActions as actions,
  TodosState,
} from './';
/**
 * FIXTURES
 */
const getInitialState = (initial?: Partial<TodosState>) =>
  reducer(initial as TodosState, {} as any);
/**
 * STORIES
 */
describe('Todos Stories', () => {
  describe('initial state', () => {
    it('should match a snapshot', () => {
      const initialState = getInitialState();
      expect(initialState).toMatchSnapshot();
    });
  });
  describe('adding todos', () => {
    it('should add a new todo as the first element', () => {
      const initialState = getInitialState();
      expect(initialState.todos).toHaveLength(0);
      const state = reducer(initialState, actions.add('new todo'));
      expect(state.todos).toHaveLength(1);
      expect(state.todos[0].title).toEqual('new todo');
    });
  });
  describe('toggling completion state', () => {
    it('should mark active todo as complete', () => {
      const activeTodo = { id: '1', completed: false, title: 'active todo' };
      const initialState = getInitialState({ todos: [activeTodo] });
      expect(initialState.todos[0].completed).toBeFalsy();
      const state1 = reducer(initialState, actions.toggle(activeTodo.id));
      expect(state1.todos[0].completed).toBeTruthy();
    });
  });
});

⇧ 返回顶部


使用 redux-observable 编写异步流

epics 类型声明

import { RootAction, RootState, Services } from 'MyTypes';
import { Epic } from 'redux-observable';
import { tap, ignoreElements, filter } from 'rxjs/operators';
import { isOfType } from 'typesafe-actions';
import { todosConstants } from '../todos';
// contrived example!!!
export const logAddAction: Epic<RootAction, RootAction, RootState, Services> = (
  action$,
  state$,
  { logger }
) =>
  action$.pipe(
    filter(isOfType(todosConstants.ADD)), // action is narrowed to: { type: "ADD_TODO"; payload: string; }
    tap(action => {
      logger.log(
        `action type must be equal: ${todosConstants.ADD} === ${action.type}`
      );
    }),
    ignoreElements()
  );

⇧ 返回顶部


测试 epics

import { StateObservable, ActionsObservable } from 'redux-observable';
import { RootState, Services, RootAction } from 'MyTypes';
import { Subject } from 'rxjs';
import { add } from './actions';
import { logAddAction } from './epics';
// Simple typesafe mock of all the services, you dont't need to mock anything else
// It is decoupled and reusable for all your tests, just put it in a separate file
const services = {
  logger: {
    log: jest.fn<Services['logger']['log']>(),
  },
  localStorage: {
    loadState: jest.fn<Services['localStorage']['loadState']>(),
    saveState: jest.fn<Services['localStorage']['saveState']>(),
  },
};
describe('Todos Epics', () => {
  let state$: StateObservable<RootState>;
  beforeEach(() => {
    state$ = new StateObservable<RootState>(
      new Subject<RootState>(),
      undefined as any
    );
  });
  describe('logging todos actions', () => {
    beforeEach(() => {
      services.logger.log.mockClear();
    });
    it('should call the logger service when adding a new todo', done => {
      const addTodoAction = add('new todo');
      const action$ = ActionsObservable.of(addTodoAction);
      logAddAction(action$, state$, services)
        .toPromise()
        .then((outputAction: RootAction) => {
          expect(services.logger.log).toHaveBeenCalledTimes(1);
          expect(services.logger.log).toHaveBeenCalledWith(
            'action type must be equal: todos/ADD === todos/ADD'
          );
          // expect output undefined because we're using "ignoreElements" in epic
          expect(outputAction).toEqual(undefined);
          done();
        });
    });
  });
});

⇧ 返回顶部


使用 reselect 生成 Selectors

import { createSelector } from 'reselect';
import { TodosState } from './reducer';
export const getTodos = (state: TodosState) => state.todos;
export const getTodosFilter = (state: TodosState) => state.todosFilter;
export const getFilteredTodos = createSelector(getTodos, getTodosFilter, (todos, todosFilter) => {
  switch (todosFilter) {
    case 'completed':
      return todos.filter(t => t.completed);
    case 'active':
      return todos.filter(t => !t.completed);
    default:
      return todos;
  }
});

⇧ 返回顶部


使用 react-redux 的 connect 方法

连接组件类型声明

注意:在下面一段代码中,只有关于 connect 类型声明背后概念的简短说明。请查看 Redux 连接组件 章节了解更多更具体的例子

import MyTypes from 'MyTypes';
import { bindActionCreators, Dispatch, ActionCreatorsMapObject } from 'redux';
import { connect } from 'react-redux';
import { countersActions } from '../features/counters';
import { FCCounter } from '../components';
// Type annotation for "state" argument is mandatory to check 
// the correct shape of state object and injected props you can also
// extend connected component Props interface by annotating `ownProps` argument
const mapStateToProps = (state: MyTypes.RootState, ownProps: FCCounterProps) => ({
  count: state.counters.reduxCounter,
});
// "dispatch" argument needs an annotation to check the correct shape
//  of an action object when using dispatch function
const mapDispatchToProps = (dispatch: Dispatch<MyTypes.RootAction>) =>
  bindActionCreators({
    onIncrement: countersActions.increment,
  }, dispatch);
// shorter alternative is to use an object instead of mapDispatchToProps function
const dispatchToProps = {
    onIncrement: countersActions.increment,
};
// Notice we don't need to pass any generic type parameters to neither
// the connect function below nor map functions declared above
// because type inference will infer types from arguments annotations automatically
// This is much cleaner and idiomatic approach
export const FCCounterConnected =
  connect(mapStateToProps, mapDispatchToProps)(FCCounter);
// You can add extra layer of validation of your action creators
// by using bindActionCreators generic type parameter and RootAction type
const mapDispatchToProps = (dispatch: Dispatch<MyTypes.RootAction>) =>
  bindActionCreators<ActionCreatorsMapObject<Types.RootAction>>({
    invalidActionCreator: () => 1, // Error: Type 'number' is not assignable to type '{ type: "todos/ADD"; payload: Todo; } | { ... }
  }, dispatch);

⇧ 返回顶部


连接组件类型声明,并集成 redux-thunk

注意:使用 thunk action creators 时你需要使用 bindActionCreators。只有这样,你才能获得正确的 dispatch props 类型签名,如下所示。

const thunkAsyncAction = () => async (dispatch: Dispatch): Promise<void> => {
  // dispatch actions, return Promise, etc.
}
const mapDispatchToProps = (dispatch: Dispatch<Types.RootAction>) =>
  bindActionCreators(
    {
      thunkAsyncAction,
    },
    dispatch
  );
type DispatchProps = ReturnType<typeof mapDispatchToProps>;
// { thunkAsyncAction: () => Promise<void>; }

/* Without "bindActionCreators" fix signature will be the same as the original "unbound" thunk function: */
// { thunkAsyncAction: () => (dispatch: Dispatch<AnyAction>) => Promise<void>; }

⇧ 返回顶部


配置和开发者工具

通用 Npm Scripts

通用的、跨项目的、 TS 相关的 npm scripts

"prettier": "prettier --list-different 'src/**/*.ts' || (echo '\nPlease fix code formatting by running:\nnpm run prettier:fix\n'; exit 1)",
"prettier:fix": "prettier --write 'src/**/*.ts'",
"lint": "tslint -p ./",
"tsc": "tsc -p ./ --noEmit",
"tsc:watch": "tsc -p ./ --noEmit -w",
"test": "jest --config jest.config.json",
"test:watch": "jest --config jest.config.json --watch",
"test:update": "jest --config jest.config.json -u"
"ci-check": "npm run prettier && npm run lint && npm run tsc && npm run test",

⇧ 返回顶部


tsconfig.json

我们有推荐的 tsconfig.json 配置文件,你可以借助 react-redux-typescript-scripts 方便地把它添加到你的项目里。

{
  "include": [
    "src",
    "typings"
  ],
  "exclude": [
    "src/**/*.spec.*"
  ],
  "extends": "./node_modules/react-redux-typescript-scripts/tsconfig.json",
  "compilerOptions": {}
}

⇧ 返回顶部


TSLib

https://www.npmjs.com/package/tslib

这个库通过把运行时辅助函数外置化,而不是内嵌到每个文件中,来减少你的打包文件大小。

安装

npm i tslib

把这行加到你的 tsconfig.json 中:

"compilerOptions": {
  "importHelpers": true
}

⇧ 返回顶部


TSLint

https://palantir.github.io/tslint/

安装

npm i -D tslint

如果用于 React 项目,你应该加上额外的 react 规则集:npm i -D tslint-react https://github.com/palantir/tslint-react

我们有推荐配置文件,你可以借助 react-redux-typescript-scripts 方便地把它添加到你的项目里。

tslint.json
{
  "extends": [
    "./node_modules/react-redux-typescript-scripts/tslint.json",
    "./node_modules/react-redux-typescript-scripts/tslint-react.json"
  ],
  "rules": {
    // you can further customize options here
  }
}

⇧ 返回顶部


ESLint

https://eslint.org/
https://typescript-eslint.io

安装

npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

我们有推荐配置文件,他会自动添加 TypeScript 的解析器和插件,你可以借助 react-redux-typescript-scripts 方便地把它添加到你的项目里。

.eslintrc
{
  "extends": [
    "react-app",
    "./node_modules/react-redux-typescript-scripts/eslint.js"
  ],
  "rules": {
    // you can further customize options here
  }
}

⇧ 返回顶部


Jest

https://jestjs.io/

安装

npm i -D jest ts-jest @types/jest
jest.config.json
{
  "verbose": true,
  "transform": {
    ".(ts|tsx)": "ts-jest"
  },
  "testRegex": "(/spec/.*|\\.(test|spec))\\.(ts|tsx|js)$",
  "moduleFileExtensions": ["ts", "tsx", "js"],
  "moduleNameMapper": {
    "^Components/(.*)": "./src/components/$1"
  },
  "globals": {
    "window": {},
    "ts-jest": {
      "tsConfig": "./tsconfig.json"
    }
  },
  "setupFiles": ["./jest.stubs.js"],
  "testURL": "http://localhost/"
}
jest.stubs.js
// Global/Window object Stubs for Jest
window.matchMedia = window.matchMedia || function () {
  return {
    matches: false,
    addListener: function () { },
    removeListener: function () { },
  };
};
window.requestAnimationFrame = function (callback) {
  setTimeout(callback);
};
window.localStorage = {
  getItem: function () { },
  setItem: function () { },
};
Object.values = () => [];

⇧ 返回顶部


风格指南

“react-styleguidist”

⟩⟩⟩ styleguide.config.js

⟩⟩⟩ demo

⇧ 返回顶部


贴士

通用小贴士

- 使用 TS 时我还需要使用 React.PropTypes 吗?

不。用了 TypeScript 之后,没有必要再使用 PropTypes。当声明 Props 和 State 接口后,你将通过静态类型检查获得完全的自动补全和编码时的安全性。这样,你就能直接避免运行时错误,并减少大量调试时间。额外的好处是,这也是一种用于在源码中解释组件公共 API 的优雅而标准化的方法。

⇧ 返回顶部


- 什么时候使用 interface 声明,什么时候使用 type 别名?

从实际来看,使用 interface 声明在编译错误时会生成一个 interface 同名标识,相反 type 别名不会生成标识名,并且会展开显示所有属性和嵌套的类型。
尽管我大部分时候更喜欢用 type ,但是有时候编译错误过于冗长影响排查,我会根据两者的差别,改用 interface 来隐藏报错中没那么重要的类型细节。
相关的 ts-lint 规则:https://palantir.github.io/tslint/rules/interface-over-type-literal/

⇧ 返回顶部

- 具名 exports 和 default export 那个比较好?

一个常见的适应性方案是使用文件夹模块模式,这样你可以根据情况同时使用具名和默认 import。
这个方案的好处是你能实现更好的封装,以及能够安全地重构内部命名和文件夹结构,而不影响你的业务代码:

// 1. create your component files (`select.tsx`) using default export in some folder:
// components/select.tsx
const Select: React.FC<Props> = (props) => {
...
export default Select;
// 2. in this folder create an `index.ts` file that will re-export components with named exports:
// components/index.ts
export { default as Select } from './select';
...
// 3. now you can import your components in both ways, with named export (better encapsulation) or using default export (internal access):
// containers/container.tsx
import { Select } from '@src/components';
or
import Select from '@src/components/select';
...

⇧ 返回顶部


- 什么是初始化 class 实例或静态属性的最佳实践?

首选新语法来进行 class 属性初始化

class ClassCounterWithInitialCount extends React.Component<Props, State> {
  // default props using Property Initializers
  static defaultProps: DefaultProps = {
    className: 'default-class',
    initialCount: 0,
  };
  // initial state using Property Initializers
  state: State = {
    count: this.props.initialCount,
  };
  ...
}

⇧ 返回顶部


- 什么是声明组件 handler 方法的最佳实践?

首选新语法,用箭头函数声明 class 方法字段

class ClassCounter extends React.Component<Props, State> {
// handlers using Class Fields with arrow functions
  handleIncrement = () => {
    this.setState({ count: this.state.count + 1 });
  };
  ...
}

⇧ 返回顶部


module 环境声明小贴士

(译注:环境声明(ambient)模块扩展(augmentation)

环境声明中的 imports

若要进行 module 扩展,import 应该位于 module 声明外部。

import { Operator } from 'rxjs/Operator';
import { Observable } from 'rxjs/Observable';
declare module 'rxjs/Subject' {
  interface Subject<T> {
    lift<R>(operator: Operator<T, R>): Observable<R>;
  }
}

创建第三方类型定义时,所有 imports 应该位于 module 声明内部,否则 imports 将被视为扩展并报错。

declare module "react-custom-scrollbars" {
    import * as React from "react";
    export interface positionValues {
    ...

⇧ 返回顶部


类型定义小贴士

缺少类型定义的错误

如果你找不到第三方模块的类型声明,你可以自己写一个,或借助 Shorthand Ambient Modules 禁用该模块的类型检查。

// typings/modules.d.ts
declare module 'MyTypes';
declare module 'react-test-renderer';
为 npm 模块使用自定义 d.ts 文件

如果你想为(自带类型定义的)某些 npm 模块使用替代的(自定义的)类型定义,你可以通过覆写编译选项中 paths 字段来实现。

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "redux": ["typings/redux"], // use an alternative type-definitions instead of the included one
      ...
    },
    ...,
  }
}

⇧ 返回顶部

类型扩展小贴士

外部类型定义文件(*.d.ts)相关问题的处理策略

对库的内部声明进行扩展 - 使用相对路径 import
// added missing autoFocus Prop on Input component in "antd@2.10.0" npm package
declare module '../node_modules/antd/lib/input/Input' {
  export interface InputProps {
    autoFocus?: boolean;
  }
}
对库的公开声明进行扩展 - 使用 node_modules import
// fixed broken public type-definitions in "rxjs@5.4.1" npm package
import { Operator } from 'rxjs/Operator';
import { Observable } from 'rxjs/Observable';

declare module 'rxjs/Subject' {
  interface Subject<T> {
    lift<R>(operator: Operator<T, R>): Observable<R>;
  }
}

更多搭配第三方类型定义的进阶场景可以在 TypeScript 官方文档 找到

⇧ 返回顶部


教程和文章

相关进阶教程精选清单

高阶组件:

  • https://medium.com/@jrwebdev/react-higher-order-component-patterns-in-typescript-42278f7590fb

ubject {
lift(operator: Operator<T, R>): Observable;
}
}


创建第三方类型定义时,所有 imports 应该位于 module 声明内部,否则 imports 将被视为扩展并报错。

```tsx
declare module "react-custom-scrollbars" {
    import * as React from "react";
    export interface positionValues {
    ...

⇧ 返回顶部


类型定义小贴士

缺少类型定义的错误

如果你找不到第三方模块的类型声明,你可以自己写一个,或借助 Shorthand Ambient Modules 禁用该模块的类型检查。

// typings/modules.d.ts
declare module 'MyTypes';
declare module 'react-test-renderer';
为 npm 模块使用自定义 d.ts 文件

如果你想为(自带类型定义的)某些 npm 模块使用替代的(自定义的)类型定义,你可以通过覆写编译选项中 paths 字段来实现。

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "redux": ["typings/redux"], // use an alternative type-definitions instead of the included one
      ...
    },
    ...,
  }
}

⇧ 返回顶部

类型扩展小贴士

外部类型定义文件(*.d.ts)相关问题的处理策略

对库的内部声明进行扩展 - 使用相对路径 import
// added missing autoFocus Prop on Input component in "antd@2.10.0" npm package
declare module '../node_modules/antd/lib/input/Input' {
  export interface InputProps {
    autoFocus?: boolean;
  }
}
对库的公开声明进行扩展 - 使用 node_modules import
// fixed broken public type-definitions in "rxjs@5.4.1" npm package
import { Operator } from 'rxjs/Operator';
import { Observable } from 'rxjs/Observable';

declare module 'rxjs/Subject' {
  interface Subject<T> {
    lift<R>(operator: Operator<T, R>): Observable<R>;
  }
}

更多搭配第三方类型定义的进阶场景可以在 TypeScript 官方文档 找到

⇧ 返回顶部


教程和文章

相关进阶教程精选清单

高阶组件:

  • https://medium.com/@jrwebdev/react-higher-order-component-patterns-in-typescript-42278f7590fb

⇧ 返回顶部

React 系列 : Redux + TypeScript React 系列 : Redux + TypeScript前言正文StoreActions基本同步 actions異步 actions + TSThunkAction 類型TReturnTypeTStateTExtraThunkArgTBasicActioninner functionReducerReact + ReduxTodoApp 效果展示結語參考 前言 前幾天寫過一篇關於 react + TS 的簡單入門,大致上就是說說 TS 的強大以及基本怎麼跟 react 配合起來,若不清楚歡迎先去看看 Re 阅读详情

相关推荐

网站多语言切换实战:从原生JS到Vue/React的完整方案

国际化(i18n)是构建面向全球用户网站的核心技术,它通过语言包映射机制实现内容的多语言动态替换。其原理在于将界面文本抽象为键值对,根据用户偏好实时切换渲染。这项技术的价值在于提升用户体验、扩大市场覆盖并增强品牌专业形象,广泛应用于电商、官网、后台系统等场景。本文聚焦**中英文切换**这一常见需求,深入剖析**纯前端方案**与前后端结合方案的选型,并详解从原生JavaScript到集成Vue I18n、react-i18next等主流框架的完整实现路径,涵盖语言包管理、状态持久化、SEO优化及常见问题排查等

weixin_34005042的博客 322

Nuxt3+TypeScript+Vite项目模板搭建(TypeScript、ESLint、Prettier、Sass、Stylelint、husky、Vite、pnpm 配置)【图文详细教程】

图文详细教程 Nuxt 3 + TypeScript + Vite 初始项目模板,目前已完成配置的内容:TypeScript、ESLint、vite-plugin-eslint、Prettier、Sass、Stylelint、husky、src 目录别名、pnpm、项目全局环境变量

emmmm..... 4867

前端测试框架选型指南:Jest、Vitest与Cypress深度对比与实践

在前端开发中,测试是保障代码质量和应用稳定性的关键环节。单元测试和端到端测试构成了现代测试体系的核心,它们分别验证代码逻辑的正确性和用户流程的完整性。理解测试框架的设计哲学与适用场景,能显著提升开发效率和项目可维护性。Jest作为成熟的单元测试框架,以其强大的Mock能力和开箱即用的特性,成为React等大型项目的可靠选择;而Vitest凭借与Vite的深度集成,提供了极速的热更新和测试运行体验,尤其适合现代前端项目。对于需要模拟真实用户操作的场景,Cypress提供了真实的浏览器环境和出色的调试能力,是端

weixin_30634661的博客 309

React+TypeScript如何使用Redux

记录一下React+TypeScript如何使用redux 一、截至本篇文章发布依赖均为最新版本,版本号如下 "@types/react": "^17.0.37", "@types/react-dom": "^17.0.11", "@types/react-redux": "^7.1.20", "react": "^17.0.2", "react-dom": "^17.0.2", "react-redux": "^7.2.6", "react-router-dom": "^6.0.2", "react-sc

旭哥的博客 1177

最详细教程【React+TS】从零开始搭建React+TypeScript+Router+Redux+Less+px2rem自适应+sass+axios反向代理+别名@+Antd-mobile

你需要准备好node.js版本不低于6.14.8 和 git文章内容比较长(保姆级别教程),全是干货,请耐心看完 注: 项目名称不可以出现大字母出现Happy hacking! 就说明项目搭建成功 打开vscode查看项目当项目中出现tsx 就说明项目搭建成功然后启动项目 npm start / npm run start下载路由 src目录下创建views文件夹,views内创建Home,Contact,About,Navbar四个tsx文件,其中Navbar用来控制路由,其他三个

weixin_56529964的博客 2万+

基于TypeScriptReduxReact-Redux使用说明

首先创建reducer 在文件中创建store,将reducer传入createStore中: 类组件中通过getState()读取数据: 组件中通过定义action,派发action改变state中的数据: 接着回到reducer中,处理组件派发过来的action,并更新改变state中的数据: 最后一步,通过store.subscribe“发送订阅信息”,即实时更新组件中的数据: ...

liangjinghao的博客 1858

用Electron, TypeScript, ReactRedux构建RSS聚合器

本文介绍了一个使用Electron, TypeScript, ReactRedux技术栈开发的RSS聚合器项目。通过详细解析项目的构建过程,包括创建Reducer、连接到store、在容器组件中映射状态和动作创建者到组件属性,以及如何从组件中消费store。此外,还探讨了错误处理和数据获取的策略,以及如何利用React MDL库来改善用户界面。

weixin_35756637的博客 195

React面试题笔记(一)

React面试题笔记(一)

qq_42750608的博客 1233

JSONEditor-React:为现代React应用设计的JSON编辑架构解决方案

在数据驱动的现代Web应用中,JSON作为数据交换和配置的标准格式,其编辑体验直接影响开发效率。传统的JSON编辑器往往面临与React生态整合困难、性能瓶颈和用户体验不一致的挑战。JSONEditor-React通过React原生组件化设计,将josdejong/jsoneditor的强大功能无缝集成到React应用中,为开发者提供了一套完整的企业级JSON编辑解决方案。 ## React生态

gitblog_00825的博客 90

Slack桌面3.0迁移到BrowserView

Slack最近在beta通道中提供了3.0版本,该版本包含大量的性能提升和漏洞修复。大多数主要变更都是围绕从Electron组件WebView到BrowserView的迁移,BrowserView是一个更新且更稳定的替代选择。Slack工程师Charlie Hess发表了一篇博客,概述了这次迁移过程。\\Slack使用Electron开发,Electron是一种框架,它使用像NodeJS和Chro...

weixin_33796205的博客 104

前端AI编程工具实战对比:TypeScriptReact场景下的选型指南

AI编程工具已成为现代前端开发的基础能力,其核心价值在于提升TypeScript类型安全编码效率、增强React等框架语义理解,并深度融入ESLint/Jest/Vite等工程化链路。原理上,真正可用的工具需通过类型系统穿透力、框架语义理解深度和工程化链路兼容性三重验证;技术价值体现在降低CRUD重复劳动、辅助复杂异步逻辑建模、推动团队规范自动化落地;典型应用场景包括日常组件开发、多层状态管理攻坚、新人知识快速对齐。本文基于2025–2026年真实业务代码库实测,聚焦前端开发、AI编程工具、对比测评三大热词

weixin_29011239的博客 90

node.js npm javascript typescript ts pm2 yarn

public # 存放图片文件。/app/ui # 存放ui文件。/app/ui/fonts.ts # 存放字体文件。

lalading6543的博客 442

Visual Studio Code 常用配置

Visual Studio Code 常用配置Visual Studio Codesettings.jsonExtentionsCommonJavascriptPythonMarkdownFlutterAngular 2 Visual Studio Code settings.json 语义高亮 js 文件 和 jsx 文件中启用 emmet { "editor.tokenColorCust...

levin blog 884

Webpack Bundle Size Analyzer源码解读:从Webpack Stats到依赖树

Webpack Bundle Size Analyzer是一个强大的**Webpack bundle分析工具**,它能帮你快速找出项目中哪些依赖包占用了最多的bundle空间。对于前端开发者来说,优化bundle大小是提升应用性能的关键步骤,而这个工具正是解决这个痛点的利器。 ## 🔍 工具的核心功能 Webpack Bundle Size Analyzer主要提供两大功能: 1. **命

gitblog_01079的博客 524

构建前端复杂组件库:从领域抽象到工程提效的实战指南

在现代前端开发中,组件化是构建复杂应用的核心范式。其原理在于通过封装和复用,将UI界面拆分为独立、可组合的单元,从而提升开发效率和代码可维护性。这一技术的核心价值在于,它允许团队在应对特定业务领域的复杂交互和状态管理时,能够基于一套高内聚、可扩展的基础设施进行高效开发,避免重复造轮子。典型的应用场景包括数据可视化大屏、低代码搭建平台、实时协作编辑器以及复杂表单与流程设计器等。本文聚焦于如何针对这些特定领域,设计并实现一个真正意义上的“复杂组件库”,深入探讨其从领域模型驱动、分层架构到关键技术选型与性能优化的

weixin_34355559的博客 376

Blocks UI测试实战:从单元到集成的完整策略与Jest、Vue/React工具链配置

在现代前端开发中,组件化与模块化是构建复杂用户界面的核心理念。其原理在于将UI拆分为独立、可复用的单元(组件),通过组合与状态管理来构建应用。这种模式的技术价值在于提升代码的可维护性、复用性和开发效率。然而,随着组件数量和交互复杂度的增加,确保UI的稳定性和功能正确性成为关键挑战,尤其是在涉及状态管理、路由和API集成的应用场景中。为此,建立系统化的测试策略至关重要,它能够将质量保障左移,在开发阶段拦截缺陷,并为持续集成/持续部署(CI/CD)奠定基础。本文聚焦于Blocks UI(即由可复用UI块构成的界

weixin_34335458的博客 545

从零构建企业级 AI Agent 平台

本文介绍从零构建企业级AI Agent平台的架构全景与技术选型。因数据主权、治理深度、存量系统绑定及质量运营需求,选择自建而非SaaS。平台采用模块化单体,分渠道、API、服务、数据四层,核心模块包括知识采集、RAG、Agent运行时、Workflow、权限治理等。技术选型以Python+FastAPI为主,数据层复用MySQL、Doris、ES等,强调“模型不能决定授权”的架构纪律。

qq_33055075的博客 666
上一篇: Airbnb JavaScript 风格指南
下一篇: VS Code的使用
Jay·Yuen
博客等级 码龄9年 342粉丝 69原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值