【翻译】React Hooks 是什么

原文链接What are React Hooks?
文章很长,我尽量采取意译,一些不重要的内容没有翻译。文章中涉及到这个作者的很多其他文章,都非常值得一看而且通俗易懂,建议大家可以直接看作者的英文博客。这篇翻译只是我自己一个学习记录,请勿转载。

React Conf October 2018 上,React团队介绍了 React Hooks,它是解决在React 函数组件中使用状态和副作用的一种方法。尽管在此之前,我们把函数组件称为无状态的组件functional stateless components,但是在React Hooks中,它们已经能够使用状态。因此,现在人们更多地把它们称为函数组件。

在这篇文章中,我将会解释引入 hooks 的动机,这给 React 带来的变化,以及为什么我们不必对新的变化感到担忧。最后,我还会通过一些通用的React Hooks 使用案例,比如状态和副作用 hooks 来展示 React Hooks 如何在函数组件中使用。


为什么使用 React Hooks ?

React Hooks 是 React 团队构建的,用于在函数组件中引入状态和副作用。这是更加容易地在项目中完全使用函数组件的方法,不必为了引入副作用或者使用本地状态,而将函数组件重构为 React 类组件以便使用生命周期钩子函数。

不必要的组件重构:在这之前,只有 React 类函数能用于本地状态管理或者使用生命周期钩子函数。在 React 类组件中,生命周期钩子对于引入副作用,例如事件订阅或者数据拉取是至关重要的。

import React from 'react';
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button
          onClick={() =>
            this.setState({ count: this.state.count + 1 })
          }
        >
          Click me
        </button>
      </div>
    );
  }
}
export default Counter;

只有你不需要使用state或者生命周期方法的时候,我们才能够使用函数式无状态组件。因为 React 函数组件更加轻便和优雅,我们已经使用过大量函数组件。随之而来的缺点是,我们必须在每次需要使用状态和生命周期方法的时候将函数组件重构为类组件。

import React, { useState } from 'react';
// how to use the state hook in a React function component
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
export default Counter;

使用 Hooks 我们就不需要考虑重构了。使用了 Hooks,副作用和状态的都可以在React 函数中使用。这也是为什么我们要将函数无状态组件重命名为函数组件。

副作用逻辑:在React类组件中,一些生命周期方法经常会引入一些副作用(比如,componentDidMount, componentDidUpdate, componentWillUnmount)。一个副作用能够从 React 中拉取数据(How to fetch data in React)或者和浏览器API进行交互(Intersection Observer API in React)。通常来说,我们引入副作用之后需要有一个启动或者清除的阶段。例如,如果你忘记移除监听器,可能会导致 React 发生一些性能问题(Prevent React setState on unmounted Component)。

// side-effects in a React class component
class MyComponent extends Component {
  // setup phase
  componentDidMount() {
    // add listener for feature 1
    // add listener for feature 2
  }
  // clean up phase
  componentWillUnmount() {
    // remove listener for feature 1
    // remove listener for feature 2
  }
  ...
}
// side-effects in React function component with React Hooks
function MyComponent() {
  useEffect(() => {
    // add listener for feature 1 (setup)
    // return function to remove listener for feature 1 (clean up)
  });
  useEffect(() => {
    // add listener for feature 2 (setup)
    // return function to remove listener for feature 2 (clean up)
  });
  ...
}

现在,如果你想要在 React 类组件的生命周期中引入一个或者更多的副作用,所有的副作用是根据生命周期方法分组的而不是根据副作用。这是 React Hooks 将要去改变的,也就是通过将一个副作用封装在一个 hook 中而且每个 hook 包含一个副作用的启动和清除阶段。在稍后,你可以在这篇教程中看到我们通过添加和移除监听器在一个 React Hook 中是可行的。

