【译】什么是React Hooks?

3,546 阅读17分钟

原文链接

在2018年10月的React Conf上引入了React Hooks,作为在React函数组件中使用state和副作用的一种方式。尽管功能组件以前被称为函数无状态组件(FSC),但它们最终能够与React Hooks一起使用状态。因此,许多人现在将它们称为函数组件。

在本演练中,我想解释钩子函数背后的动机,React会发生什么变化,本教程只是对React Hooks的介绍。在本教程的最后,您将找到更多的教程来深入了解React Hooks。 ##为什么要使用React Hooks? React Hooks是由React团队发明的,旨在在函数组件中引入状态管理和副作用。这是他们的一种方法,它无需使用生命周期方法将React函数组件重构为React类组件,而是以使用副作用或state的方式,使得React函数组件变得更加轻松,React Hooks让我们能够仅使用函数组件来编写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;

仅当您不需要状态或生命周期方法时,才可以使用React无状态组件。而且由于React函数组件更轻巧(和美观),人们已经使用了很多函数组件。这样做的缺点是每次需要状态或生命周期方法时都会将组件从React函数组件重构为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时,不需要进行此重构。副作用和状态最终可以在React函数组件中获得。这就是将函数无状态组件重命名为函数组件的合理原因。

副作用逻辑:在React类组件中,副作用大多是在生命周期方法中引入的(例如componentDidMount,componentDidUpdate,componentWillUnmount)。副作用可能是在React中获取数据与Browser API交互。通常,这些副作用来自设置和清理阶段。例如,如果您想删除您的监听器,可能会遇到React性能问题

// 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通过useEffect钩子函数将副作用封装在其中,而每个hook在处理和清理阶段都有其自己的副作用。您将在本教程的后面部分中看到如何通过在React Hook中添加和删除监听器来实现此功能。

React的抽象地狱:React中的高阶组件render props组件体现了抽象以及可复用性。还有React的上下文Context及其Provider和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上下文中的Consumer组件)和High-Order组件,包装的组件有数十个。它变成了一个不可读的组件树,因为所有抽象的逻辑都被其他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提出的建议。所有副作用都直接存在于组件中,而没有引入其他组件作为业务逻辑的容器。容器消失了,逻辑仅存在于函数的React Hooks中。

JavaScript类混乱: JavaScript很好地融合了两个概念:面向对象编程(OOP)和函数式编程。React向开发人员很好地展示了这两个概念。一方面,React(和Redux)向人们介绍了由函数组成的函数式编程(FP),还有其他函数的通用编程概念(例如,高阶函数,JavaScript内置方法(如map,reduce,filter))以及其他术语,例如作为不变性(immutability )和副作用( side-effects)。React本身并没有真正引入这些东西,因为它们是语言或编程范例本身的功能,但是它们在React中大量使用,使得每个React开发人员都潜移默化地成为更好的JavaScript开发人员

另一方面,React使用JavaScript类作为定义React组件的一种方法。类仅是声明,而组件实际上是类的实例化。它创建一个类实例,而该类实例的this对象用于与类方法进行交互(例如setState,forceUpdate,其他自定义类方法)。但是,对于没有面向对象编程思想(OOP)的React初学者,课程的学习曲线更为陡峭。这就是为什么类绑定,this对象和类继承会造成混淆。

现在,许多人都认为React不应该取消JavaScript类,这是人们不理解React Hooks。但是,引入Hooks API的假设之一是,对于React初学者来说,当他们一开始就编写没有JavaScript类的React组件时,学习曲线就更加平滑。


React Hooks:给React带来了什么变化?

每次引入新功能时,人们都会对此加以关注。创新派会因此感到兴奋,而部分人则对变革感到恐惧。我听人们最关心React Hooks的问题是:

  • 一切都变了!细思极恐。。。
  • React变得像Angular一样臃肿!
  • 这没用,用Class组件也可以正常工作。

让我来回答以上问题:

一切都变了:React Hooks将来会改变我们编写React应用程序的方式。但是,现在还是什么都没有改变。您仍然可以使用局部状态和生命周期方法编写类组件,还有其他例如高阶组件或render props组件。React团队确保React保持向后兼容。与React 16.7相同。

React变得像Angular一样臃肿:React一直被视为具有轻量API的库。没错,将来也是如此。但是,为了适应几年前基于组件构建应用程序的做法,并且不被其他库所取代,React引入了一些更改,以支持较旧的API。如果React有全新的东西,应该只有函数组件和React Hooks。但是React是在几年前发布的,需要进行调整以跟上现状或改变现状。也许几年后将会弃用React类组件和生命周期方法,以支持React函数组件和React Hooks,但是目前,React团队将React类组件保留在其工具库中。毕竟,React团队利用hooks作为一项发明希望可以长期使用。显然,React Hooks为React添加了另一个API,但是有利于将来简化React的API。我喜欢这种过渡,而不是推出一个与之前React截然不同的React2。

这没用,用Class组件也可以正常工作:假设您从零开始学习React,然后直接给您介绍React Hooks。也许创建React应用程序不会从React类组件开始,而是从React函数组件开始。您需要学习的所有组件都是React Hooks。它们管理状态和副作用,因此您只需要了解state和effect hooks。对于React初学者来说,无需使用JavaScript类(继承,this,绑定,super,...)带来的所有其他开销,这些都是React类组件之前的做法。通过React Hooks学习React将会更加简单。想象一下React Hooks是一种如何编写React组件的新方法-这是一种新思维。我参加过很多次React研讨会,并且我是一个多疑的人,但是当我用React Hooks编写了一些简单的场景,我就确信这是编写和学习React的最简单方法。

