可靠React组件设计的7个准则之纯组件

翻译:刘小夕javascript

原文连接:dmitripavlutin.com/7-architect…html

原文的篇幅很是长,不过内容太过于吸引我,仍是忍不住要翻译出来。此篇文章对编写可重用和可维护的React组件很是有帮助。但由于篇幅实在太长,我对文章进行了分割,本篇文章重点阐述 纯组件和几乎纯组件 。因水平有限,文中部分翻译可能不够准确,若是你有更好的想法,欢迎在评论区指出。java

更多优质文章可戳: github.com/YvetteLau/B…react

———————————————我是一条分割线————————————————ios

纯组件和几乎纯组件

纯组件老是为相同的属性值渲染相同的元素。几乎纯的组件老是为相同的属性值呈现相同的元素,可是会产生反作用。git

在函数编程属于中,对于给定的相同输入,纯函数老是返回相同的输出,而且不会对外界产生反作用。github

function sum(a, b) {
    return a + b;
}
sum(5, 10); // => 15
复制代码

对于给定的两个数字,sum() 函数老是会返回相同的结果。编程

当一个函数输入相同,而输出不一样时,它就不是一个纯函数。当这个函数依赖于全局的状态时,就不是一个纯函数,例如:redux

let said = false;

function sayOnce(message) {
    if (said) {
        return null;
    }
    said = true;
    return message;
}

sayOnce('Hello World!'); // => 'Hello World!'
sayOnce('Hello World!'); // => null
复制代码

sayOnce('Hello World!') 第一次调用时,返回 Hello World.axios

即便输入参数相同,都是 Hello World,可是第二次调用 sayOnce('Hello World!'),返回的结果是 null 。这里有一个非纯函数的特征:依赖全局状态 said

sayOnce() 的函数体内,said = true 修改了全局状态,对外界产生的反作用,这也是非纯函数的特征之一。

而纯函数没有反作用且不依赖于全局状态。只要输入相同,输出必定相同。所以,纯函数的结果是可预测的,肯定的,能够复用,而且易于测试。

React 组件也应该考虑设计为纯组件,当 prop 的值相同时, 纯组件(注意区分React.PureComponent)渲染的内容相同,一块儿来看例子:

function Message({ text }) {
    return <div className="message">{text}</div>;
}

<Message text="Hello World!" />
// => <div class="message">Hello World</div>
复制代码

当传递给 Messageprop 值相同时,其渲染的元素也相同。

想要确保全部的组件都是纯组件是不可能的,有时候,你须要知道与外界交互,例以下面的例子:

class InputField extends Component {
    constructor(props) {
        super(props);
        this.state = { value: '' };
        this.handleChange = this.handleChange.bind(this);
    }

    handleChange({ target: { value } }) {
        this.setState({ value });
    }

    render() {
        return (
            <div> <input type="text" value={this.state.value} onChange={this.handleChange} /> You typed: {this.state.value} </div> ); } } 复制代码

<InputField> 组件,不接受任何 props,而是根据用户的输入内容渲染输出。<InputField> 必须是非纯组件,由于它须要经过 input 输入框与外界交互。

非纯组件是必要的,大多数应用程序中都须要全局状态,网络请求,本地存储等。你所能作的就是将 纯组件和非纯组件隔离,也就是说将你的组件进行提纯

非纯代码显式的代表了它有反作用,或者是依赖全局状态。在隔离状态下,不纯代码对系统其它部分的不可预测的影响较小。

让咱们详细介绍一下提纯的例子。

案例研究:从全局变量中提取纯组件

我不喜欢全局变量,由于它们打破了封装,创造了不可预测的行为,而且使测试变得困难。

全局变量能够做为可变对象或者是不可变对象使用。

可变的全局变量使得组件的行为难以控制,数据能够随意的注入和修改,影响协调过程,这显然是错误的。

若是你须要可变的全局状态,那么你能够考虑使用 Redux 来管理你的应用程序状态。

不可变的全局变量一般是应用程序的配置对象,这个对象中包含站点名称、登陆用户名或者其它的配置信息。

如下代码定义一个包含站点名称的配置对象:

export const globalConfig = {
    siteName: 'Animals in Zoo'
};
复制代码

<Header> 组件渲染应用的头部,包括展现站点名称: Animals in Zoo

import { globalConfig } from './config';

export default function Header({ children }) {
    const heading =
        globalConfig.siteName ? <h1>{globalConfig.siteName}</h1> : null;
    return (
        <div> {heading} {children} </div>
    );
}
复制代码

<Header> 组件使用 globalConfig.siteName 来展现站点名称,当 globalConfig.siteName 未定义时,不显示。

首先须要注意的是 <Header> 是非纯组件。即便传入的 children 值相同,也会由于 globalConfig.siteName 值的不一样返回不一样的结果。

