React Render Props模式和React Hooks

197 阅读5分钟

Render Props模式

使用一个名为render的prop,render的值类型为函数通过这个函数向子组件传递值。

// 子组件依赖于父组件的某些数据时,需要将父组件的数据传到子组件,子组件拿到数据并渲染
<DataProvider render={data => (
  <Cat target={data.target} />
)}/>

function DataProvider( {render} ){
   var data = { target: 'cat'};
   return render(data);
}

也可以另外一种写法,函数作为父组件的children。

<DataProvider>
  {data => (
    <Cat target={data.target} />
  )}
</DataProvider>

function DataProvider( props ){
   var data = { target: 'cat'};
   return props.children(data);
}

render props模式用于跨组件共享数据逻辑。容器组件仅实现数据逻辑功能,把UI呈现委托给其他组件,容器组件把数据传递给UI组件。数据逻辑和UI呈现分离,能够实现数据逻辑组件和UI组件复用。

React Hooks

使用render props有一系列问题。但是,React没有找到一种不使用class方式去使用state和组件生命周期方式。所以我们必须使用一个容器组件实现render props值。 然而,让人激动的是,随着React Hooks的引入这一切都改变了。React Hooks让我们只用几行代码就可以在功能组件中使用状态和生命周期钩子。更好的是,我们可以实现自己的自定义hook,为跨组件共享逻辑提供了一个简单而强大的方式。因此我们不需要使用class类或一个render props模式在两个组件内共享代码。 React Hooks使用state和其他特性可以通过函数式编写组件,而不是必须写class组件。 下面看如何使用React Hooks的。

343010319-b34ad224-ebcd-497e-b391-11a188b1fdb0.png

如上图所示,点击edit按钮进入编辑状态,编辑展示内容。编辑完以后,进入展示信息状态。代码实现如下。

import React, { useState } from "react";

function EditableItem({ label, initialValue }) {
  const [value, setValue] = useState(initialValue);
  const [editorVisible, setEditorVisible] = useState(false);

  const toggleEditor = () => setEditorVisible(!editorVisible);

  return (
    <main>
      {editorVisible ? (
        <label>
          {label}
          <input
            type="text"
            value={value}
            onChange={event => setValue(event.target.value)}
          />
        </label>
      ) : (
        <span>{value}</span>
      )}
      <button onClick={toggleEditor}>{editorVisible ? "Done" : "Edit"}</button>
    </main>
  );
}

引入react库的useState钩子,在组件EditableItem中,定义了value state和 editorVisible state,并且对应的setValue和setEditorVisible方法。value是绑定在input上的内容和span上展示的内容,editorVisible用来判断是编辑状还是展示信息状态。

自定义Hooks

自定义钩子是一个能够使state逻辑复用的方法。在上面例子中,editorVisible state 是true和false的状态切换,这种状态的切换在UI中是常见的。如果我们想跨组件共享切换逻辑,我们可以定义个Toggler组件并且使用render props模式共享切换方法。但是,如果使用函数比使用组件更简单,我们可以使用自定义钩子。把切换逻辑从EditableItem组件抽离到一个独立的函数,即自定义钩子。通常推荐自定义钩子以use开头,这个自定义钩子命名为useToggle。自定义钩子useToggle代码如下:

import React, { useState } from "react";

function useToggle(initialValue) {
  const [toggleValue, setToggleValue] = useState(initialValue);
  const toggler = () => setToggleValue(!toggleValue);

  return [toggleValue, toggler];
}

从上面的代码中,我们做了以下事情。

  • 使用useSate钩子定义state和对应的更新state方法
  • 定义toggler函数,设置toggleValue的相反的值
  • 返回一个含有两个元素的数组,toggleValue值表示当前的state的值,toggler用于切换toggleValue值。

自定义钩子和其他钩子使用方式一样。在EditableItem组件中使用钩子很容易的。

import React, { useState } from "react";

import useToggle from 'useToggle.js';
function EditableItem({ label, initialValue }) {
  const [value, setValue] = useState(initialValue);
  const [editorVisible, toggleEditorVisible] = useToggle(false);

  return (
    <main>
      {editorVisible ? (
        <label>
          {label}
          <input
            type="text"
            value={value}
            onChange={event => setValue(event.target.value)}
          />
        </label>
      ) : (
        <span>{value}</span>
      )}
      <button onClick={toggleEditorVisible}>
        {editorVisible ? "Done" : "Edit"}
      </button>
    </main>
  );
}

