4

React 模式(中文版)

 3 years ago
source link: https://zhuanlan.zhihu.com/p/55546844
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

React 模式(中文版)

公众号「肆零玖陆」 — 以程序员的视角谈天说地

React 模式(中文版) on Github

React 模式(原版)on Github

函数组件 (Function component)

函数组件 是最简单的一种声明可复用组件的方法

他们就是一些简单的函数。

function Greeting() {
  return <div>Hi there!</div>;
}

从第一个形参中获取属性集 (props)

function Greeting(props) {
  return <div>Hi {props.name}!</div>;
}

按自己的需要可以在函数组件中定义任意变量

最后一定要返回你的 React 组件。

function Greeting(props) {
  let style = {
    fontWeight: "bold",
    color: context.color
  };

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

使用 defaultProps 为任意必有属性设置默认值

function Greeting(props) {
  return <div>Hi {props.name}!</div>;
}
Greeting.defaultProps = {
  name: "Guest"
};

属性解构 (Destructuring props)

解构赋值 是一种 JavaScript 特性。

出自 ES2015 版的 JavaScript 新规范。

所以看起来可能并不常见。

好比字面量赋值的反转形式。

let person = { name: "chantastic" };
let { name } = person;

同样适用于数组。

let things = ["one", "two"];
let [first, second] = things;

解构赋值被用在很多 函数组件 中。

下面声明的这些组件是相同的。

function Greeting(props) {
  return <div>Hi {props.name}!</div>;
}

function Greeting({ name }) {
  return <div>Hi {name}!</div>;
}

有一种语法可以在对象中收集剩余属性。

叫做 剩余参数,看起来就像这样。

function Greeting({ name, ...restProps }) {
  return <div>Hi {name}!</div>;
}

那三个点 (...) 会把所有的剩余属性分配给 restProps 对象

然而,你能使用 restProps 做些什么呢?

继续往下看...


JSX 中的属性展开 (JSX spread attributes)

属性展开是 JSX 中的一个的特性。

它是一种语法,专门用来把对象上的属性转换成 JSX 中的属性

参考上面的 属性解构,
我们可以 扩散 restProps 对象的所有属性到 div 元素上

function Greeting({ name, ...restProps }) {
  return <div {...restProps}>Hi {name}!</div>;
}

这让 Gretting 组件变得非常灵活。

我们可以通过传给 Gretting 组件 DOM 属性并确定这些属性一定会被传到 div

<Greeting name="Fancy pants" className="fancy-greeting" id="user-greeting" />

避免传递非 DOM 属性到组件上。 解构赋值是如此的受欢迎,是因为它可以分离 组件特定的属性DOM/平台特定属性

function Greeting({ name, ...platformProps }) {
  return <div {...platformProps}>Hi {name}!</div>;
}

合并解构属性和其它值 (Merge destructured props with other values)

组件就是一种抽象。

好的抽象是可以扩展的。

比如说下面这个组件使用 class 属性来给按钮添加样式。

function MyButton(props) {
  return <button className="btn" {...props} />;
}

一般情况下这样做就够了,除非我们需要扩展其它的样式类

<MyButton className="delete-btn">Delete...</MyButton>

在这个例子中把 btn 替换成 delete-btn

JSX 中的属性展开 对先后顺序是敏感的

扩散属性中的 className 会覆盖组件上的 className

我们可以改变它两的顺序,但是目前来说 className 只有 btn

function MyButton(props) {
  return <button {...props} className="btn" />;
}

我们需要使用解构赋值来合并入参 props 中的 className 和基础的(组件中的) className。 可以通过把所有的值放在一个数组里面,然后使用一个空格连接它们。

function MyButton({ className, ...props }) {
  let classNames = ["btn", className].join(" ");

  return <button className={classNames} {...props} />;
}

为了保证 undefined 不被显示在 className 上,可以使用 默认值

function MyButton({ className = "", ...props }) {
  let classNames = ["btn", className].join(" ");

  return <button className={classNames} {...props} />;
}

条件渲染 (Conditional rendering)

不可以在一个组件声明中使用 if/else 语句 You can't use if/else statements inside a component declarations.
所以可以使用 条件(三元)运算符短路计算

如果

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

除非

{
  condition || <span>Rendered when `falsy`</span>;
}

如果-否则

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

子元素类型 (Children types)

很多类型都可以做为 React 的子元素。

多数情况下会是 数组 或者 字符串

字符串 String

<div>Hello World!</div>

数组 Array

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

数组做为子元素 (Array as children)

将数组做为子元素是很常见的。

列表是如何在 React 中被绘制的。

我们使用 map() 方法创建一个新的 React 元素数组

<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>

函数做为子元素 (Function as children)

React 组件不支持函数类型的子元素。

然而 渲染属性 是一种可以创建组件并以函数作为子元素的模式。

渲染属性 (Render prop)

这里有个组件,使用了一个渲染回调函数 children。

这样写并没有什么用,但是可以做为入门的简单例子。

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

组件把 children 做为函数调用,同时还可以传一些参数。上面这个 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);
  }
}

