可靠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…

關注公衆號,加入技術交流羣

相關文章
相關標籤/搜索