[译] 更可靠的 React 组件:提纯

原文摘自:https://dmitripavlutin.com/7-architectural-attributes-of-a-reliable-react-component/react

pure、almost-pure 和 impure

一个 纯组件(pure componnet) 老是针对一样的 prop 值渲染出一样的元素;ios

一个 几乎纯的组件(almost-pure compoent) 老是针对一样的 prop 值渲染一样的元素,而且会产生一个 反作用(side effect)编程

在函数式编程的术语里,一个 纯函数(pure function) 老是根据某些给定的输入返回相同的输出。让咱们看一个简单的纯函数:redux

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

对于给定的两个数字,sum() 函数老是返回一样的相加值。axios

一旦对相同的输入返回不一样的输出了,一个函数就变成 非纯(impure) 的了。这种状况可能发生在函数依赖了全局状态的时候。举个例子:api

let said = false;

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

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

即使是使用了一样的参数 'Hello World!',两次的调用返回值也是不一样的。就是由于非纯函数依赖了全局状态: 变量 saidpromise

sayOnce() 的函数体中的 said = true 语句修改了全局状态。这产生了反作用,这是非纯的另外一个特征。bash

所以能够说,纯函数没有反作用,也不依赖全局状态。 其单一数据源就是参数。因此纯函数是能够预测并可判断的,从而可重用并能够直接测试。服务器

React 组件应该从纯函数特性中受益。给定一样的 prop 值,一个纯组件(不要和 React.PureComponent 弄混)老是会渲染一样的元素。来看一看:网络

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

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

能够确定的是 <Message> 接受相同的 prop 值后会渲染出相同的元素。

有时也不老是可以把组件作成纯的。好比要像下面这样依赖一些环境信息:

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,但根据用户输入会渲染不一样的输出。由于要经过 input 域访问环境信息,因此 <InputField> 只能是非纯的。

非纯代码虽然有害但不可或缺。大多数应用都须要全局状态、网络请求、本地存储等等。你能作的只是将非纯代码从纯代码中隔离出来,这一过程又成为提纯(purification)

孤立的非纯代码有明确的反作用,或对全局状态的依赖。在隔离状态下,非纯代码对系统中其他部分的不可预测性影响会下降不少。

来看一些提纯的例子。

案例学习1:从全局变量中提纯

我不喜欢全局变量。它们破坏了封装、形成了不可预测的行为,并使得测试困难重重。

全局变量能够做为可变(mutable)对象使用,也能够当成不可变的只读对象。

改变全局变量会形成组件的不可控行为。数据被随意注入和修改,将干扰一致性比较(reconciliation)过程,这是一个错误。

若是须要可变的全局状态,解决的办法是引入一个可预测的系统状态管理工具,好比 Redux。

全局中不可变的(或只读的)对象常常用于系统配置等。好比包含站点名称、已登陆的用户名或其余配置信息等。

下面的语句定义了一个配置对象,其中保存了站点的名称:

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

随后,<Header> 组件渲染出系统的头部,其中显示了以上定义的站点名称:

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 渲染到一个 <h1> 标签中。当站点名称没有定义(好比赋值为 null)时,头部就不显示。

首先要关注的是 <Header> 是非纯的。在给定相同 children 的状况下,组件会根据 globalConfig.siteName 返回不一样的结果:

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

或是:

// globalConfig.siteName 为 `null`
<Header>Some content</Header>  
// 渲染:
<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() {
    //修改全局变量:
    globalConfig.siteName = null;
    const wrapper = shallow(
      <Header>Some content</Header>
    );
    assert(appWithHeading.find('h1').length === 0);
  });
});
复制代码

为了测试而修改全局变量 globalConfig.siteName = null 既不规范又使人不安。 之因此如此是由于 <Heading> 紧依赖了全局环境。

为了解决这种非纯状况,最好是将全局变量注入组件的做用域,让全局变量做为组件的一个输入。

