快速了解React-Hooks

Presentational and Container Components

react团队成员之一的Dan Abramov在medium上写过一篇文章 Presentational and Container Components,他在文中将组件分为两类,分别是Presentational ContainerPresentational是展示类组件,比如说Page,Sidebar,Story,ListContainer组件是功能类组件,比如UserPage, FollowersSidebar, StoryContainer, FollowedUserList

它们是React组件的两种设计模式,和组件本身是class component还是function component关系不大。

Hooks 出现的动机

便于复用组件中的逻辑

组件中逻辑的复用,解决render props和hoc所做的事,抽离它们中的逻辑。hoc组件太多会导致标签结构混乱复杂,大多数情况下一个Container Component外面会包裹很多Presentational Component,通过Hooks,你可以从组件中抽离状态逻辑,让它们变得可以单独测试和重复使用,Hooks可以让你复用状态逻辑,而不用去变更组件的层级关系,这使得与其他组件共享Hooks变得容易,甚至可以编写社区共用的Hooks。

组件太复杂而变得难以理解

组件的生命周期中经常会混入很多不相关的逻辑,比如在componentDidMountcomponentDidUpdate中会进行数据的请求,但是在componentDidMount中我们可能还会进行一些事件的监听,在componentWillUnmount中会清理这些事件,这就使得不相关逻辑的代码混在一起,而逻辑相关的代码会被拆分在两个方法里面,这会使得维护变得困难,大多数的组件不能再次拆分成更小的组件,因为逻辑遍布整个文件。很多人会选择引入一个单独的状态管理工具,但是这又将会引入一大部分抽象,并且将在各个文件中切换。

JavaScript中的class使得学习React变得困难

为了使用class取编写组件需要编写很多无意义的代码,class组件中需要去考虑this问题,在React的开发人员中,关于class组件和function组件的讨论也存在着巨大的分歧。而且对于Prepack这种提前优化工具,class变得不那么容易去优化,React团队希望能够提供一种方式,让这些逻辑代码变得可以优化。React团队建议在设计组件的时候能够向function组件靠拢,Hooks提供了能够在function组件中去编写生命周期时的逻辑,而不需要去理解复杂的响应原理。

目前React团队没有将class组件移除的计划

React Hooks简单使用

React Hooks是React提交的一系列函数,他们提供在function组件中hook state和生命周期的特性,它们不能在class组件中被使用。

State Hook

例子:

import { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useState就是一个React Hooks,这个方法接收一个值,并返回一个数组,通过JavaScript的Array destructuring特性,可以将返回的值赋给变量。 useState会返回两个值,第一个是state,第二个是更新该state的方法,类似setState。 一个function组件中可以使用多次useState

function ExampleWithManyStates() {
  // Declare multiple state variables!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  // ...
}

Effect Hook

例子:

import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

React团队提出了一种叫“side effects”的操作,比如fetch data、subscriptions、或者手动去变更DOM,因为它们会影响其他组件,并且在渲染中无法进行。useEffect hook类似于类组件中componentDidMountcomponentDidUpdatecomponentWillUnmount的统一。上面的例子中将会在Effect中去改变title的值。 在使用useEffect时,只需要告诉React在更新DOM之后需要进行的“effect”,通常运行这些“effect”会在渲染之后(包括第一次渲染)。 useEffect可以通过返回一个函数来进行清理操作,这些操作会在组件unmounts时运行,或者因为后续渲染重新运行“effect”之前。下面的例子在effect中subscribe好友的状态,并且通过返回一个函数unsubscribe状态。

import { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);

    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

useState一样,在function组件中也可以多次使用useEffect

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }
  // ...

这样可以很方便的根据逻辑代码去组织逻辑,而不是通过生命周期去组织代码。

Hooks的使用规则

Hooks是JavaScript函数,但是在使用它们时需要强制遵循两条规则规则

  • 调用Hooks必须在顶层函数,不能在循环、条件和嵌套函数中调用
  • 调用Hooks必须在function组件中,不能在传统的JavaScript方法中调用(也可以在自己定义的Hooks中调用) React团队提供了相关的eslint插件,可以在eslint中检查上面的规则。linter plugin

自定义Hook

有时我们想在不同的组件中执行相同的逻辑,比如上面的subscribe好友状态,下面的例子中抽离了相关的逻辑,定义了一个useFriendStatus的方法

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

它接收一个friendID作为参数,并且返回好友在线的状态,现在可以在两个组件中使用它

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

这些组件的状态是完全独立的,钩子是重用有状态逻辑的一种方式,而不是状态本身。事实上,每次调用Hook都有一个完全隔离的状态,所以你甚至可以在一个组件中使用相同的自定义Hook两次。 如果一个函数需要以use开头,在React中回叫它自定义Hooks,在lint工具中,这会是一个检查的条件。

其他Hook

有一些Hook不经常会用到,但是很有用,比如useContext,它可以让你subscribe React context

function Example() {
  const locale = useContext(LocaleContext);
  const theme = useContext(ThemeContext);
  // ...
}

useReducer可以让你在复杂的组件中使用Reducer的方式管理state

function Todos() {
  const [todos, dispatch] = useReducer(todosReducer);
  // ...

更多的Hook可以阅读React提供的API文档 Hooks API Reference

引用

  1. Introducing Hooks