React的抽象地狱:在 React 中的高阶组件(A gentle Introduction to React’s Higher Order Components)和 Render Prop (React’s Render Props Pattern)组件中,都引入抽象和随之而来的复用性。还有React的上下文及其提供者组件和消费者组件(React’s Context API explained: Provider and Consumer),它们引入了另一个抽象级别。React中的所有这些高级模式都使用了所谓的包装组件。以下组件的实现对正在创建大型react应用程序的开发人员不应该是陌生的。

import { compose } from 'recompose';
import { withRouter } from 'react-router-dom';
function App({ history, state, dispatch }) {
  return (
    <ThemeContext.Consumer>
      {theme =>
        <Content theme={theme}>
          ...
        </Content>
      }
    </ThemeContext.Consumer>
  );
}
export default compose(
  withRouter,
  withReducer(reducer, initialState)
)(App);

Sophie Alpert 把这称为在 React 中的“包装地狱”。你不单会在实现中看到这种情况,当你在浏览器中检查你的组件时同样也会看到。由于使用 Render Prop 组件(包括 React 上下文中的消费者组件)和高阶组件,我们产生了大量的包装组件。由于所有的抽象逻辑都被掩盖在其他 React 组件中,组件树变得复杂难以读取。我们很难从浏览器的 DOM 树中追踪到真实的可见的组件。那么,要是其他多余的组件不再需要,我们仅将将这些逻辑作为副作用封装进函数中呢?结果是,你可以移除所有这些包装组件和拍平你的组件树结构。

function App() {
  const theme = useTheme();
  const history = useRouter();
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <Content theme={theme}>
      ...
    </Content>
  );
}
export default App;

这也是为什么 React Hooks 开始公开讨论的原因。所有的副作用直接贮存在组件中而不是将其他组件作为容器以提供这种逻辑。Andrew Clark 也已经在他的那个非常受欢迎的高阶组件库 recompose 中留下一个他支持 React Hooks 的声明。

JavaScript类的困惑:JavaScript 能够很好的融合这两个试接:面向对象和函数式编程。React 为所有开发者提供了这两种世界。一方面,React(和 Redux ) 为我们介绍了函数组合式的函数式编程,通用的带函数的编程概念(例如,高阶函数,JavaScript 内置方法,如 map, reduce, filter)还有其他术语,比如 immutability 和 side-effects. React 本身并没有介绍这些东西,因为它们本身是语言特性或者编程范式,但是它们确确实实频繁地在 React 中被使用,因此每个 React 开发者不知不觉地变成一个更好的 JavaScript 开发者(JavaScript fundamentals before learning React)。

另一方面,React 使用 JavaScript 类作为唯一的方法去定义 React 组件。类只是声明,而组件的实际用法是实例化它。它创建一个类实例,而类实例的这个对象用于与类方法(例如 setstate、forceupdate 或者其他类方法)进行交互。然而,对于那些没有 OOP 学习背景的初学者来说,课程的学习曲线更为陡峭。这也是类绑定、this 对象和继承很难理解的原因。在我的 React book 中,我有几章只关注 React 的这一方面内容,对于初学者来说,React 总是最令人困惑的。

// I THOUGHT WE ARE USING A CLASS. WHY IS IT EXTENDING FROM SOMETHING?
class Counter extends Component {
  // WAIT ... THIS WORKS???
  state = { value: 0 };
  // I THOUGH IT'S THIS WAY, BUT WHY DO I NEED PROPS HERE?
  // constructor(props) {
  //  SUPER???
  //  super(props);
  //
  //  this.state = {
  //    value: 0,
  //  };
  // }
  // WHY DO I HAVE TO USE AN ARROW FUNCTION???
  onIncrement = () => {
    this.setState(state => ({
      value: state.value + 1
    }));
  };
  // SHOULDN'T IT BE this.onDecrement = this.onDecrement.bind(this); in the constructor???
  // WHAT'S this.onDecrement = this.onDecrement.bind(this); DOING ANYWAY?
  onDecrement = () => {
    this.setState(state => ({
      value: state.value - 1
    }));
  };
  render() {
    return (
      <div>
        {this.state.value}
        {/* WHY IS EVERYTHING AVAILABLE ON "THIS"??? */}
        <button onClick={this.onIncrement}>+</button>
        <button onClick={this.onDecrement}>-</button>
      </div>
    )
  }
}