现在我```javascript们render props模式和React hooks对比:

class Toggler extends Component {
  constructor(props) {
    super(props);
    this.state = {
      toggleValue: props.initialValue
    };
    this.toggler = this.toggler.bind(this);
  }

  toggler() {
    this.setState(prevState => ({
      toggleValue: !prevState.toggleValue
    }));
  }
  render() {
    return this.props.children(this.state.toggleValue, this.toggler);
  }
}

class EditableItem extends Component {
  constructor(props) {
    super(props);
    this.state = {
      value: props.initialValue
    };
  }

  setValue(newValue) {
    this.setState({
      value: newValue
    });
  }

  render() {
    return (
      <Toggler initialValue={false}>
        {(editorVisible, toggleEditorVisible) => (
          <main>
            {editorVisible ? (
              <label>
                {this.props.label}
                <input
                  type="text"
                  value={this.state.value}
                  onChange={event => this.setValue(event.target.value)}
                />
              </label>
            ) : (
              <span>{this.state.value}</span>
            )}
            <button onClick={toggleEditorVisible}>
              {editorVisible ? "Done" : "Edit"}
            </button>
          </main>
        )}
      </Toggler>
    );
  }
}

毫无疑问,使用自定义Hooks在组件之间重用代码更容易,并且需要更少的代码。

使用useContext共享数据

useContext能够跨组件共享数据。例子如下:

343103634-50365b94-8af8-4591-b331-4a798fe4422b.png

上面的例子展示使用context Hooks跨组件共享user信息,通过下拉框改变user信息。 在这个例子中,有UserProfile和ChangeProfile两个组件。UserProfile组件展示用户信息,ChangeProfile组件切换用户信息。实现如下:

import React, { createContext, useState, useContext } from "react";
const UserContext = createContext();

function UserProfile() {
  const { user } = useContext(UserContext);
  const emailLink = `mailto:${user.email}`;
  return (
    <section>
      <h3>{user.name}</h3>
      <a href={emailLink} title={emailLink}>
        {user.email}
      </a>
    </section>
  );
}

function ChangeProfile() {
  const profiles = [
    {
      name: "Aditya",
      email: "adityaa803@gmail.com",
    },
    {
      name: "Arnold",
      email: "arnold@terminator.machines",
    },
  ];
  const { user, setUser } = useContext(UserContext);
  const updateUser = (event) => {
    const profile = profiles[event.target.value];
    setUser(profile);
  };
  return (
    <select onChange={updateUser}>
      {profiles.map((profile, index) => (
        <option value={index} key={profile.email}>
          {profile.name}
        </option>
      ))}
    </select>
  );
}

function User({ children }) {
  const [user, setUser] = useState({
    name: "Aditya",
    email: "adityaa803@gmail.com",
  });
  const value = { user, setUser };
  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}

export default function App() {
  return (
    <div className="App">
      <User>
        <ChangeProfile />
        <UserProfile />
      </User>
    </div>
  );
}

在上面的代码中,做了以下事情:

  • 创建一个userContext,它将user信息传递给每个组件
  • userProfile组件展示用户信息
  • ChangeProfile组件为一个下拉列表,下拉选择user,使用setUser方法更新context
  • 创建User组件存储user信息,通过UserContext把user信息传递给孩子组件UserProfile和ChangeProfile组件。

实现组件插槽slots

另外一个常见的使用render props模式是实现组件插槽。

function Card({ title, body, action}) {
  return (
    <section className='card'>
      <nav className='header'>
        {title()}
      </nav>
      <main className='main'>
        {body()}
      </main>
      <footer className='footer'>
        {action()}
      </footer>
    </section>
  )
}

function App() {
  return (
    <Card
      title={() => (
        <h2>Card Title</h2>
      )}
      body={() => (
        <div>
          <p>
            Some Content
          </p>
          <a href="/link">Some Link</a>
        </div>
      )}
      action={() => (
        <button onClick={() => console.log('clicked')}>Some Action</button>
      )}
    />
  )
}

一个更简单的方法是不需要函数作为props,我们可以使用JSX写一个prop,如下面所示:

function Card({ title, body, action }) {
  return (
    <section className="card">
      <nav className="header">{title}</nav>
      <main className="main">{body}</main>
      <footer className="footer">{action}</footer>
    </section>
  );
}

function App() {
  return (
    <Card
      title={<h2>Card Title</h2>}
      body={
        <div>
          <p>Some Content</p>
          <a href="/link">Some Link</a>
        </div>
      }
      action={
        <button onClick={() => console.log("clicked")}>Some Action</button>
      }
    />
  );

使用render props模式在这里是使用错误的,因为render props意向在组件间共享数据。所以,在这种场景,我们应该避免使用render props。

总结

在我看来,render props模式并不打算为上面用例所使用的,但是React团队没有其他选择。React团队注意到了这一点并且寻找新的技术方式React Hooks。React Hooks和render props模式可以共存,因为它们解决了将状态从组件移走的相同问题。

参考文献:

  1. React render props vs. custom Hooks
  2. Render Props Pattern
  3. React中的代码复用(Render-Props、HOC)