// globalConfig.siteName is 'Animals in Zoo'
<Header>Some content</Header>
    // Renders:
    <div>
        <h1>Animals in Zoo</h1>
        Some content
</div>
复制代码

或:

// globalConfig.siteName is `null`
<Header>Some content</Header>
    // Renders:
    <div>
        Some content
</div>
复制代码

其次,测试变得困难重重,为了测试组件如何处理站点名为 null,咱们不得不手动地设置 globalConfig.siteName = null

import assert from 'assert';
import { shallow } from 'enzyme';
import { globalConfig } from './config';
import Header from './Header';

describe('<Header />', function () {
    it('should render the heading', function () {
        const wrapper = shallow(
            <Header>Some content</Header>
        );
        assert(wrapper.contains(<h1>Animals in Zoo</h1>));
    });

    it('should not render the heading', function () {
        // Modification of global variable:
        globalConfig.siteName = null;
        const wrapper = shallow(
            <Header>Some content</Header>
        );
        assert(appWithHeading.find('h1').length === 0);
    });
});
复制代码

为了测试而修改 globalConfig.siteName = null 是不方便的。发生这种状况是由于 <Heading> 对全局变量有很强的依赖。

为了解决这个问题,能够将全局变量做为组件的输入,而非将其注入到组件的做用域中。

咱们来修改一下 <Header> 组件,使其多接受一个 siteNmaeprop, 而后使用 recompose 库中的 defaultProps 高阶组件来包装组件,defaultProps 能够保证在没有传入props时,使用默认值。

import { defaultProps } from 'recompose';
import { globalConfig } from './config';

export function Header({ children, siteName }) {
    const heading = siteName ? <h1>{siteName}</h1> : null;
    return (
        <div className="header"> {heading} {children} </div>
    );
}

export default defaultProps({
    siteName: globalConfig.siteName
})(Header);
复制代码

<Header> 变成了一个纯函数组合,再也不直接依赖 globalConfig 变量,让测试变得简单。

同时,当咱们没有设置 siteName时,defaultProps 会传入 globalConfig.siteName 做为 siteName 属性值。这就是不纯代码被分离和隔离开的地方。

如今让咱们测试纯版本的 <Header> 组件:

import assert from 'assert';
import { shallow } from 'enzyme';
import { Header } from './Header'; // Import the pure Header

describe('<Header />', function () {
    it('should render the heading', function () {
        const wrapper = shallow(
            <Header siteName="Animals in Zoo">Some content</Header>
        );
        assert(wrapper.contains(<h1>Animals in Zoo</h1>));
    });

    it('should not render the heading', function () {
        const wrapper = shallow(
            <Header siteName={null}>Some content</Header>
        );
        assert(appWithHeading.find('h1').length === 0);
    });
});
复制代码

如今好了,测试纯组件 <Header> 很简单。测试作了一件事:验证组件是否呈现给定输入的预期元素。无需导入、访问或修改全局变量,无反作用。设计良好的组件易于测试。

案例研究:从网络请求中提取纯组件

回顾 <WeatherFetch> 组件,当其挂载时,它会发出网络请求去获取天气信息。

class WeatherFetch extends Component {
    constructor(props) {
        super(props);
        this.state = { temperature: 'N/A', windSpeed: 'N/A' };
    }

    render() {
        const { temperature, windSpeed } = this.state;
        return (
            <WeatherInfo temperature={temperature} windSpeed={windSpeed} /> ); } componentDidMount() { axios.get('http://weather.com/api').then(function (response) { const { current } = response.data; this.setState({ temperature: current.temperature, windSpeed: current.windSpeed }) }); } } 复制代码

<WeatherFetch> 是非纯组件,由于相同的输入会产生不一样的输出,由于组件渲染依赖于服务端的返回结果。

不幸的是,HTTP 请求的反作用是没法消除的,<WeatherFetch> 的职责就是从服务端请求数据。

可是你可让 <WeatherFetch> 为相同的属性值渲染相同的内容。这样就能够将反作用隔离到 prop 的函数属性 fetch() 上。这样的一个组件类型被称为几乎纯组件。

咱们来将非纯组件<WeatherFetch>改写成几乎纯组件。 Redux 能够很好的帮助咱们将反作用的实现细节从组件中提取出来。所以,咱们须要设置一些 Redux 的结构。

fetch() action creater 启动服务器调用:

export function fetch() {
    return {
        type: 'FETCH'
    };
}
复制代码

redux-saga 拦截了 Fetch action, 实际想服务端请求,当请求完成时,派发 FETCH_SUCCESSaction

import { call, put, takeEvery } from 'redux-saga/effects';

export default function* () {
    yield takeEvery('FETCH', function* () {
        const response = yield call(axios.get, 'http://weather.com/api');
        const { temperature, windSpeed } = response.data.current;
        yield put({
            type: 'FETCH_SUCCESS',
            temperature,
            windSpeed
        });
    });
}
复制代码

