原文地址:10 React mini-patterns
对于 React 初学者的一些经验之谈,初次翻译,望多指教。

过去几年里,我参与了几个较大的 React 项目,也做了非常多的小项目,在这过程中,我总结了一些 React 常用的开发模式。
这些模式是我在 React 入门阶段非常希望看到的,如果你是 React 新手,那你今天算是赚到了,如果你已经是 React 老鸟,不妨看看有哪些模式。本文较长,如果觉得一些介绍比较枯燥(比如 3、6、8、10),可以选择跳过。

1. 数据传输

我建议每个 React 初学者都去了解 React 组件向下传递数据(对象、字符串等)的模式,以及传递让子组件传回数据的方法的模式。可以联想救援队将食物与对讲机送抵井底被困矿工。
举个例子:

图中左右分别是父组件和子组件,你可以想象连接父子组件的这两个属性允许了数据的双向流通。

其实这一条不算真正意义上的开发模式,下面才是。

2. 修复 HTML 原生 input

React 和 web 组件的一大好处是,当页面出现 bug,你可以很快定位到问题所在。
如果你在考虑页面中使用不同的输入标签,你会发现这些标签的命名大多是无意义,因此,如果我在处理一个包含多个输入的页面时,我首先会处理这个问题。

这些输入标签虽然不是纯粹的花架子,但是仍有很多地方可以改进:

  • 输入应该通过 onChange 方法返回值,而不是通过 Javascript 事件绑定
  • 保证 onChange 返回值的类型与输入的值类型统一,如果 typeof props.value 是一个 number 类型,应该将 e.target.value 转换成 number 类型再返回
  • 一套 radio 标签和一个 select 标签在功能上都是相同的,唯一的区别只是 UI 的不同,推荐项目中保留一个 <PickOneFromMany /> 组件,可以通过属性 ui="radio" 或者 ui="dropDown" 进行控制

以上是我处理原生输入标签时用到的方法,你可以选择其他方式,关键是将这些原生标签转换成为你所用,再也不需要忍受那些糟糕的原生输入标签了。

3. 给 input 绑定唯一 ID 的标签

关于 input 输入,如果你注重用户体验,你应该给每个 <input> 标签绑定一个 <label>,通过 id/for 属性进行关联。
但如果给每一个 input 都想一个独一无二且生动形象的 ID,那样太浪费时间,而且使用随机生成 ID 的方式也不可行,客户端与服务器端生成的 ID 不同,导致校验不通过,这里推荐你创建一个提供增量 ID 的模块,并在 input 组件中使用,如下所示:

class Input extends React.Component {
    constructor(props) {
        super(props);
        this.id = getNextId();
        this.onChange = this.onChange.bind(this);
  }
   onChange(e) {
        this.props.onChange(e.target.value);
  }
   render() {
       return (
       <label htmlFor={this.id}>
           {this.props.label}
           <input
               id={this.id}
               value={this.props.value} 
               onChange={this.onChange}
               />
               </label>
           );
      }
}

虽然这里解决 ID 的问题,但是这个方案有漏洞,getNextId() 方法每被调用一次,数字会增加,如果是在服务端渲染,这个数字会持续增加到,因此应该在每次渲染之前进行一次重置(每一次网络请求)。因此,一个完整的获取 ID 模块应该是这样:

let count = 1;
export const resetId = () => {
    count = 1;
}
export const getNextId = () => {
    return element-id-${count++};
}

4. 通过 props 控制 CSS

当你想在不同的实例中应用不同的 CSS 样式,你可以通过传入不同的 props 值来控制需要应用的样式。表面上看,这样的操作似乎很简单,但实际应用中往往会出现很多错误。
我总结共有三种不同的方式来控制组件的样式:

使用主题

借鉴主题的思路,将一系列的 CSS 声明组合在一起,统一成一个主题,在组件中生命组件的主题,例如 primary 按钮以及 secondary 按钮:
<Button theme="secondary">Hello</Button>
一个组件中尽量使用一个主题。

使用标记

也许你的页面中会有一些圆角 button,但这样的风格不符合你已经定义的主题,遇到这种情况,你可能要去找 UI 商量一个统一的方案,或是在元素中添加一个布尔属性,像这样:
<Button theme="secondary" rounded>Hello</Button>
等同于这种写法:
<Button theme="secondary" rounded={true}>Hello</Button>

设置值

当然,你肯定会遇到直接在组建中写 CSS 样式的情况,像这样:
<Icon width="25" height="25" type="search" />

举个例子

设想你现在需要实现一个链接,但现在有三种截然不同的主题,一些链接有下划线,一些没有,就像这样:

下面给出我的处理方式:

// demo.jsx
const Link = (props) => {
  let className = link link--${props.theme}-theme;
  if (!props.underline) className += ' link--no-underline';
  return <a href={props.href} className={className}>{props.children}</a>;
};
Link.propTypes = {
  theme: PropTypes.oneOf([
'default', // primary color, no underline
'blend', // inherit surrounding styles
'primary-button', // primary color, solid block
  ]),
  underline: PropTypes.bool,
  href: PropTypes.string.isRequired,
  children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.array,
PropTypes.string,
  ]).isRequired,
};
Link.defaultProps = {
  theme: 'default',
  underline: false,
};

