有狀態(tài)組件
當(dāng)我們的組件需要根據(jù)用戶的輸入更新的時(shí)候,我們需要有狀態(tài)的組件。
深入理解React的有狀態(tài)組件的最佳實(shí)踐超出了本文的討論范圍,但是我們可以快速看一下給我們得到Hello組件加上狀態(tài)之后是什么樣子。我們將渲染兩個(gè)<button>來更新Hello組件顯示的感嘆號(hào)的數(shù)量。
要做到這一點(diǎn),我們需要做:
- 為狀態(tài)定義一個(gè)類型(如:
this.state) - 根據(jù)我們?cè)跇?gòu)造函數(shù)中給出的props來初始化
this.state。 - 為我們的按鈕創(chuàng)建兩個(gè)事件處理程序(
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定義一個(gè)新的類型:
State - 使用
this.setState更新React中的state - 使用箭頭函數(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

添加狀態(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)。我們將為onIncrement和onDecrement的Props添加兩個(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)換為容器,通過以下mapStateToProps和mapDispatchToProps這兩個(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首先獲取mapStateToProps和mapDispatchToProps,然后返回另一個(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-redux的Provider將props與container連接起來。導(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
);