现在,很多人认为 React 不应该把 JavaScript 类剔除,因为人们不理解它们。毕竟,它们属于语言。然而,引入 Hooks API 的假设之一是,当初学者编写不带 JavaScript 类的 React 组件时,对于 React 初学者来说是一个更平滑的学习曲线。

由于 REACT HOOKS 带来了什么变化?

每次引入一个新特性,人们总是会非常担忧。团队中的一部分人对于即将而来的变化感到非常兴奋,另一部分人则害怕改变。我经常听到的关于 React Hooks 的担忧有:

  • 一切都变了!感到一丝恐慌…
  • React 像 Angular 一样臃肿!
  • 这毫无作用,类同样也能很好的实现
  • 这是魔法!

REACT USESTATE HOOK

你在之前的典型 Counter 例子的代码段中已经看到 useState Hook 的使用。它是用于在函数组件中管理本地状态。让我们在一个更加复杂的例子中使用 hook,也就是我们将管理一个数组的项目。在我的另一篇文章中,你可以学习到如何在 React 中管理数组状态,但这次我们使用 React hooks 来实现。那么,开始学习吧:

import React, { useState } from 'react';
const INITIAL_LIST = [
  {
    id: '0',
    title: 'React with RxJS for State Management Tutorial',
    url:
      'https://www.robinwieruch.de/react-rxjs-state-management-tutorial/',
  },
  {
    id: '1',
    title: 'A complete React with Apollo and GraphQL Tutorial',
    url: 'https://www.robinwieruch.de/react-graphql-apollo-tutorial',
  },
];
function App() {
  const [list, setList] = useState(INITIAL_LIST);
  return (
    <ul>
      {list.map(item => (
        <li key={item.id}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}
export default App;

useState hook 使用数组的解析构接受初始状态作为参数然后返回,两个参数的变量名可以随意命名。第一个变量是真实的状态,第二个变量是用于更新状态的函数。

这个场景的目标是为了从数组中移除项目。为了实现这一目标,渲染数组中的每个项目都有一个携带点击处理事件的按钮。这个点击事件处理函数可以在函数组件中声明,它能够在之后利用 list 和 setList。你不需要传递这些变量给事件处理函数,因为它们已经在组件外部声明。

function App() {
  const [list, setList] = useState(INITIAL_LIST);
  function onRemoveItem() {
    // remove item from "list"
    // set the new list in state with "setList"
  }
  return (
    <ul>
      {list.map(item => (
        <li key={item.id}>
          <a href={item.url}>{item.title}</a>
          <button type="button" onClick={onRemoveItem}>
            Remove
          </button>
        </li>
      ))}
    </ul>
  );
}

然而,我们需要去知道那个项目是要从数据中移除的。使用高阶函数,我们可以传递项目的一个 id 标识给事件处理含糊是。否则,我们无法识别哪个项目是要被移除的。

function App() {
  const [list, setList] = useState(INITIAL_LIST);
  function onRemoveItem(id) {
    // remove item from "list"
    // set the new list in state with "setList"
  }
  return (
    <ul>
      {list.map(item => (
        <li key={item.id}>
          <a href={item.url}>{item.title}</a>
          <button type="button" onClick={() => onRemoveItem(item.id)}>
            Remove
          </button>
        </li>
      ))}
    </ul>
  );
}

最后,利用数组的内置方法根据id标识去过滤数组。它会返回一个新的数组并重新设置数组的状态。

function App() {
  const [list, setList] = useState(INITIAL_LIST);
  function onRemoveItem(id) {
    const newList = list.filter(item => item.id !== id);
    setList(newList);
  }
  return (
    <ul>
      {list.map(item => (
        <li key={item.id}>
          <a href={item.url}>{item.title}</a>
          <button type="button" onClick={() => onRemoveItem(item.id)}>
            Remove
          </button>
        </li>
      ))}
    </ul>
  );
}

useState hook 提供了所有我们需要的用于在函数组件中管理状态的东西:初始化状态,最新状态和状态更新函数。其他一切都是 JavaScript 。此外,您不需要像在类组件中那样为状态对象的浅合并而烦恼。相反,您用 useState 封装一个域(例如 list ),但是如果您需要另一个状态(例如 counter ),那么只需用另一个 useState 封装这个域。您可以阅读 React 文档中有关 useState hook的更多信息(Using the State Hook)。

REACT USEEFFECT HOOK

让我们继续学习下一个 hook,叫做 useEffect。正如前面所提到的,函数组件应该使用 hooks 来管理状态和副作用。管理状态在之前已经在 useState hook 中展示了。现在,轮到 useEffect hook 起作用了,它可以用来处理和 Browser/DOM API 的交互或者其他 API,比如数据拉取。让我们通过实现一个简单秒表的例子,来看下 useEffect hook 是如何与 Browser API 交互的。

import React, { useState } from 'react';
function App() {
  const [isOn, setIsOn] = useState(false);
  return (
    <div>
      {!isOn && (
        <button type="button" onClick={() => setIsOn(true)}>
          Start
        </button>
      )}
      {isOn && (
        <button type="button" onClick={() => setIsOn(false)}>
          Stop
        </button>
      )}
    </div>
  );
}
export default App;

目前这还不是一个秒表。但至少这里已经实现了一个条件判断用于选择渲染 “Start” 或者 “Stop” 按钮。这个状态的布尔值是由 useState hook 管理的。

让我们利用 useEffect 来引入一个用于记录间隔的副作用。用于记录间隔的函数每秒向浏览器的开发人员工具发出一个控制台日志记录。

import React, { useState, useEffect } from 'react';
function App() {
  const [isOn, setIsOn] = useState(false);
  useEffect(() => {
    setInterval(() => console.log('tick'), 1000);
  });
  return (
    <div>
      {!isOn && (
        <button type="button" onClick={() => setIsOn(true)}>
          Start
        </button>
      )}
      {isOn && (
        <button type="button" onClick={() => setIsOn(false)}>
          Stop
        </button>
      )}
    </div>
  );
}
export default App;

为了在组件卸载后移除间隔定时器(但这也发生在每次渲染更新时),你可以在 useEffect 中返回一个函数用于清除。例如,当组件不再存在时内存中应该不会发生内存泄露问题。

import React, { useState, useEffect } from 'react';
function App() {
  const [isOn, setIsOn] = useState(false);
  useEffect(() => {
    const interval = setInterval(() => console.log('tick'), 1000);
    return () => clearInterval(interval);
  });
  ...
}
export default App;

现在,你想在组件挂载时启动副作用,组件卸载时清除副作用。如果你打印出 effect 中函数调用了多少次,你会发现在每次组件状态改变的时候会新设置一个定时器(比如,点击 “Start”/“Stop” 按钮)。

import React, { useState, useEffect } from 'react';
function App() {
  const [isOn, setIsOn] = useState(false);
  useEffect(() => {
    console.log('effect runs');
    const interval = setInterval(() => console.log('tick'), 1000);
    return () => clearInterval(interval);
  });
  ...
}
export default App;

为了只在挂载和卸载时运行 effect,你可以传入一个空数组作为第二个参数。

import React, { useState, useEffect } from 'react';
function App() {
  const [isOn, setIsOn] = useState(false);
  useEffect(() => {
    const interval = setInterval(() => console.log('tick'), 1000);
    return () => clearInterval(interval);
  }, []);
  ...
}
export default App;

然而,既然定时器会在每次渲染的时候被清除,我们需要在更新的时候重新设置一个定时器。只有数组中的任意一个变量改变的时候,effect 才会在更新循环中运行。如果,你保持一个空数组,effect 只会在挂载和卸载的时候运行,因为再次运行副作用的时候没有变量可以被检查。

import React, { useState, useEffect } from 'react';
function App() {
  const [isOn, setIsOn] = useState(false);
  useEffect(() => {
    const interval = setInterval(() => console.log('tick'), 1000);
    return () => clearInterval(interval);
  }, [isOn]);
  ...
}
export default App;

不管 isOn 是 true 或者 false, 定时器都会运行。当秒表激活的时候运行会更好。

import React, { useState, useEffect } from 'react';
function App() {
  const [isOn, setIsOn] = useState(false);
  useEffect(() => {
    let interval;
    if (isOn) {
      interval = setInterval(() => console.log('tick'), 1000);
    }
    return () => clearInterval(interval);
  }, [isOn]);
  ...
}
export default App;

现在,我们为了显示秒表计数引入另一个状态。它用于当秒表激活的时候,更新计数。

import React, { useState, useEffect } from 'react';
function App() {
  const [isOn, setIsOn] = useState(false);
  const [timer, setTimer] = useState(0);
  useEffect(() => {
    let interval;
    if (isOn) {
      interval = setInterval(
        () => setTimer(timer + 1),
        1000,
      );
    }
    return () => clearInterval(interval);
  }, [isOn]);
  return (
    <div>
      {timer}
      {!isOn && (
        <button type="button" onClick={() => setIsOn(true)}>
          Start
        </button>
      )}
      {isOn && (
        <button type="button" onClick={() => setIsOn(false)}>
          Stop
        </button>
      )}
    </div>
  );
}
export default App;

代码目前还有一个错误。当定时器运行的时候,它会在每秒通过加 1 更新时间,然而,它依赖于之前的 state 。只有当 isOn 值改变的时候是可行的。为了在定时器运行的时候接收到最新的 state ,你可以使用函数来更新状态,以获得最新状态。

import React, { useState, useEffect } from 'react';
function App() {
  const [isOn, setIsOn] = useState(false);
  const [timer, setTimer] = useState(0);
  useEffect(() => {
    let interval;
    if (isOn) {
      interval = setInterval(
        () => setTimer(timer => timer + 1),
        1000,
      );
    }
    return () => clearInterval(interval);
  }, [isOn]);
  ...
}
export default App;

另一个方法是当 timer 更新的时候运行 effect,那么 effect 会接收到最新的 timer 状态。

import React, { useState, useEffect } from 'react';
function App() {
  const [isOn, setIsOn] = useState(false);
  const [timer, setTimer] = useState(0);
  useEffect(() => {
    let interval;
    if (isOn) {
      interval = setInterval(
        () => setTimer(timer + 1),
        1000,
      );
    }
    return () => clearInterval(interval);
  }, [isOn, timer]);
  ...
}
export default App;

这是使用浏览器 API 实现秒表的例子,如果你想继续,可以扩展这里例子通过添加一个 “Reset” 按钮。

import React, { useState, useEffect } from 'react';
function App() {
  const [isOn, setIsOn] = useState(false);
  const [timer, setTimer] = useState(0);
  useEffect(() => {
    let interval;
    if (isOn) {
      interval = setInterval(
        () => setTimer(timer => timer + 1),
        1000,
      );
    }
    return () => clearInterval(interval);
  }, [isOn]);
  const onReset = () => {
    setIsOn(false);
    setTimer(0);
  };
  return (
    <div>
      {timer}
      {!isOn && (
        <button type="button" onClick={() => setIsOn(true)}>
          Start
        </button>
      )}
      {isOn && (
        <button type="button" onClick={() => setIsOn(false)}>
          Stop
        </button>
      )}
      <button type="button" disabled={timer === 0} onClick={onReset}>
        Reset
      </button>
    </div>
  );
}
export default App;

useEffect hook 被用于在 React 函数组件中使用副作用,这对于和 Browser/DOM API 或者其他第三方 API (比如数据拉取)是非常有用的。你可以在官方文档中阅读到更多内容(Using the Effect Hook)。

REACT CUSTOM HOOKS

最后但并非最不重要的,在你了解了在函数组件中引入状态和副作用的两个最流行的钩子之后,最后还有一件事我想向您展示:自定义 hooks。没错,您可以实现自己的自定义 React Hooks,这些 hooks 可以在您的应用程序或其他人中重用。让我们看看他们如何使用一个示例应用程序,该应用程序能够检测您的设备是在线的还是离线的。

import React, { useState } from 'react';
function App() {
  const [isOffline, setIsOffline] = useState(false);
  if (isOffline) {
    return <div>Sorry, you are offline ...</div>;
  }
  return <div>You are online!</div>;
}
export default App;

再次介绍副作用的 useEffect hook 。在这种情况下,effect 会添加和删除用于检查设备是否在线或者离线的监听器。两个侦听器在挂载时只设置一次,在卸载时只清除一次(第二个参数为空数组)。每当调用其中一个侦听器时,它都为isOffline布尔值设置状态。

import React, { useState, useEffect } from 'react';
function App() {
  const [isOffline, setIsOffline] = useState(false);
  function onOffline() {
    setIsOffline(true);
  }
  function onOnline() {
    setIsOffline(false);
  }
  useEffect(() => {
    window.addEventListener('offline', onOffline);
    window.addEventListener('online', onOnline);
    return () => {
      window.removeEventListener('offline', onOffline);
      window.removeEventListener('online', onOnline);
    };
  }, []);
  if (isOffline) {
    return <div>Sorry, you are offline ...</div>;
  }
  return <div>You are online!</div>;
}
export default App;

现在一切都很好地封装在一个 effect 中。这是一个很好的功能,应该在其他地方复用。这就是为什么我们可以将功能提取为它的自定义 hook 的原因,该 hook 遵循与其他 hook 相同的命名约定。

import React, { useState, useEffect } from 'react';
function useOffline() {
  const [isOffline, setIsOffline] = useState(false);
  function onOffline() {
    setIsOffline(true);
  }
  function onOnline() {
    setIsOffline(false);
  }
  useEffect(() => {
    window.addEventListener('offline', onOffline);
    window.addEventListener('online', onOnline);
    return () => {
      window.removeEventListener('offline', onOffline);
      window.removeEventListener('online', onOnline);
    };
  }, []);
  return isOffline;
}
function App() {
  const isOffline = useOffline();
  if (isOffline) {
    return <div>Sorry, you are offline ...</div>;
  }
  return <div>You are online!</div>;
}
export default App;

提取 custom hook 作为函数并不是唯一的事情。您还必须从 custom hook 返回isOffline状态,以便在应用程序中使用它向离线用户显示消息。否则,它将呈现正常的应用程序。这是用于检测您是在线还是离线的 custom hook 。您可以在React的文档中了解更多关于 custom hook 的信息(Building Your Own Hooks)。

如果你想了解更多,你可以查看以下教程:


 本篇
【翻译】React Hooks 是什么 【翻译】React Hooks 是什么
原文链接What are React Hooks?文章很长,我尽量采取意译,一些不重要的内容没有翻译。文章中涉及到这个作者的很多其他文章,都非常值得一看而且通俗易懂,建议大家可以直接看作者的英文博客。这篇翻译只是我自己一个学习记录,请勿转
2019-08-18
下一篇 
CSS动画技巧及性能 CSS动画技巧及性能
接着前文看《CSS权威指南》,本文整理一些CSS动画的知识点,同时给出一些例子。 预备知识变形 Transform在CSS动画中我们经常需要通过变形来达到一些奇妙的效果。使用 transform 首先要明确变形是基于笛卡尔坐标系,也就是通常
2019-07-09
  目录