许多开发人员都喜欢 高阶组件 来实现这种功能。但这只是个人喜好问题。

子组件的传递 (Children pass-through)

你可能会创建一个组件,这个组件会使用 context 并且渲染它的子元素。

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

  render() {
    // 如果能直接返回 `children` 就完美了
  }
}

你将面临一个选择。把 children 包在一个 div 中并返回,或者直接返回 children。第一种情况需要要你添加额外的标记(这可能会影响到你的样式)。第二种将产生一个没什么用处的错误。

// option 1: extra div
return <div>{children}</div>;

// option 2: unhelpful errors
return children;

最好把 children 做为一种不透明的数据类型对待。React 提供了 React.Children 方法来处理 children

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

代理组件 (Proxy component)

(我并不确定这个名字的准确叫法 译:代理、中介、装饰?)

按钮在 web 应用中随处可见。并且所有的按钮都需要一个 type="button" 的属性。

<button type="button">

重复的写这些属性很容易出错。我们可以写一个高层组件来代理 props 到底层组件。

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

我们可以使用 Button 组件代替 button 元素,并确保 type 属性始终是 button。

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

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

样式组件 (Style component)

这也是一种 代理组件,用来处理样式。

假如我们有一个按钮,它使用了「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" />

这对于样式维护来说是非常好的。它将样式的所有关注点分离到单个组件上。

组织事件 (Event switch)

当我们在写事件处理函数的时候,通常会使用 handle{事件名字} 的命名方式。

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

当需要添加很多事件处理函数的时候,这些函数名字会显得很重复。这些函数的名字并没有什么价值,因为它们只代理了一些动作或者函数。

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}"`)
  }
}

另外,对于简单的组件,你可以在组件中使用箭头函数直接调用导入的动作或者函数

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

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

布局组件 (Layout component)

布局组件表现为一些静态 DOM 元素的形式。它们一般并不需要经常更新。

就像下面的这个组件一样,两边各自渲染了一个 children。

<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>
  }
}

容器组件 (Container component)

「容器用来获取数据然后渲染到子组件上,仅仅如此。」—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} />
  }
}

对于不同的应用上下文,我们可以写不同的容器组件。

高阶组件 (Higher-order component)

高阶函数 是至少满足下列一个条件的函数:

  • 接受一个或多个函数作为输入
  • 输出一个函数

所以高阶组件又是什么呢?

如果你已经用过 容器组件, 这仅仅是一些泛化的组件, 包裹在一个函数中。

让我们以 Greeting 组件开始

const Greeting = ({ name }) => {
  if (!name) {
    return <div>连接中...</div>;
  }

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

如果 props.name 存在,组件会渲染这个值。否则将展示「连接中...」。现在来添加点高阶的感觉

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);

这是一个强大的模式,它可以用来获取数据和给定数据到任意 函数组件 中。

状态提升 (State hoisting)

函数组件 没有状态 (就像名字暗示的一样)。

事件是状态的变化。

它们的数据需要传递给状态化的父 容器组件

这就是所谓的「状态提升」。

它是通过将回调从容器组件传递给子组件来完成的

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

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

Name 组件从 NameContainer 组件中接收 onChange 回调,并在 input 值变化的时候调用。

上面的 alert 调用只是一个简单的演示,但它并没有改变状态

让我们来改变 NameContainer 组件的内部状态。

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

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

这个状态 被提升 到了容器中,通过添加回调函数,回调中可以更新本地状态。这就设置了一个很清晰边界,并且使功能组件的可重用性最大化。

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

受控输入 是一种与状态提升同时使用时很重要的模式

(最好是在一个状态化的组件上处理事件对象)

受控输入 (Controlled input)

讨论受控输入的抽象并不容易。让我们以一个不受控的(通常)输入开始。

<input type="text" />

当你在浏览器中调整此输入时,你会看到你的更改。 这个是正常的

受控的输入不允许 DOM 变更,这使得这个模式成为可能。通过在组件范围中设置值而不是直接在 DOM 范围中修改

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

显示静态的输入框值对于用户来说并没有什么用处。所以,我们从状态中传递一个值到 input 上。

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

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

然后当你改变组件的状态的时候 input 的值就自动改变了。

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

这是一个受控的输入框。它只会在我们的组件状态发生变化的时候更新 DOM。这在创建一致 UI 界面的时候非常有用。

如果你使用 函数组件 做为表单元素,那就得阅读 状态提升 一节,把状态转移到上层的组件树上。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK