1. 无状态函数(Stateless function)
无状态函数是一种创建高度可复用组件的牛逼闪闪的方法,它自己不管理状态,他只是函数。
1 | const Greeting = () => <div>Hi there!</div>; |
可以传递props
和context
。
1 | const Greeting = (props, context) => <div style={{ color: context.color }}>Hi {props.name}!</div>; |
也可以定义局部变量。
1 | const Greeting = (props, context) => { |
当然也可以不定义局部变量,改为函数。
1 | const getStyle = context => ({ |
无状态函数也可以拥有defaultProps
,propTypes
和contextTypes
。
1 | Greeting.propTypes = { |
2. JSX 展开属性(JSX Spread Attributes)
展开属性是 JSX 的一个特性,一种语法糖,用来将一个对象的所有属性作为 JSX 的属性传递。
以下两个例子是等价的
1 | // props written as attributes |
1 | // props "spread" from object |
用它可以方便地将属性转发给底层组件。
1 | const FancyDiv = props => <div className="fancy" {...props} />; |
这时我给可以FancyDiv
组件添加他关心和他不关心的属性。
1 | <FancyDiv data-id="my-fancy-div">So Fancy</FancyDiv> |
注意属性顺序很重要,如果外部传入className
属性,那么FancyDiv
定义的className
将会被覆盖。
1 | <FancyDiv className="my-fancy-div" /> |
也可以让FancyDiv
定义的className
永远生效,只需要将它放在展开属性({…props})后面。
1 | // my `className` clobbers your `className` |
你应该优雅地处理这类情形,这种情况下我会合并使用者定义的className
和组件自身的className
。
1 | const FancyDiv = ({ className, ...props }) => <div className={['fancy', className].join(' ')} {...props} />; |
3. 参数解构(Destructuring Arguments)
参数解构是 ES2015 的特性,它能够很好的配合无状态函数的参数。
以下两个例子是等价的。
1 | const Greeting = props => <div>Hi {props.name}!</div>; |
剩余参数(中文链接)语法可以将剩余的参数手机到一个新对象中。
1 | const Greeting = ({ name, ...props }) => <div>Hi {name}!</div>; |
反过来,这个新对象可以通过展开属性将属性转发给底层组件。
1 | const Greeting = ({ name, ...props }) => <div {...props}>Hi {name}!</div>; |
应该避免将非 DOM 属性转发给原生组件,通过解构可以创建一个不包含高阶组件特有属性的新对象,因此解构可以让这个工作更加简单。
4. 条件渲染(Conditional Rendering)
组件定义内部是不能使用 if/else 条件语句的,但是可以使用条件表达式。
if
1 | { |
else
1 | { |
if-else (tidy one-liners)
1 | { |
if-else (big blocks)
1 | { |
5. Children 类型(Children types)
React 中 children 有好几种类型,常见的有数组和字符串。
字符串
1 | <div>Hello World!</div> |
array
1 | <div>{['Hello ', <span>World</span>, '!']}</div> |
children 也可以是函数,但是必须和父组件协作才能用。
function
1 | <div> |
6. 数组类型的 children(Array as children)
数组类型的 children 是非常常见的,列表就是这么渲染出来的。使用map
函数就可以创建 React 元素数组。
1 | <ul> |
和下面这个数组字面量方式等价
1 | <ul>{[<li>first</li>, <li>second</li>]}</ul> |
为了更加简洁,可以结合解构,JSX 展开属性,其他组件一起使用。
1 | <ul> |
7. 函数类型的 children(Function as children)
函数类型的 children 不是天然有用的。
1 | <div>{() => { return "hello world!"}()}</div> |
这种技术通常被称为渲染回调,可以给组件创作带来更多空间和便利。比如ReactMotion使用这种高能技术以后,渲染逻辑可以由使用者提供,而不是被类库写死。更多细节,请参考下一章渲染回调。
8. 渲染回调(Render callback)
下面这个组件使用了渲染回调技术,它没什么用,但它是一个很好的开端。
1 | const Width = ({ children }) => children(500); |
该组件将 children 当做函数来调用,并传递了一个数字类型值为 500 的参数。
下面我们将使用该组件,并给它传递一个函数类型的 children.
1 | <Width>{width => <div>window is {width}</div>}</Width> |
我们将得到以下结果。
1 | <div>window is 500</div> |
有了这些设置,我们可以根据宽度来决定渲染什么。
1 | <Width>{width => (width > 600 ? <div>min-width requirement met!</div> : null)}</Width> |
如果这个逻辑会被多次使用,我们可以创建一个新组件来封装可重用逻辑。
1 | const MinWidth = ({ width: minWidth, children }) => <Width>{width => (width > minWidth ? children : null)}</Width>; |
很明显这对于一个有着固定宽度的组件没有什么意义,但对一个监听浏览器窗口宽度的组件就有意义了,以下是示例代码。
1 | class WindowWidth extends React.Component { |
很多开发者更喜欢高阶组件完成类似功能,这是个人偏好问题。
9. Children 值传(Children pass-through)
有时候你可能会创建一个组件,只用来处理上下文并且直接渲染其 children.
1 | class SomeContextProvider extends React.Component { |
现在你需要作出决定,将children
包裹在一个<div />
中,还是直接返回children
。第一种做法多了一层标签(可能导致样式失效),第二种做法将会导致一个错误。
1 | // option 1: extra div |
最好的做法是将children
看做一个不透明的数据类型,React
提供了React.Children
来合理的处理children
。
1 | return React.Children.only(this.props.children); |
10. 组件代理(Proxy component)
(我不确定这个名字是否有意义)
按钮(Button)在网页应用中随处可见,每一个按钮都必须有一个type
属性并设成button
。
1 | <button type="button"> |
书写次数多了,也就容易导致错误,我们可以创建一个高阶组件代理该低阶组件。
1 | const Button = props => |
这时我们可以使用Button
代替button
,确保type
属性总被正确使用。
1 | <Button /> |
11. 使用样式(Style component)
这是一种使用样式的组件代理。
假设我们通过使用class
将一个button
装饰成主要(primary)按钮。
1 | <button type="button" className="btn btn-primary"> |
我们可以通过两个单一职责组件达到此目的。
1 | const PrimaryBtn = props => <Btn {...props} primary />; |
便于理解,请看下面的图示。
1 | PrimaryBtn() |
通过这些组件,以下代码是等价的。
1 | <PrimaryBtn /> |
对于样式维护来说真是一大福音,它将样式问题封装在单一组件中。
12. 事件切换(Event switch)
在写事件回调时通过采用handle{EventName}
规则。
1 | handleClick(e) { /* do something */ } |
对于一个需要处理多种事件事件的组件来说,这些函数名显得非常啰嗦。函数名中也不会带有更多信息,因为他们一般直接调用其他action
或function
。
1 | handleClick() { require("./actions/doStuff")(/* action stuff */) } |
下面只给组件写一个事件处理函数,并通过event.type
区分。
1 | handleEvent({type}) { |
或者,对于简单组件,你可以通过胖箭头函数方式直接调用action
或function
。
1 | <div onClick={() => someImportedAction({ action: 'DO_STUFF' })} /> |
不要担心性能问题,知道性能问题爆发。一定不要过早进行性能优化。
13. 布局组件(Layout component)
布局组件会产生一些静态 DOM 元素,他们可能不会有任何改变,即使改变了也不会很频繁。
下面是一个并排显示两个子组件的组件。
1 | <HorizontalSplit leftSide={<SomeSmartComponent />} rightSide={<AnotherSmartComponent />} /> |
我们可以尽量去优化这个组件。
虽然HorizontalSplit
是两个组件的父组件,但是它绝不是这两个组件的所有者。我们可以让它永不更新,不影响组件的生命周期。
1 | class HorizontalSplit extends React.Component { |
14. 容器组件(Container component)
“容器负责获取数据并渲染其子组件,这就够了”
—Jason Bonta
假设我们已经有了可复用的CommentList
组件。
1 | const CommentList = ({ comments }) => ( |
接下来我们可以创建一个新组件负责获取数据并渲染无状态的CommentList
组件。
1 | class CommentListContainer extends React.Component { |
我们可以给不同的应用上下文创建不同的容器组件。
14. 高阶组件(Higher-order component)
高阶函数是一个接受函数类型的参数或返回一个新函数的函数。那么什么是高阶组件呢?
如果你已经开始使用容器组件,它们都是包裹在一个函数中的通用容器。
下面我们从一个无状态的Greeting
组件开始。
1 | const Greeting = ({ name }) => { |
如果Greeting
组件接到props.name
,它就回去渲染这个数据,否则他会说正在连接。现在我们创建一个高阶组件。
1 | const Connect = ComposedComponent => |
它就是一个函数,返回一个渲染作为参数传递进去的组件的新组件。
最后,我们需要用Connect
组件将Greeting
组件包裹起来,如下:
1 | const ConnectedMyComponent = Connect(Greeting); |
高阶组件是一个功能很强的模式,可以用来获取数据并给其他无状态组件提供数据。
15. 状态提升(State hoisting)
无状态组件并不持有状态,正如它名称暗示的那样。
Events are changes in state. Their data needs to be passed to stateful container components parents.
This is called “state hoisting”. It’s accomplished by passing a callback from a container component to a child component.
1 | class NameContainer extends React.Component { |
Name
组件从NameContainer
组件中获得onChange
回调并在事件中调用。
上面的alert
只是简单演示并不修改状态,下面的代码将会修改NameContainer
组件的状态。
1 | class NameContainer extends React.Component { |
通过回调,状态被提升到维护局部状态的容器组件中。这给无状态函数一个清晰的边界和最大限度的可重用性。
这个模式并不局限于无状态函数,因为无状态函数没有生命周期事件,该模式同样适用于无状态组件。
受控的 input 就是一个使用了状态提升的重要模式。
16. 受控的 input(Controlled input)
直接讨论受控的 input 比较困难,我们先从不受控的 input 谈起。
1 | <input type="text" /> |
当你在浏览器中输入框中输入时,你会看到输入框的值发生变化,这很正常。
受控的 input 禁用 DOM 突变,它的值只能被组件修改,不能被 DOM 修改。
1 | <input type="text" value="This won't change. Try it." /> |
上面的输入框有着固定值没有什么意义,下面输入框的值将会从state
中获取。
1 | class ControlledNameInput extends React.Component { |
接着,修改输入框的值就是修改组件状态。
1 | return <input value={this.state.name} onChange={e => this.setState(e.target.value)} />; |
这就是受控的 input,只有当组件的状态改变了才能改变 DOM,对于创建一致的 UI,有着非常大的作用。