TypeScript

有狀態(tài)組件

當(dāng)我們的組件需要根據(jù)用戶的輸入更新的時(shí)候,我們需要有狀態(tài)的組件。
深入理解React的有狀態(tài)組件的最佳實(shí)踐超出了本文的討論范圍,但是我們可以快速看一下給我們得到Hello組件加上狀態(tài)之后是什么樣子。我們將渲染兩個(gè)<button>來更新Hello組件顯示的感嘆號(hào)的數(shù)量。
要做到這一點(diǎn),我們需要做:

  1. 為狀態(tài)定義一個(gè)類型(如:this.state
  2. 根據(jù)我們?cè)跇?gòu)造函數(shù)中給出的props來初始化this.state。
  3. 為我們的按鈕創(chuàng)建兩個(gè)事件處理程序(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定義一個(gè)新的類型:State
  2. 使用this.setState更新React中的state
  3. 使用箭頭函數(shù)初始化方法類(如: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文件。當(dāng)執(zhí)行run的時(shí)候,引入的.css文件將被編譯到輸出文件中。因此在src/components/Hello.tsx中加入引入:

import './Hello.css';

用Jest寫測(cè)試

我們可以根據(jù)我們?cè)O(shè)想的組件的功能,為組件編寫測(cè)試。
首先需要安裝Enzyme及其相關(guān)依賴。Enzyme是React生態(tài)系統(tǒng)中的常用工具,可以更輕松地編寫組件行為方式的測(cè)試。

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

(譯者注:原文中沒有安裝jest-cli,運(yùn)行測(cè)試時(shí)會(huì)報(bào)錯(cuò),此處已加上)
在編寫測(cè)試之前,我們需要使用React16的適配器對(duì)Enzyme進(jìn)行配置。創(chuàng)建文件src/setupTests.ts, 該配置文件在運(yùn)行測(cè)試時(shí)自動(dòng)加載:

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

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

現(xiàn)在,可以開始寫測(cè)試文件了。新建文件src/components/Hello.test.tsx,與被測(cè)試的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();
});

運(yùn)行測(cè)試:npm run test

測(cè)試報(bào)錯(cuò).png

添加狀態(tài)管理

通過redux對(duì)組件進(jìn)行狀態(tài)管理。

安裝

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

定義應(yīng)用的狀態(tài)

我們需要定義Redux存儲(chǔ)的狀態(tài)的形式。因此,新建文件src/types/index.tsx,該文件包含可能在整個(gè)程序中用到的類型的定義。

// src/types/index.tsx

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

我們的目的是:languageName將是這個(gè)應(yīng)用程序編寫的編程語言(即TypeScript或JavaScript),而enthusiasmLevel有所不同。當(dāng)我們編寫第一個(gè)container時(shí),就會(huì)理解為什么要故意讓state和props不同。

添加actions

讓我們從創(chuàng)建一組消息類型開始,我們的應(yīng)用程序可以在src / constants / index.tsx中響應(yīng)。

// 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模式允許我們以易于訪問和可重構(gòu)的方式使用TypeScript的字符串文字類型。
接下來,我們將創(chuàng)建一組可以在src / actions / index.tsx中創(chuàng)建這些操作的操作和函數(shù)。

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

我們創(chuàng)建了兩個(gè)類型,用以描述increment actions和decrement actions看起來是什么樣子。我們還創(chuàng)建了一個(gè)類型(EnthusiasmAction)來描述一個(gè)action可以是 increment還是decrement的情況。最后,我們創(chuàng)建了兩個(gè)函數(shù)來實(shí)際執(zhí)行我們可以使用的acions。

添加一個(gè)reducer

reducer是一個(gè)通過創(chuàng)建應(yīng)用的state的副本,來產(chǎn)生變化的函數(shù),并且沒有副作用。
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;
}

創(chuàng)建一個(gè)container

使用Redux,我們經(jīng)常會(huì)編寫組件和容器。組件通常與數(shù)據(jù)無關(guān),并且主要在表示級(jí)別工作。容器通常包裝組件并向其提供顯示和修改狀態(tài)所需的任何數(shù)據(jù)。
首先,更新src / components / Hello.tsx,以便可以修改狀態(tài)。我們將為onIncrementonDecrementProps添加兩個(gè)可選的回調(diào)屬性:
首先,修改src/components/Hello.tsx,使它可以修改狀態(tài)。為onIncrement和onDecrement的Props添加兩個(gè)可選的回調(diào)屬性:

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

然后將這兩個(gè)回調(diào)函數(shù)綁定到在組件中添加的兩個(gè)按鈕上:

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

接下來可以把組件包裝成一個(gè)容器(container)了。首先創(chuàng)建文件src/containers/Hello.tsx,并導(dǎo)入:

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

react-redux的connect函數(shù)將能夠?qū)ello組件轉(zhuǎn)換為容器,通過以下mapStateToPropsmapDispatchToProps這兩個(gè)函數(shù)實(shí)現(xiàn):

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()),
  }
}

最后,調(diào)用connect。connect首先獲取mapStateToPropsmapDispatchToProps,然后返回另一個(gè)用來包裝組件的函數(shù)。生成的容器使用以下代碼行定義:

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

創(chuàng)建store

回到src / index.tsx。我們需要?jiǎng)?chuàng)建一個(gè)具有初始狀態(tài)的store,并使用所有的reducer進(jìn)行設(shè)置。

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連接起來。導(dǎo)入這些并且將store傳遞給Provider的屬性:

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

ReactDOM.render(
  <Provider store={store}>
    <Hello />
  </Provider>,
  document.getElementById('root') as HTMLElement
);
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容