TypeScript

2019-10-14  本文已影响0人  朝曦

有状态组件

当我们的组件需要根据用户的输入更新的时候,我们需要有状态的组件。
深入理解React的有状态组件的最佳实践超出了本文的讨论范围,但是我们可以快速看一下给我们得到Hello组件加上状态之后是什么样子。我们将渲染两个<button>来更新Hello组件显示的感叹号的数量。
要做到这一点,我们需要做:

  1. 为状态定义一个类型(如:this.state
  2. 根据我们在构造函数中给出的props来初始化this.state
  3. 为我们的按钮创建两个事件处理程序(onIncrementonDecrement)。
// src/components/StatefulHello.tsx

import * as React from "react";

export interface Props {
  name: string;
  enthusiasmLevel?: number;
}

interface State {
  currentEnthusiasm: number;
}

class Hello extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { currentEnthusiasm: props.enthusiasmLevel || 1 };
  }

  onIncrement = () => this.updateEnthusiasm(this.state.currentEnthusiasm + 1);
  onDecrement = () => this.updateEnthusiasm(this.state.currentEnthusiasm - 1);

  render() {
    const { name } = this.props;

    if (this.state.currentEnthusiasm <= 0) {
      throw new Error('You could be a little more enthusiastic. :D');
    }

    return (
      <div className="hello">
        <div className="greeting">
          Hello {name + getExclamationMarks(this.state.currentEnthusiasm)}
        </div>
        <button onClick={this.onDecrement}>-</button>
        <button onClick={this.onIncrement}>+</button>
      </div>
    );
  }

  updateEnthusiasm(currentEnthusiasm: number) {
    this.setState({ currentEnthusiasm });
  }
}

export default Hello;

function getExclamationMarks(numChars: number) {
  return Array(numChars + 1).join('!');
}

说明:

  1. 像props一样,我们需要为state定义一个新的类型:State
  2. 使用this.setState更新React中的state
  3. 使用箭头函数初始化方法类(如:onIncrement = () => ...

加入样式

src/components/Hello.css新建css文件:

.hello {
  text-align: center;
  margin: 20px;
  font-size: 48px;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

.hello button {
  margin-left: 25px;
  margin-right: 25px;
  font-size: 40px;
  min-width: 50px;
}

create-react-app使用的Webpack和loaders等工具允许我们引入stylesheets文件。当执行run的时候,引入的.css文件将被编译到输出文件中。因此在src/components/Hello.tsx中加入引入:

import './Hello.css';

用Jest写测试

我们可以根据我们设想的组件的功能,为组件编写测试。
首先需要安装Enzyme及其相关依赖。Enzyme是React生态系统中的常用工具,可以更轻松地编写组件行为方式的测试。

npm install -D enzyme jest-cli @types/enzyme enzyme-adapter-react-16 @types/enzyme-adapter-react-16 react-test-renderer

(译者注:原文中没有安装jest-cli,运行测试时会报错,此处已加上)
在编写测试之前,我们需要使用React16的适配器对Enzyme进行配置。创建文件src/setupTests.ts, 该配置文件在运行测试时自动加载:

import * as enzyme from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-16';

enzyme.configure({ adapter: new Adapter() });

现在,可以开始写测试文件了。新建文件src/components/Hello.test.tsx,与被测试的Hello.tsx在同一目录下。

// src/components/Hello.test.tsx

import * as React from 'react';
import * as enzyme from 'enzyme';
import Hello from './Hello';

it('renders the correct text when no enthusiasm level is given', () => {
  const hello = enzyme.shallow(<Hello name='Daniel' />);
  expect(hello.find(".greeting").text()).toEqual('Hello Daniel!')
});

it('renders the correct text with an explicit enthusiasm of 1', () => {
  const hello = enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={1}/>);
  expect(hello.find(".greeting").text()).toEqual('Hello Daniel!')
});

it('renders the correct text with an explicit enthusiasm level of 5', () => {
  const hello = enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={5} />);
  expect(hello.find(".greeting").text()).toEqual('Hello Daniel!!!!!');
});

it('throws when the enthusiasm level is 0', () => {
  expect(() => {
    enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={0} />);
  }).toThrow();
});

it('throws when the enthusiasm level is negative', () => {
  expect(() => {
    enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={-1} />);
  }).toThrow();
});

运行测试:npm run test

测试报错.png

添加状态管理

通过redux对组件进行状态管理。

安装

npm install -S redux react-redux @types/react-redux

定义应用的状态

我们需要定义Redux存储的状态的形式。因此,新建文件src/types/index.tsx,该文件包含可能在整个程序中用到的类型的定义。

// src/types/index.tsx

export interface StoreState {
    languageName: string;
    enthusiasmLevel: number;
}

我们的目的是:languageName将是这个应用程序编写的编程语言(即TypeScript或JavaScript),而enthusiasmLevel有所不同。当我们编写第一个container时,就会理解为什么要故意让state和props不同。

添加actions

让我们从创建一组消息类型开始,我们的应用程序可以在src / constants / index.tsx中响应。

// src/constants/index.tsx

export const INCREMENT_ENTHUSIASM = 'INCREMENT_ENTHUSIASM';
export type INCREMENT_ENTHUSIASM = typeof INCREMENT_ENTHUSIASM;