下面来修改 <Header>,让其再多接收一个 prop siteName。而后用 recompose 库提供的 defaultProps() 高阶组件包裹 <Header>,以确保缺失 prop 时填充默认值:

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 变量了。纯化版本是一个命名过的模块: export function Header() {...},这在测试时是颇有用的。

与此同时,用 defaultProps({...}) 包装过的组件会在 siteName 属性缺失时将其设置为 globalConfig.siteName。正是这一步,非纯组件被分离和孤立出来。

让咱们测试一下纯化版本的 <Header>

import assert from 'assert';  
import { shallow } from 'enzyme';  
import { Header } from './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> 的单元测试很是简单。测试只作了一件事:检验组件是否针对给定的输入渲染出指望的输出。不须要引入、访问或修改全局变量,也没有什么摸不许的反作用了。

设计良好的组件易于测试,纯组件正是如此。

案例学习2:从网络请求中提纯

重温一下以前文章中提过的 <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> 针对相同 props 值渲染相同的输出。而后将反作用隔离到一个叫作 fetch() 的 prop 函数中。这样的组件类型能够称为 几乎纯(almost-pure) 的组件。

让咱们来把非纯组件 <WeatherFetch> 转变为几乎纯的组件。Redux 在将反作用实现细节从组件中抽离出的方面是一把好手。

fetch() 这个 action creator 开启了服务器调用:

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

一个 saga (译注:Sage是一个能够用来处理复杂异步逻辑的中间件,而且由 redux 的 action 触发)拦截了 "FETCH" action,并发起真正的服务器请求。当请求完成后,"FETCH_SUCCESS" action 会被分发:

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 负责更新应用的 state:

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;
  }
}
复制代码

(Redux store 和 sagas 的初始化过程在此被省略了)

即使考虑到使用了 Redux 后须要额外的构造器,如 actions、 reducers 和 sagas,这仍然将 <FetchWeather> 转化为了几乎纯的组件。

那么把 <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>.

当组件加载后,this.props.fetch() 这个 action creator 会被调用,触发一个服务器请求。当请求完成后,Redux 会更新系统状态并让 <WeatherFetch> 从 props 中得到 temperaturewindSpeed

this.props.fetch() 做为被孤立并扁平化的非纯代码,正是它产生了反作用。要感谢 Redux 的是,组件不会再被 axios 库的细节、服务端 URL,或是 promise 搞得混乱。此外,对于相同的 props 值,新版本的 <WeatherFetch> 老是会渲染相同的元素。组件变为了几乎纯的。

相比于非纯的版本,测试几乎纯的 <WeatherFetch> 就更简单了:

import assert from 'assert';  
import { shallow, mount } from 'enzyme';  
import { spy } from 'sinon';  
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);
  });
});
复制代码

要测试的是对于给定的 props, <WeatherFetch> 渲染出了符合指望的 <WeatherInfo>,以及加载后 fetch() 会被调用。简单又易行。

让“几乎纯”的“更纯”

实际上至此为止,你可能已经结束了隔离非纯的过程。几乎纯的组件在可预测性和易于测试方面已经表现不俗了。

可是... 让咱们看看兔子洞到底有多深。几乎纯版本的 <WeatherFetch> 还能够被转化为一个更理想的纯组件。

让咱们把 fetch() 的调用抽取到 recompose 库提供的 lifecycle() HOC 中:

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() HOC 接受一个指定生命周期的对象。componentDidMount() 被 HOC 处理,也就是用来调用 this.props.fetch()。经过这种方式,反作用被从 <WeatherFetch> 中彻底消除了。

如今 <WeatherFetch> 是一个纯组件了。没有反作用,且老是对于给定的相同 temperaturewindSpeed props 值渲染相同的输出。

纯化版本的 <WeatherFetch> 在可预测性和简单性方面无疑是很棒的。为了将非纯组件逐步提纯,虽然增长了引入 compose() 和 lifecycle() 等 HOC 的开销,一般这是很划算的买卖。


(end)


----------------------------------------

转载请注明出处

长按二维码或搜索 fewelife 关注咱们哦

相关文章
相关标签/搜索