有狀態(tài)組件
當我們的組件需要根據用戶的輸入更新的時候,我們需要有狀態(tài)的組件。
深入理解React的有狀態(tài)組件的最佳實踐超出了本文的討論范圍,但是我們可以快速看一下給我們得到Hello組件加上狀態(tài)之后是什么樣子。我們將渲染兩個<button>來更新Hello組件顯示的感嘆號的數量。
要做到這一點,我們需要做:
- 為狀態(tài)定義一個類型(如:
this.state) - 根據我們在構造函數中給出的props來初始化
this.state。 - 為我們的按鈕創(chuàng)建兩個事件處理程序(
onIncrement和onDecrement)。
// 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('!');
}
說明:
- 像props一樣,我們需要為state定義一個新的類型:
State - 使用
this.setState更新React中的state - 使用箭頭函數初始化方法類(如:
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

添加狀態(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)。我們將為onIncrement和onDecrement的Props添加兩個可選的回調屬性:
首先,修改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組件轉換為容器,通過以下mapStateToProps和mapDispatchToProps這兩個函數實現:
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首先獲取mapStateToProps和mapDispatchToProps,然后返回另一個用來包裝組件的函數。生成的容器使用以下代碼行定義:
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-redux的Provider將props與container連接起來。導入這些并且將store傳遞給Provider的屬性:
import Hello from './containers/Hello';
import { Provider } from 'react-redux';
ReactDOM.render(
<Provider store={store}>
<Hello />
</Provider>,
document.getElementById('root') as HTMLElement
);