Skip to content

yueshuiniao/reactpatterns.com

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

85 Commits
 
 

Repository files navigation

Contents

无状态函数

无状态函数 是定义高度可复用组件的绝妙方法。它们不保存状态,它们仅仅是函数。

const Greeting = () => <div>Hi there!</div>

它们得到传入的propscontext.

const Greeting = (props, context) =>
  <div style={{color: context.color}}>Hi {props.name}!</div>

它们可以定义函数块内的局部变量。

const Greeting = (props, context) => {
  const style = {
    fontWeight: "bold",
    color: context.color,
  }

  return <div style={style}>{props.name}</div>
}

但使用其他函数可以得到相同的结果。

const getStyle = context => ({
  fontWeight: "bold",
  color: context.color,
})

const Greeting = (props, context) =>
  <div style={getStyle(context)}>{props.name}</div>

它们也可以定义defaultProps, propTypescontextTypes.

Greeting.propTypes = {
  name: PropTypes.string.isRequired
}
Greeting.defaultProps = {
  name: "Guest"
}
Greeting.contextTypes = {
  color: PropTypes.string
}

JSX扩展属性

扩展属性是一个JSX特性。这是用于将所有对象的属性作为JSX属性传递的语法糖。

这两个例子是等价的。

// 写作属性的props
<main className="main" role="main">{children}</main>

// 从对象中扩展的props
<main {...{className: "main", role: "main", children}} />

使用这种方式传送props给下层组件

const FancyDiv = props =>
  <div className="fancy" {...props} />

现在,我可以期待FancyDiv添加它所关注还不属于它的属性。

<FancyDiv data-id="my-fancy-div">So Fancy</FancyDiv>

// output: <div className="fancy" data-id="my-fancy-div">So Fancy</div>

要记得顺序很重要。如果props.className被定义,它将会破坏FancyDiv定义的className

<FancyDiv className="my-fancy-div" />

// output: <div className="my-fancy-div"></div>

我们可以将FancyDivclassName放置在扩展props{...props})之后,使其始终“获胜”。

// my `className` clobbers your `className`
const FancyDiv = props =>
  <div {...props} className="fancy" />

你应该优雅地处理这些类型的props。在这种情况下,我将合并作者的props.className和样式组件所需的className

const FancyDiv = ({ className, ...props }) =>
  <div
    className={["fancy", className].join(' ')}
    {...props}
  />

解构参数

解构赋值是一个ES2015特性。它与无状态函数中的props配对得很好。

这些例子是等价的。

const Greeting = props => <div>Hi {props.name}!</div>

const Greeting = ({ name }) => <div>Hi {name}!</div>

剩余参数语法 (...)允许你收集新对象中的所有其余属性。

const Greeting = ({ name, ...props }) =>
  <div>Hi {name}!</div>

反过来,该对象可以使用JSX扩展属性props转发给组合组件。

const Greeting = ({ name, ...props }) =>
  <div {...props}>Hi {name}!</div>

避免将非DOM props转发给组合组件。解构使得这很容易,因为你可以创建一个新的props对象,而不需要特定的组件props

条件渲染

你不能在组件定义中使用常规的if/else条件。条件(三元)运算符是你的朋友。

if

{condition && <span>Rendered when `truthy`</span> }

unless

{condition || <span>Rendered when `falsey`</span> }

if-else (整洁的一行)

{condition
  ? <span>Rendered when `truthy`</span>
  : <span>Rendered when `falsey`</span>
}

if-else (大块儿)

{condition ? (
  <span>
    Rendered when `truthy`
  </span>
) : (
  <span>
    Rendered when `falsey`
  </span>
)}

子元素类型

React可以渲染多种类型的子元素。在大多数情况下,它是一个'array'或一个'string'。

string

<div>
  Hello World!
</div>

array

<div>
  {["Hello ", <span>World</span>, "!"]}
</div>

函数可能被用作子元素。但是,它需要与父组件协调才有用。

function

<div>
  {(() => { return "hello world!"})()}
</div>

数组做为子元素

提供一个数组作为子元素是非常普遍的。在React中列表就是这么绘制的。

我们使用map()创建一个数组,该数组中的每个值对应一个React Elements。

<ul>
  {["first", "second"].map((item) => (
    <li>{item}</li>
  ))}
</ul>

这相当于提供一个直译的数组。

<ul>
  {[
    <li>first</li>,
    <li>second</li>,
  ]}
</ul>

这个模式可以与解构,JSX扩展属性和其他组件结合使用,以获得一些严谨的简洁。

<ul>
  {arrayOfMessageObjects.map(({ id, ...message }) =>
    <Message key={id} {...message} />
  )}
</ul>

函数作为子元素

用函数作为子元素不是天生就有用的。

<div>{(() => { return "hello world!"})()}</div>

但是,它能被用在一些严肃有力的组件创造上。这种技术通常被称为渲染回调

这种强大的技巧被很多库使用像ReactMotion。在应用时,渲染逻辑可以保存在所有者组件中,而不是被委派。

更多详细信息,请参阅渲染回调

渲染回调

这是一个使用渲染回调的组件。它没有用,但是一个用来开始的简单例子。

const Width = ({ children }) => children(500)

该组件把子组件当做函数调用,传入一些数字参数。在这里是数字500。

为了使用这个组件,我们给它一个函数作为子元素

<Width>
  {width => <div>window is {width}</div>}
</Width>

我们得到这个输出。

<div>window is 500</div>

通过这个设置,我们可以使用这个宽度来作出渲染决定。

<Width>
  {width =>
    width > 600
      ? <div>min-width requirement met!</div>
      : null
  }
</Width>

如果我们打算使用这个条件,我们可以定义另一个组件来封装重用的逻辑。

const MinWidth = ({ width: minWidth, children }) =>
  <Width>
    {width =>
      width > minWidth
        ? children
        : null
    }
  </Width>

很显然,一个静态的width组件并不是很有用,但是它可以监视浏览器窗口。这是一个示例实现。

class WindowWidth extends React.Component {
  constructor() {
    super()
    this.state = { width: 0 }
  }

  componentDidMount() {
    this.setState(
      {width: window.innerWidth},
      window.addEventListener(
        "resize",
        ({ target }) =>
          this.setState({width: target.innerWidth})
      )
    )
  }

  render() {
    return this.props.children(this.state.width)
  }
}

许多开发人员喜欢这种类型的函数性的高阶组件。这是一个偏好的问题。

子元素传递

你可以创建一个旨在应用上下文(context)并呈现其子级的组件。

class SomeContextProvider extends React.Component {
  getChildContext() {
    return {some: "context"}
  }

  render() {
    // how best do we return `children`?
  }
}

你面临着一个决定。将子元素包裹在无关<div />中或直接返回子元素。第一个选项会增加额外的标记(这可能破坏一些样式表)。第二个将导致无益的错误(译注:我用15.4+版本测试的这里不会报错,不知道是作者表述有误还是我理解有误,demo)。

// option 1: 额外的div
return <div>{children}</div>

// option 2: 无益的错误
return children

最好把子元素当成一种不透明的数据类型。React提供React.Children来适当地处理子元素。

return React.Children.only(this.props.children)

代理组件

(我不确定这个名字是否有意义)

网络应用程序中的按钮无处不在。而且它们每个都必须将类型属性设置为"button"。

<button type="button">

写这个属性几百次是容易出错的。我们可以编写更高级别的组件来代理props到更低级别的按钮组件。

const Button = props =>
  <button type="button" {...props}>

我们可以使用Button来代替button,并确保类型属性始终适用于任何地方。

<Button />
// <button type="button"><button>

<Button className="CTA">Send Money</Button>
// <button type="button" class="CTA">Send Money</button>

样式组件

这是一个应用于样式实践的代理组件.

假设我们有一个按钮。它使用class "primary"作为按钮样式。

<button type="button" className="btn btn-primary">

我们可以使用几个单一用途的组件来生成这个输出。

import classnames from 'classnames'

const PrimaryBtn = props =>
  <Btn {...props} primary />

const Btn = ({ className, primary, ...props }) =>
  <button
    type="button"
    className={classnames(
      "btn",
      primary && "btn-primary",
      className
    )}
    {...props}
  />

更形象化的展示。

PrimaryBtn()
   Btn({primary: true})
     Button({className: "btn btn-primary"}, type: "button"})
       '<button type="button" class="btn btn-primary"></button>'

使用这些组件,所有这些都会产生相同的输出。

<PrimaryBtn />
<Btn primary />
<button type="button" className="btn btn-primary" />

这对样式的维护很有好处。它将样式的所有关注点分离到单个组件。

事件转换

在编写事件处理程序时,通常采用 handle{eventName} 命名约定。

handleClick(e) { /* do something */ }

对于处理多个事件类型的组件,这些函数名称可能会重复。名称本身可能不会提供太多的意义,因为它们只是代理其他的actions/functions。

handleClick() { require("./actions/doStuff")(/* action stuff */) }
handleMouseEnter() { this.setState({ hovered: true }) }
handleMouseLeave() { this.setState({ hovered: false }) }

考虑为你的组件编写一个单独的事件处理程序,并开启 event.type

handleEvent({type}) {
  switch(type) {
    case "click":
      return require("./actions/doStuff")(/* action dates */)
    case "mouseenter":
      return this.setState({ hovered: true })
    case "mouseleave":
      return this.setState({ hovered: false })
    default:
      return console.warn(`No case for event type "${type}"`)
  }
}

另外,对于简单的组件,可以使用箭头函数直接从组件中调用导入的actions/functions。

<div onClick={() => someImportedAction({ action: "DO_STUFF" })}

在遇到问题之前,不要担心性能优化问题。真的不要。

布局组件

布局组件会导致某种形式的静态DOM元素。如果有的话,它可能不需要经常更新。

考虑一个能够渲染两个并排子元素的组件。

<HorizontalSplit
  leftSide={<SomeSmartComponent />}
  rightSide={<AnotherSmartComponent />}
/>

我们可以积极地优化这个组件。

虽然 HorizontalSplit 将成为这两个组件的父组件,但它永远不会是他们的所有者。我们可以告诉它永远不更新,而不会中断组件的生命周期。

class HorizontalSplit extends React.Component {
  shouldComponentUpdate() {
    return false
  }

  render() {
    <FlexContainer>
      <div>{this.props.leftSide}</div>
      <div>{this.props.rightSide}</div>
    </FlexContainer>
  }
}

容器组件

“一个容器获取数据,然后渲染其相应的子组件,就是这样。”—Jason Bonta

给定这个可复用的CommentList组件。

const CommentList = ({ comments }) =>
  <ul>
    {comments.map(comment =>
      <li>{comment.body}-{comment.author}</li>
    )}
  </ul>

我们可以创建一个新的组件,负责获取数据并渲染无状态的CommentList组件。

class CommentListContainer extends React.Component {
  constructor() {
    super()
    this.state = { comments: [] }
  }

  componentDidMount() {
    $.ajax({
      url: "/my-comments.json",
      dataType: 'json',
      success: comments =>
        this.setState({comments: comments});
    })
  }

  render() {
    return <CommentList comments={this.state.comments} />
  }
}

我们可以为不同的应用上下文编写不同的容器。

高阶组件

高阶函数是一个函数,它接受和/或返回一个函数。这并不比这更复杂。那么,什么是高阶组件?

如果你已经在使用容器组件, 这些只是封装在函数中的通用容器。

让我们从我们的无状态 Greeting 组件开始吧。

const Greeting = ({ name }) => {
  if (!name) { return <div>Connecting...</div> }

  return <div>Hi {name}!</div>
}

如果它得到的 props.name,它会渲染该数据。否则,会显示"Connecting..."。然后是高阶部分。

const Connect = ComposedComponent =>
  class extends React.Component {
    constructor() {
      super()
      this.state = { name: "" }
    }

    componentDidMount() {
      // this would fetch or connect to a store
      this.setState({ name: "Michael" })
    }

    render() {
      return (
        <ComposedComponent
          {...this.props}
          name={this.state.name}
        />
      )
    }
  }

这只是一个返回组件的函数,该函数将渲染作为参数传递的组件。

最后一步,我们需要将我们的 Greeting 组件包装在 Connect 中。

const ConnectedMyComponent = Connect(Greeting)

这是为任何数量的无状态函数组件提供获取和绑定数据的强大模式。

状态提升

无状态函数不保存状态(顾名思义)。

事件是状态的变化。 数据要传给有状态的父容器组件

这就是所谓的"状态提升"。它是通过从容器组件传递一个回调到一个子组件来完成的。

class NameContainer extends React.Component {
  render() {
    return <Name onChange={newName => alert(newName)} />
  }
}

const Name = ({ onChange }) =>
  <input onChange={e => onChange(e.target.value)} />

Name接收来自NameContaineronChange回调并调用事件。 上面的alert是一个简洁的演示,但它不会改变状态。我们来改变NameContainer的内部状态。

class NameContainer extends React.Component {
  constructor() {
    super()
    this.state = {name: ""}
  }

  render() {
    return <Name onChange={newName => this.setState({name: newName})} />
  }
}

通过提供的回调,状态被提升到容器,用于更新本地状态。这设置了一个很好的清晰边界,并最大化了无状态函数的可复用性。

这种模式不限于无状态函数。因为无状态函数没有生命周期事件,所以你也可以在类组件中使用这个模式。

*受控的input是了解状态提升使用的重要模式。

(最好在有状态组件上处理事件对象)

受控的input

摘要中很难谈到受控的input。让我们从一个不受控制的(正常)输入开始,然后从那里开始。

<input type="text" />

当你在浏览器中捣鼓这个输入时,你会看到你的改变。这个是正常的。

受控输入不允许使这成为可能的DOM突变。你可以在组件区域中设置输入的值,并且在DOM区域中不会更改

<input type="text" value="This won't change. Try it." />

显然,静态输入对用户来说不是很有用。所以,我们从state获得value

class ControlledNameInput extends React.Component {
  constructor() {
    super()
    this.state = {name: ""}
  }

  render() {
    return <input type="text" value={this.state.name} />
  }
}

然后,改变输入是一个改变组件状态的问题。

    return (
      <input
        value={this.state.name}
        onChange={e => this.setState({ name: e.target.value })}
      />
    )

这是一个受控的输入。它只在组件状态发生变化时更新DOM。创建一致的用户界面时,这是非常宝贵的。

如果你正在使用无状态函数作表单元素,请阅读使用状态提升将新状态移动到组件树上。

About

Patterns for React Developers

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published