这个 reducer 负责更新应用的状态。

const initialState = { temperature: 'N/A', windSpeed: 'N/A' };

export default function (state = initialState, action) {
    switch (action.type) {
        case 'FETCH_SUCCESS':
            return {
                ...state,
                temperature: action.temperature,
                windSpeed: action.windSpeed
            };
        default:
            return state;
    }
}
复制代码

ps: 为了简单起见,省略了 Redux storesagas 的初始化。

尽管使用 Redux 须要额外的结构,例如: actions ,reducerssagas,可是它有助于使得 <WeatherFetch> 成为几乎纯组件。

咱们来修改一下 <WeatherFetch> ,使其和 Redux 结合起来。

import { connect } from 'react-redux';
import { fetch } from './action';

export class WeatherFetch extends Component {
    render() {
        const { temperature, windSpeed } = this.props;
        return (
            <WeatherInfo temperature={temperature} windSpeed={windSpeed} /> ); } componentDidMount() { this.props.fetch(); } } function mapStateToProps(state) { return { temperature: state.temperate, windSpeed: state.windSpeed }; } export default connect(mapStateToProps, { fetch }); 复制代码

connect(mapStateToProps, { fetch }) HOC 包装了 <WeatherFetch>.

当组件挂载时,action creator this.props.fetch() 被调用,触发服务端请求,当请求完成时, Redux 更新应用的 state,使得 <WeatherFetch>props 中接收 temperaturewindSpeed

this.props.fetch 是为了隔离产生反作用的非纯代码。由于 Redux 的存在,组件内部再也不须要使用 axois 库,请求 URL 或者是处理 promise。此外,新版本的 <WeatherFetch>会为相同的props值渲染相同的元素。这个组件变成了几乎纯组件。

与非纯版本相比,测试几乎纯版本 <WeatherFetch> 更加容易:

import assert from 'assert';
import { shallow, mount } from 'enzyme';
import { spy } from 'sinon';
// Import the almost-pure version WeatherFetch
import { WeatherFetch } from './WeatherFetch';
import WeatherInfo from './WeatherInfo';

describe('<WeatherFetch />', function () {
    it('should render the weather info', function () {
        function noop() { }
        const wrapper = shallow(
            <WeatherFetch temperature="30" windSpeed="10" fetch={noop} />
        );
        assert(wrapper.contains(
            <WeatherInfo temperature="30" windSpeed="10" />
        ));
    });

    it('should fetch weather when mounted', function () {
        const fetchSpy = spy();
        const wrapper = mount(
            <WeatherFetch temperature="30" windSpeed="10" fetch={fetchSpy} />
        );
        assert(fetchSpy.calledOnce);
    });
});
复制代码

你须要检查,给定的 prop 值,<WeatherFetch>的渲染结果是否与预期一致,并在挂载时调用 fetch()。简单且明了。

将几乎纯组件转换成纯组件

实际上,在这一步,你不在须要分离不纯的代码,几乎纯组件具备良好的可预测性,而且易于测试。

可是...咱们一块儿来看看兔子洞究竟有多深。几乎纯版本的 <WeatherFetch> 组件能够被转换成一个理想的纯组件。

咱们来将 fethc 回调提取到 recompose 库的 lifecycle() 高阶组件中。

import { connect } from 'react-redux';
import { compose, lifecycle } from 'recompose';
import { fetch } from './action';

export function WeatherFetch({ temperature, windSpeed }) {
    return (
        <WeatherInfo temperature={temperature} windSpeed={windSpeed} /> ); } function mapStateToProps(state) { return { temperature: state.temperate, windSpeed: state.windSpeed }; } export default compose( connect(mapStateToProps, { fetch }), lifecycle({ componentDidMount() { this.props.fetch(); } }) )(WeatherFetch); 复制代码

lifecycle() 高阶组件接受一个有生命周期方法的对象。 调用 this.props.fecth() 方法的 componentDidMount() 由高阶组件处理,将反作用从 <WeatherFetch> 中提取出来。

如今,<WeatherFetch> 是一个纯组件,它再也不有反作用,而且当输入的属性值 temperaturewindSpeed 相同时,输出老是相同。

虽然纯版本的 <WeatherFetch> 在可预测性和捡东西方面很好,可是它须要相似 compose()lifecycle() 等高阶组件,所以,一般,是否将几乎纯组件转换成纯组件须要咱们去权衡。

最后谢谢各位小伙伴愿意花费宝贵的时间阅读本文,若是本文给了您一点帮助或者是启发,请不要吝啬你的赞和Star,您的确定是我前进的最大动力。github.com/YvetteLau/B…

关注公众号,加入技术交流群

相关文章
相关标签/搜索