TypeScript
有状态组件
当我们的组件需要根据用户的输入更新的时候,我们需要有状态的组件。
深入理解React的有状态组件的最佳实践超出了本文的讨论范围,但是我们可以快速看一下给我们得到Hello
组件加上状态之后是什么样子。我们将渲染两个<button>来更新Hello
组件显示的感叹号的数量。
要做到这一点,我们需要做:
- 为状态定义一个类型(如:
this.state
) - 根据我们在构造函数中给出的props来初始化
this.state
。 - 为我们的按钮创建两个事件处理程序(
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文件。当执行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
添加状态管理
通过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
,以便可以修改状态。我们将为onIncrement
和onDecrement
的Props
添加两个可选的回调属性:
首先,修改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组件转换为容器,通过以下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);
创建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-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
);