TypeScript

有狀態(tài)組件

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

  1. 為狀態(tài)定義一個類型(如:this.state
  2. 根據我們在構造函數中給出的props來初始化this.state
  3. 為我們的按鈕創(chuàng)建兩個事件處理程序(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文件。當執(zhí)行run的時候,引入的.css文件將被編譯到輸出文件中。因此在src/components/Hello.tsx中加入引入:

import './Hello.css';

用Jest寫測試

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

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

(譯者注:原文中沒有安裝jest-cli,運行測試時會報錯,此處已加上)
在編寫測試之前,我們需要使用React16的適配器對Enzyme進行配置。創(chuàng)建文件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

添加狀態(tài)管理

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

安裝

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

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

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

// src/types/index.tsx

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

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

添加actions

讓我們從創(chuàng)建一組消息類型開始,我們的應用程序可以在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的字符串文字類型。
接下來,我們將創(chuàng)建一組可以在src / actions / index.tsx中創(chuàng)建這些操作的操作和函數。

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

添加一個reducer

reducer是一個通過創(chuàng)建應用的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;
}

創(chuàng)建一個container

使用Redux,我們經常會編寫組件和容器。組件通常與數據無關,并且主要在表示級別工作。容器通常包裝組件并向其提供顯示和修改狀態(tài)所需的任何數據。
首先,更新src / components / Hello.tsx,以便可以修改狀態(tài)。我們將為onIncrementonDecrementProps添加兩個可選的回調屬性:
首先,修改src/components/Hello.tsx,使它可以修改狀態(tài)。為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)了。首先創(chuàng)建文件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()),
  }
}

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

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

創(chuàng)建store

回到src / index.tsx。我們需要創(chuàng)建一個具有初始狀態(tài)的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
);
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容