CSS 代码如下:

// demo.css
.link--default-theme,
.link--blend-theme:hover {
  color: #D84315;
}

.link--blend-theme {
  color: inherit;
}

.link--default-theme:hover,
.link--blend-theme:hover {
  text-decoration: underline;
}

.link--primary-button-theme {
  display: inline-block;
  padding: 12px 25px;
  font-size: 18px;
  background: #D84315;
  color: white;
}

.link--no-underline {
  text-decoration: none;
}

你可能注意到我在类名(例如 link-no-underline )中使用了 --,源自于我过去一直以少写 CSS 代码为目标,但后来意识到这是错的。如果样式可以更好地应用在布局中,我更喜欢使用一些双重和多选择器规则集。
虽然我之前提过,但我还想再强调一下,扩展一个网站最困难的部分是 CSS 的部分,Javascript 部分都很容易,CSS 开始就写的很混乱,后面维护会非常困难,一入布局深似海。
实际项目中,一些 web 开发者往往被 CSS 特异性给难倒了,如果你在浏览网页,不妨查看一下页面中的元素(比如导航栏中的提示图标)是如何用 CSS 实现的。
如果你不想打开控制台去找,也可以思考这个元素(比如圆圈中包含数字)的实现样式涉及了哪些 CSS 规则。

译者注:所说的元素包含在原文站点中

共有二十三条规则,还不包括从其他十一条规则下集成的规则,其中 line-height 被复写了九次……即便 line-height 是一只猫,也未能幸免于难。

译者注:猫有九命的梗

使用 React 之后,我们更好地处理页面样式,更周到地决定设计哪些类应用在我们的组件中,将全局设置迁移到 Button.scss 文件中,移除所有对于特异性以及文件顺序的依赖。
边注:我梦想有一天,我们再也不需要浏览器对于样式使用的建议。::user-agent-styles: none-whatsoever; 让这样的梦想成为现实。

5. 开关组件

开关组件是呈现众多组件之一的组件。可以是用来展示页面的 <Page> 组件或者是 tab 集合中的 tab,也可以是模态组件中的不同模式。
我过去习惯使用 switch 语句处理,实际传递到我想要渲染的组件,再从组件本身导出对组件的引用(作为命名导出,作为组件的属性)。
现在看来这些都是可怕的方式,我已经解决的一个潜在危险方法是我用一个对象将 prop 值映射到组件中。

import HomePage from './HomePage.jsx';
import AboutPage from './AboutPage.jsx';
import UserPage from './UserPage.jsx';
import FourOhFourPage from './FourOhFourPage.jsx';
const PAGES = {
home: HomePage,
about: AboutPage,
user: UserPage,
};
const Page = (props) => {
const Handler = PAGES[props.page] || FourOhFourPage;
return <Handler {...props} />
};
Page.propTypes = {
page: PropTypes.oneOf(Object.keys(PAGES)).isRequired,
};

PAGE 对象中的值可以在 prop 类型中用来捕获开发时错误。当然,我们可以像这样使用 <Page page="home" />,如果你将 homeaboutuser 分别提换成 //about 以及 /user,那么你就有了半个路由啦。
(下一步想法:移除 react-router)

6. 进入组件内部

如果你想提高用户体验,不妨试试在页面输入较频繁的输入框添加 autofocus,非常简单,但却可以大大提高用户的使用体验。
设想页面中有一个登陆表单,而作为“用户体验高级设计师”的你想在表单的“用户名”输入框中添加一个闪烁的光标,但发现登陆表单显示在模态框中,而 autofocus 属性只能应用在页面加载。
现在你该怎么办 ?
你可能会用 Javascript 实现,给 input 标签一个 id,再用 document.getElementById('user-name-input').focus() 让输入框聚焦。这种方法虽然有效,但不够优雅,你的程序中对字符串匹配的依赖应该越少越好。
比较幸运的是,有一种非常简单的方法可以实现这个效果:

class SignInModal extends Component {
  componentDidMount() {
this.InputComponent.focus();
  }
  render() {
return (
  <div>
<label>User name: </label>
<Input
  ref={comp => { this.InputComponent = comp; }}
/>
  </div>
)
  }
}

需要注意的是,当你对组件使用 ref 时,是对组件的引用(而不是底层元素),所以你可以访问其方法。

7. 组件的边界

设想你正在写一个搜索用户的组件,当你输入的时候,你会看到一列潜在匹配的用户名和头像,就像这样。

当你在设计这个组件时,你可能会犹豫,列表中的每一项都属于 SearchSuggestion 组件吗 ?只有几行 HTML 和 CSS 代码,也许不是 ?但我曾经告诉自己:“如果感到疑惑,那就再建一个新组件”。