export const DECREMENT_ENTHUSIASM = 'DECREMENT_ENTHUSIASM';
export type DECREMENT_ENTHUSIASM = typeof DECREMENT_ENTHUSIASM;

这种const / type模式允许我们以易于访问和可重构的方式使用TypeScript的字符串文字类型。
接下来,我们将创建一组可以在src / actions / index.tsx中创建这些操作的操作和函数。

import * as constants from '../constants';

export interface IncrementEnthusiasm {
    type: constants.INCREMENT_ENTHUSIASM;
}

export interface DecrementEnthusiasm {
    type: constants.DECREMENT_ENTHUSIASM;
}

export type EnthusiasmAction = IncrementEnthusiasm | DecrementEnthusiasm;

export function incrementEnthusiasm(): IncrementEnthusiasm {
    return {
        type: constants.INCREMENT_ENTHUSIASM
    }
}

export function decrementEnthusiasm(): DecrementEnthusiasm {
    return {
        type: constants.DECREMENT_ENTHUSIASM
    }
}

我们创建了两个类型,用以描述increment actions和decrement actions看起来是什么样子。我们还创建了一个类型(EnthusiasmAction)来描述一个action可以是 increment还是decrement的情况。最后,我们创建了两个函数来实际执行我们可以使用的acions。

添加一个reducer

reducer是一个通过创建应用的state的副本,来产生变化的函数,并且没有副作用。
reducer文件为src/reducers/index.tsx。它的功能是确保increments 将enthusiasm level提高1,而decrements 将enthusiasm level降低1,但enthusiasm level不低于1。

// src/reducers/index.tsx

import { EnthusiasmAction } from '../actions';
import { StoreState } from '../types/index';
import { INCREMENT_ENTHUSIASM, DECREMENT_ENTHUSIASM } from '../constants/index';

export function enthusiasm(state: StoreState, action: EnthusiasmAction): StoreState {
  switch (action.type) {
    case INCREMENT_ENTHUSIASM:
      return { ...state, enthusiasmLevel: state.enthusiasmLevel + 1 };
    case DECREMENT_ENTHUSIASM:
      return { ...state, enthusiasmLevel: Math.max(1, state.enthusiasmLevel - 1) };
  }
  return state;
}

创建一个container

使用Redux,我们经常会编写组件和容器。组件通常与数据无关,并且主要在表示级别工作。容器通常包装组件并向其提供显示和修改状态所需的任何数据。
首先,更新src / components / Hello.tsx,以便可以修改状态。我们将为onIncrementonDecrementProps添加两个可选的回调属性:
首先,修改src/components/Hello.tsx,使它可以修改状态。为onIncrement和onDecrement的Props添加两个可选的回调属性:

export interface Props {
  name: string;
  enthusiasmLevel?: number;
  onIncrement?: () => void;
  onDecrement?: () => void;
}

然后将这两个回调函数绑定到在组件中添加的两个按钮上:

function Hello({ name, enthusiasmLevel = 1, onIncrement, onDecrement }: Props) {
  if (enthusiasmLevel <= 0) {
    throw new Error('You could be a little more enthusiastic. :D');
  }

  return (
    <div className="hello">
      <div className="greeting">
        Hello {name + getExclamationMarks(enthusiasmLevel)}
      </div>
      <div>
        <button onClick={onDecrement}>-</button>
        <button onClick={onIncrement}>+</button>
      </div>
    </div>
  );
}

接下来可以把组件包装成一个容器(container)了。首先创建文件src/containers/Hello.tsx,并导入:

import Hello from '../components/Hello';
import * as actions from '../actions/';
import { StoreState } from '../types/index';
import { connect, Dispatch } from 'react-redux';

react-redux的connect函数将能够将Hello组件转换为容器,通过以下mapStateToPropsmapDispatchToProps这两个函数实现:

export function mapStateToProps({ enthusiasmLevel, languageName }: StoreState) {
  return {
    enthusiasmLevel,
    name: languageName,
  }
}

export function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) {
  return {
    onIncrement: () => dispatch(actions.incrementEnthusiasm()),
    onDecrement: () => dispatch(actions.decrementEnthusiasm()),
  }
}

最后,调用connectconnect首先获取mapStateToPropsmapDispatchToProps,然后返回另一个用来包装组件的函数。生成的容器使用以下代码行定义:

export default connect(mapStateToProps, mapDispatchToProps)(Hello);

创建store

回到src / index.tsx。我们需要创建一个具有初始状态的store,并使用所有的reducer进行设置。

import { createStore } from 'redux';
import { enthusiasm } from './reducers/index';
import { StoreState } from './types/index';

const store = createStore<StoreState>(enthusiasm, {
  enthusiasmLevel: 1,
  languageName: 'TypeScript',
});

接下来,把./src/components/Hello与./src/containers/Hello交换使用,并使用react-reduxProviderpropscontainer连接起来。导入这些并且将store传递给Provider的属性:

import Hello from './containers/Hello';
import { Provider } from 'react-redux';

ReactDOM.render(
  <Provider store={store}>
    <Hello />
  </Provider>,
  document.getElementById('root') as HTMLElement
);
上一篇下一篇

猜你喜欢

热点阅读