最后,以这种方式进行思考:基于组件的解决方案(例如Angular,Vue和React)在每个发行版中都在推动Web开发的边界。它们建立在二十多年前发明的技术之上,并且不断进行调整,使得Web开发在2018年变得不费吹灰之力。他们疯狂地对其进行了优化,以满足当代的需求。我们正在使用组件而不是HTML模板来构建Web应用程序。我可以想未来象我们将坐在一起,为浏览器发明基于组件的标准。Angular,Vue和React只是这一运动的先行者。

React UseState Hooks

你已经在代码中看到了一个典型的计数器示例的useState Hook。它用于管理功能组件中的本地状态。让我们在更复杂的场景中使用该钩子函数,在该示例中,我们将管理列表项:

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: '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钩子函数接收一个初始状态作为参数,并通过使用数组解构来返回两个可命名的变量。第一个变量是state的值,而第二个变量是则是更新状态的函数。

此示例的目标是从列表中删除一个项。为了实现它,呈现列表中的每个项都有一个带有点击事件的按钮。可以在函数组件中内联一个onRemoveItem函数,它稍后将使用list和setList。不需要将这些变量传递给函数,因为它们已在组件的外部范围中可用。

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); //删除目标item
    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>
  );
}

你可以根据传递给函数的id从列表中删除列表项。然后,用filter函数过滤列表,并使用setList函数设置列表的新的state。

useState钩子函数为你提供了管理功能组件中的状态所需的一切:初始状态,最新状态和状态更新函数。其他一切都是JavaScript。此外,你不必像以前在类组件中那样为状态对象的浅层合并而烦恼。相反,你可以使用useState来封装一个域(例如,列表),但是如果你需要另一个状态(例如,计数器),则只需使用另一个useState来封装该域。

React UseEffect Hooks

让我们转到下一个名为useEffect的钩子函数。如上所述,功能组件应该能够通过钩子管理状态和副作用。通过useState挂钩展示了管理状态。现在出现了useEffect钩子,用于产生副作用,这些副作用通常用于与Browser / DOM API或外部API(例如数据提取)进行交互。让我们看看如何通过实现一个简单的秒表将useEffect钩子函数与浏览器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;

目前还没有秒表。但是至少有一个条件渲染显示“开始”或“停止”按钮。按钮的状态由useState挂钩管理。

让我们用useEffect介绍我们的副作用,该副作用记录了一个间隔。它每秒发出一个console.log,在控制台打印出来。

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;

现在,你要在挂载组件时设置副作用,并在卸下组件时清理副作用。如果你记录useEffect钩子中的函数被调用的次数,则会看到每次组件状态改变时,它都会设置一个新的interval(例如,单击“开始” /“停止”按钮)。

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;

为了仅在组件的挂载和卸载时执行副作用,您可以向其传递一个空数组作为第二个参数。

import React, { useState, useEffect } from 'react';
function App() {
  const [isOn, setIsOn] = useState(false);
  useEffect(() => {
    const interval = setInterval(() => console.log('tick'), 1000);
    return () => clearInterval(interval);
  }, []); // 这样useEffect只会在挂载和卸载阶段执行
  ...
}
export default App;

但是,由于每次卸载时清空了定时器,因此我们也需要在更新周期中setInterval。但是我们可以告诉效果useEffect仅在isOn变量更改时才运行。仅当数组中的变量之一更改时,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);
  }, [isOn]); // useEffect会在isOn变化时来执行
  ...
}
export default App;

无论isOn是true还是false,setInterval都会运行。但我们只希望在秒表激活的时候才运行:

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), // 秒表被激活时,计时器就+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;

代码中仍然存在一个错误。当setInterval运行时,它将每秒增加1,从而更新计时器。但是,它始终依赖计时器的失效状态。仅当inOn改变时,状态才时正常的。为了在间隔运行时始终接收计时器的最新状态,可以对始终具有最新状态的状态更新功能使用功能。

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;

另一种选择是在计时器更改时也运行效果。然后效果将接收最新的计时器状态。

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的秒表的实现。如果要继续,您也可以通过提供“重置”按钮来扩展示例。

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钩子函数用于React函数组件中的副作用,该函数用于与浏览器/ DOM API或其他第三方API进行交互(例如,数据获取)。

Rect自定义钩子函数

最后,当您了解了在功能组件中引入状态和副作用的两个最流行的钩子函数之后,我要向您展示的最后一件事:自定义钩子。没错,您可以实现自己的自定义React Hook,可以在您的应用程序或其他应用程序中复用。让我们看看它们如何与示例应用程序一起使用,该应用程序能够检测您的设备是在线还是离线。

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钩子以产生副作用。在这种情况下,该效果会添加和删除监听器,用于检查设备是在线还是离线。两个侦听器仅在安装时设置一次,并在卸载时清除一次(空数组作为第二个参数)。每当调用其中一个侦听器时,它都会为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;

现在一切都很好地封装在一个useEffect中,也可以在其他地方复用。这就是为什么我们可以将这个功能提取为其自定义钩子函数,该钩子遵循与其他钩子相同的命名约定。

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;

将定制钩子提取为函数并不是唯一的事情。您还必须根据isOffline从自定义钩子函数返回状态,以便在您的应用程序中使用它来向离线用户显示消息。这是用于检测您在线还是离线的自定义钩子。您可以在React的文档中阅读有关自定义钩子的更多信息。

React Hooks的可复用性非常高,因为有可能发展出一个可以从npm安装到任何React应用程序的自定义React Hooks生态系统。