我如果这样做,就一个单独的组件都没了。相反,只有一个给每个入口返回对应 DOM 的 renderSearchSuggestion 方法,我就生成了如下的结果:

const SearchSuggestions = (props) => {
  // renderSearchSuggestion() behaves as a pseduo SearchSuggestion component
  // keep it self contained and it should be easy to extract later if needed
  const renderSearchSuggestion = listItem => (
<li key={listItem.id}>{listItem.name} {listItem.id}</li>
  );
  return (
<ul>
  {props.listItems.map(renderSearchSuggestion)}
</ul>
  );
}

如果需求变得更复杂或者你想在其他地方使用这个组件,你可以把这段代码复制到新的组件中。

不要过早地组件化,组件不像茶匙,你可以有很多组件。

我的意思不是要你把你觉得应该独立成组件的部分合并到父组件中,而是想让你把那些你认为不应该独立成组件的部分做一些改进,让它看起来和所在的组件更贴合(如果可以的话)。

8. 用于格式化文字的组件

当我刚接触 React 时,我觉得组件是一个非常大的东西,一种给 DOM 结构分组的方法,但实际上,组件就像是用于格式化的一种方法。
这里有一个 <Price> 组件,输入一个数字会返回一个漂亮的字符串(加上小数点或者 $ 符)。

const Price = (props) => {
const price = props.children.toLocaleString('en', {
  style: props.showSymbol ? 'currency' : undefined,
  currency: props.showSymbol ? 'USD' : undefined,
  maximumFractionDigits: props.showDecimals ? 2 : 0,
});
return <span className={props.className}>{price}</span>
};

Price.propTypes = {
  className: React.PropTypes.string,
  children: React.PropTypes.number,
  showDecimals: React.PropTypes.bool,
  showSymbol: React.PropTypes.bool,
};

Price.defaultProps = {
  children: 0,
  showDecimals: true,
  showSymbol: true,
};

const Page = () => {
  const lambPrice = 1234.567;
  const jetPrice = 999999.99;
  const bootPrice = 34.567;
  return (
<div>
  <p>One lamb is <Price className="expensive">{lambPrice}</Price></p>
  <p>One jet is <Price showDecimals={false}>{jetPrice}</Price></p>
  <p>Those gumboots will set ya back <Price showDecimals={false} showSymbol={false}>{bootPrice}</Price> bucks.</p>
</div>
  );
};

注意:代码中没有对获取的数字进行校验……

9. Store 服务于组件

这行代码我已经写过无数遍了(虽然夸张了点):
if (props.user.signInStatus === SIGN_IN_STATUSES.SIGNED_IN)...
最近我意识到,我这样做是不是错了,我想知道的是“用户登录了没”,而不是“用户登录的状态是否等于已登录 ?”
对于我的组件而言,他们应该有足够的发展,而不该因为了忧虑这些小事叨扰它们,他们不该管得到的 price 参数是否是 Number 类型,也不应该为了一个参数的 true 或者 false 烦心。
如你所见,如果在 store 中定义的数据符合你的组件要求,你的组件就会简洁很多。如我之前所说,bug 隐藏在复杂逻辑之后,你的组件越简洁,出现 bug 的几率就越低。
但开发中肯定会遇到一些复杂的场景,关于如何解决这些问题,我这里有几点经验:

  1. 制定组件的一般结构以及其所需要的数据
  2. 设计满足这些需求的 stroe
  3. 尽量使你传入的数据匹配 stroe 的要求
    关于最后一点,我建议创建一个单独的模块来完成所有输入数据的格式处理,属性重命名、字符串转数字、对象转数组、Date 字符串转 Date 对象等等。

10. 不要通过相对路径引入组件

import Button from '../../../../Button/Button.jsx';
import Icon from '../../../../Icon/Icon.jsx';
import Footer from '../../Footer/Footer.jsx';

用下面的方式替代上面的引用方式,是不是觉得清爽很多 ?
import {Button, Icon, Footer} from ‘Components’;
理论上可以这么做:

  • 创建一个 index.js 文件来引用你所有的组件
  • 使用 Webpack 的 resolve.alias 来重定向所有组件到 index 文件
    我目前还没尝试过这种方法,我打算在先有的项目中拿一个出来转换成这样的组织方式(蛤蛤,骗你的,我一直都是这么做的)。但正如我之前写的代码一样,我后来意识到这种方式是错的,原因如下:
  1. Webpack 2 中的 resolve.alias 失效了
  2. 因为组件不在 node_modules 里,所以这算是一个 eslint 错误
  3. 如果你有一个好的 IDE,那么它会知道项目里的所有组件,如果你忘了加一些属性值,它会温馨地提示你添加,你可以通过 cmd/Ctrl + 点击就可以打开这些组件所在的文件。如果用我之前的方式引用组件,那么 IDE 将找不到我的组件的位置,我就失去了这些温馨智能的功能。

标注:matthew hsiung 在关于 eslint 和 WebStorm 的 issue 回复下面提供了一个解决方案