React-Hooks(附demo)

文章首发于3月29日。html

hooks诞生的缘由

react的今天和明天系列文章中,react开发人员介绍了class组件存在的三个问题。react

  • 逻辑复用
  • 庞大的组件
  • 难以理解的class

在class组件中经过HOC和render props中来实现组件的逻辑复用。这会带来一个问题,当咱们拆分出不少细小的组件再将它们组合到一块儿,若是在chrome打开react的扩展,会发现组件层级很是深,增长了react的计算负担。这个问题称为“包裹地狱(wrapper hell)”。git

当咱们试图解决第一个问题的时候,咱们会将更多的逻辑放在单个的组件中,致使组件日益庞大。这就是第二个问题。github

js中的class其实是function的语法糖。在使用过程当中,它隐藏了function的实现细节(static, prototype等)。而且当咱们写一个函数组件的时候,若是要添加状态,那么就必须将它转换为class组件,这会写不少样板代码。不只如此,对于机器来讲,压缩后的class的组件中的全部的方法名都是没有通过压缩的。而且也没法tree shaking。这是class组件的第三个问题。算法

Dan Abramov认为这不是三个独立的问题,而是一个问题的三个部分。为了解决这个问题,因而出现了hooks。chrome

hooks简介

hooks是另外一种书写组件的方式,在hooks中只有函数而没有class。经过hooks提供的一系列API几乎能够彻底覆盖class组件中的状况(为何说是几乎?在下面差别部分会提到)。hooks是更加简洁优雅的,逻辑分离的。json

  • v16.8。hooks是react16.8版本新添加的功能。若是要使用hooks,须要确保react版本升级到16.8.0及以上,同时保证react-dom和react的版本保持一致。redux

  • 百分之百向后兼容数组

  • react没有计划移除class组件。hooks是一种可选的写法,你依然能够选择不用而使用class(用过了你会喜欢上它)。浏览器

  • hooks中使用的依旧是class中的概念。

hooks API

hooks有下列API。全部的示例demo都在这里。codesandbox.io/s/o968n1q62… ,能够打开并直接看到效果。

Basic Hooks

  • useState
  • useEffect
  • useContext

Additional Hooks

  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeHandle
  • useLayoutEffect
  • useDebugValue

接下来会解释这些API并附有详细的demo。

1.useState

useState是hooks中最基础的一个API。其使用方式相似于class中的this.setState,不过又有所不一样。下面是demo中的示例。

function Counter() {
  const [count, setCount] = useState(4);
  return (
    <>
      <p>
        count: {count}, random: {Math.random()}
      </p>
      <button onClick={() => setCount(count < 5 ? count + 1 : count)}>
        点击这里加1
      </button>
      <button onClick={() => setCount(count - 1)}>点击这里减1</button>
    </>
  );
}
复制代码

useState(4)用于建立一个state变量,并传入初始值。返回值是一个数组,经过解构写法拿到返回的值。count是一个可变的值(只能经过setCount改变),它的初始值是useState传入的参数4。单击按钮的时候,经过setCount去改变它的值。从而从新渲染该组件。

它与class组件中的setState的差别有如下几点:

  • 不会进行状态的合并,而是进行状态的替换。这在简单类型没有什么问题,对于复杂类型如对象使用的时候须要注意(能够在demo中渲染Count2组件)。
  • 当使用setCount改变状态时,使用Object.is算法比较状态的先后值。它和 === 的区别在于对于0和-0的判断,以及NaN的判断。
  • 若是先后状态相同,那么组件不会进行渲染。在class组件中使用setState,即便某个状态先后是相同的值,仍会进行渲染(demo中点击按钮让count的值改变到5能够看到)。

另外,若是useState的参数值须要复杂的计算才能获得,那么能够传入给useState一个函数,它仅在首次渲染时执行该复杂的计算函数。useState(() => expensiveCalc())

2.useEffect/useLayoutEffect

因为useEffect和useLayoutEffect具备相同的使用方式,就放到一块儿来介绍。 正如其名,useEffect用来处理反作用。反作用包括DOM的改变、订阅、定时器、日志等。反作用不容许放到函数体中。

useEffect/useLayoutEffect能够取代componentDidMount、componentDidUpdate、componentWillUnmount三个声明周期。因此它是很是强大一个hook,在使用方面很灵活,也不是那么容易理解。

useEffect接受两个参数,其中第二个参数是可选的,不过通常状况下都须要传入第二个参数。

useEffect(() => {
  // some side effect
  // ...

  // return a clean up function
  return () => {

  }
}, [])
复制代码

第一个参数是一个函数,当组件首次渲染或者其依赖的状态改变时它会执行。该函数的返回值是可选的,能够不写,若是要写的话,必须是一个函数,用于清除上一个状态。

第二个参数是可选的,它是一个数组。数组中能够传入状态值(经过useState产生的值),当状态值改变的时候首先会执行return函数,用于清理上一个状态,而后useEffect中的函数就会再次执行。

  • 若是不传入第二个参数,表明组件中任何状态的改变该effect都会执行一次,这一般不是咱们想要的行为。
  • 若是第二个参数传递一个空数组,表明该effect仅会执行一次,至关于componentDidMount。return函数也只会在组件卸载的时候执行一次,至关于componentWillUnmount。
  • 若是第二个参数数组中有一个或多个状态(demo中的useLayEffect),那么只要有任意一个状态值发生变化,该effect都会再次执行。至关于componentDidUpdate。
// demo--useLayoutEffect
useEffect(() => {
  if (value.length > 10) {
    setValue(value.substring(0, 10));
  }
  setLengths(value.length);
},[value]);
复制代码

当value发生变化的时候,effect再次执行。改变length状态。

useLayoutEffect的语法和useEffect同样。不一样点在于:

  • useEffect是在组件状态改变后,而且在组件layout和paint以后,也就是说组件出如今页面后再进行调用。useLayoutEffect是在组件状态改变后,可是在组件layout和paint以前,也就是在组件出如今页面以前进行调用。
  • useEffect是异步的,useLayoutEffect是同步的。能够看这篇文章:juejin.im/post/5c8f43…

在useLayoutEffect的demo中,尝试将useEffect改变成useLayoutEffect,而后在输入框输入第十一个字符,能够看到明显差异。在大多数状况下你应该使用useEffect。由于useEffect是异步的,不会堵塞主线程渲染。

3.useRef

在16.3版本中,引入React.createRef,来代替字符串ref。一样,在hooks中,useRef也能够取代createRef。能够查看demo中的useRef。

function xxx() {
  const inputRef = useRef(null);
  useEffect(() => {
    inputRef.current.value = 'hello';
  }, [])
  return (
    <input type="text" ref={inputRef} />
  )
}
复制代码

useRef接受一个初始值,返回一个可变的ref对象,ref.current指向初始化的值。它能够指向别的值。

另外,因为是函数组件,this再也不指向这个组件,因此若是要达到class组件中实例变量的效果,也能够经过useRef来实现。

const timerRef = useRef(null);

useEffect(() => {
  timerRef.current = setInterval(() => {
    inputRef.current.value = 'hello';
  }, 1000);
  return () => {
    clearInterval(timerRef.current);
  };
}, []);
复制代码

这里的timerRef.current至关于class中的实例变量。

4.useContext

在React中,若是要将上层的属性传递到下层,通常来讲须要一层一层的传递。好比A->B-C->D。Context是一种数据传递机制,用于跨层级传递数据。好比在D组件能够直接使用A组件的数据。能够查看demo中的useContext。

useContext是Context.Consumer(16.3)以及static contextType(16.6)的一种简写。

上层组件定义Context.Provider,并传入value属性。在子组件中经过useContext(Context)能够获取value属性。

// parent.js
const parentContext = createContext();
function Parent(props) {
  const countArr = useState({
    count1: 0,
    count2: 1
  });
  return (
    <parentContext.Provider value={countArr}>
      {props.children}
    </parentContext.Provider>
  );
}

function Child() {
  const countArr = useContext(parentContext);
  const [countObj, setCountObj] = countArr;

  return (
    <>
      <div>
        count1: {countObj.count1} count2: {countObj.count2}
      </div>
    </>
  );
}

<Parent><Child /></Parent>
复制代码

能够看到,这里传递属性不是经过props的,而是经过Context的。须要注意的是,在父组件定义了parentContext,须要将其导出,由于子组件使用useContext的参数就是parentContext。

另外,由于没有static的限制,同一个组件中可使用多个useContext。也就是说,可使用多个上层组件传递来的数据。

5.useReducer

提到reducer,首先想到的应该是redux中的reducer。useReducer这个hook与redux中的reducer有所类似又有所不一样。能够查看demo中的useReducer。

定义一个reducer的方式和redux中是同样的:

const initialState = {
  count: 0
};
const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'ADD': {
      return {
        count: state.count + 1
      };
    }
    case 'MINUS': {
      return {
        count: state.count - 1
      };
    }
  }
};


function Counter1() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      <div>count: {state.count}</div>
      <button onClick={() => { dispatch({ type: 'ADD' }); }}>+</button>
      <button onClick={() => { dispatch({ type: 'MINUS' }); }}>-</button>
    </>
  );
}
复制代码

useReducer接受连个参数,一个是reducer,一个是initialState。返回一组值,分别是state和dispatch。经过dispatch触发一个动做,进而去更新状态。若是你使用过redux,那么理解起来没有任何困难。

另外,与redux中的reducer有所不一样的是,useReducer中的reducer是独立的。若是有多个组件使用到了同一个reducer,那么它们之间的状态是独立的。相较于redux的全局共享状态,它还依赖于react-redux提供的Provider组件。

因此是否是忽然想到了第四点中的Context,它提供了Provider。若是能配合useReducer,就能够实现全局状态共享了?确实如此!具体的代码能够查看demo中的context-reducer。

还有一点必须须要提到的是,因为性能缘由,react-redux没有推出官方的useRedux。 具体缘由能够看这篇文章:juejin.im/post/5c7c8d… 。在最近的7.0的beta版本的发布说明中,react-redux团队宣布将在7.x版本中推出useRedux API。

6.useImperativeHandle

在class组件中,若是父组件须要改变子组件的状态,有两种方式。一种是就是经过改变父组件state,该state做为props传给子组件,从而改变子组件的状态。另外一种就是经过操做子组件的ref了。传递ref的方式主要有两种,createRef和forwardRef,具体的就再也不细说。useImperativeHandle这个hook就是ref的另外一种写法。之前是在父组件中拿到子组件元素的ref,直接操做ref表明的元素节点,至关因而直接操做子元素的dom元素。如今经过这个hook能够在子组件中暴露一些API供父组件调用,而父组件是不能直接操做子组件的dom元素的。迪米特法则就是这样描述的:一个类对它所调用的类的细节知道的越少越好。具体代码能够查看demo中的useImperativeHandle。

function Child(props) {
  const inputRef = useRef(null);
  useImperativeHandle(props.myref, () => ({
    focus() {
      inputRef.current.focus();
    },
    setValue(value) {
      inputRef.current.value = value;
    }
  }));
  return (
    <>
      <input type="text" ref={inputRef} />
    </>
  );
}
复制代码

不过在实际开发中,你应该尽量经过传递props来改变子组件,经过ref来改变子组件是一种不推荐的方案。

7.useCallback

useCallback用来缓存一个函数。在函数式组件中可能有这样一种状况,父组件调用子组件,并将一个函数传递给子组件,假设子组件是使用了memo的(若是属性值没有变化,那么将不会从新渲染)。具体代码能够查看demo中的useCallback。

function Parent() {
  //...
  const handleChange = () => { // ... }
  return (
    <>
      <p>count: {count}</p>
      <Child onChange={handleChange}/>
    </>
  )
}
复制代码

当父组件中的count状态改变时,这时候父组件从新渲染,子组件尽管没有任何改变,但因为onChange这个属性是一个新的函数,它仍是从新渲染了。这是咱们不指望的行为。在class组件中咱们每每经过onChange={this.handleChange}将函数传递给子组件。那么下次父组件渲染时,this.handleChange是没有变化的。若是子组件是memo的,那么子组件将不会从新渲染。

因此uesCallback就是为了解决这样一个问题,它缓存一个函数,并接受一系列依赖项,返回一个函数。若是依赖项没有变化,那么返回的函数不会变化。

function Parent() {
  const [count, setCount] = useState(0);
  const [count2, setCount2] = useState(0);
  const [result, setResult] = useState(count);
  useEffect(() => {
    setInterval(() => {
      // setCount(prevCount => prevCount + 1);
      setCount2(prevCount => prevCount + 1);
    }, 1000);
  }, []);

  const handleChange = useCallback(() => {
    setResult(count + 1);
  }, [count]);
  // const handleChange = () => { setResult(count + 1) };

  return (
    <>
      <Counter count={count} />
      <Counter count={count2} />
      <Child onChange={handleChange} />
      <p>result: {result}</p>
    </>
  );
}
复制代码

这里的handleChange函数是被缓存了的,除非count发生变化,它才会发生变化。经过demo打开控制台,发现随着count2的改变,子组件是不会从新渲染的。

8.useMemo

useMemo用来缓存一个复杂的计算值。 useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。若是经过一个输入获得一个值须要通过复杂的计算,那么下次一样的输入再进行一遍一样复杂的计算是没有必要的。这正是useMemo存在的意义。具体代码能够查看demo中的useMemo。

function Parent() {
  const [count, setCount] = useState(10);
  const [count2, setCount2] = useState(10);

  console.time('calc');
  const result = useMemo(() => computeExpensiveValue(40), [count]);
  // const result = computeExpensiveValue(count);
  console.timeEnd('calc');

  return (
    <>
      <p>result: {result}</p>
      <div>
        <input type="number" disabled value={count} />
        <button onClick={() => setCount(count + 1)}>+</button>
        <button onClick={() => setCount(count - 1)}>-</button>
        <br />
        <input type="number" disabled value={count2} />
        <button onClick={() => setCount2(count2 + 1)}>+</button>
        <button onClick={() => setCount2(count2 - 1)}>-</button>
      </div>
    </>
  );
}
复制代码

useMemo接受一个函数,该函数涉及到复杂的计算,并接受一系列依赖项,返回一个计算后的值。若是依赖项没有变化,那么返回的值不会变化。这里useMemo依赖于count的变化。在页面中分别尝试改变count和count2的值,观察控制台输出的时间。改变count的时候,函数进行了重进计算,打印出的值比较大。改变count2的时候,直接使用缓存的值,打印出的值很小。

须要注意的是,react文档中说明,useMemo只是做为一种暗示,当依赖值变化时,并不必定能保证每一次都不计算。

9.useDebugValue

一般来讲你不须要它。它只会存在于自定义的hooks中用来标志一个自定义的hooks。当在chrome中打开react扩展的时候,若是一个组件使用到了自定义的hooks,而且该hooks使用到了useDebugValue,那么该组件下方会显示useDebugValue传入的参数。

function useUserInfo() {
  // ...

  useDebugValue('use-user-info');
  return userInfo;
}
复制代码

自定义hooks

除了官方提供的hooks之外,咱们也能够定义本身的hooks。编写自定义的hooks是很是简单的。你能够像和编写一个正常的组件同样,区别在于它返回的是数据(或者不返回),而不是jsx。使用自定义hooks须要遵循两点。demo中编写了一个自定义的hooks(/components/custom-hooks/use-user-info.js)。

  1. 自定义hooks以use开头,驼峰命名。
  2. 自定义的hooks只能被其余hooks或者函数组件使用。
const useUserInfo = (username = 'yuwanlin') => {
  const fetchRef = useRef(null);
  const [userInfo, setUserInfo] = useState({});
  const handleData = data => {
    setUserInfo(data);
  };
  useEffect(() => {
    const fetchData = username =>
      fetch(`${prefix}${username}`)
        .then(res => res.json())
        .then(data => {
          console.log('fetch success');
          handleData(data);
        });
    fetchRef.current = debounce(fetchData, 1000);
  }, []);

  useEffect(
    () => {
      fetchRef.current(username);
    },
    [username]
  );
  // useDebugValue('use-user-info');
  return userInfo;
};
复制代码

打开use-user-info demo能够看到页面中出现了用户的信息。

如何测试hooks

enzyme目前尚不支持测试hooksreact官方推出了测试hooks的方案。打开demo,在右边选项卡中。从Browser切换到Tests,能够看到经过了测试。全部位于__test__文件夹下的.test.js结尾的文件都会被当成测试文件。

hooks缺乏的部分

hooks目前不支持getSnapshotBeforeUpdate和componentDidCatch/getDerivedStateFromError生命周期,之后会加上。

一些常见的问题

因为每一个函数就是一个组件,那么整个函数体就至关于class组件中的render函数。每次状态改变,都要从新执行函数。下面是一些常见的问题:

  • 如何保存上一个状态?

在class组件中咱们经过实例变量来保存上一个状态。在函数组件中,能够经过ref来取代实例变量。usePrevious的实现以下:

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}
复制代码
  • 如何实现shouldComponentUpdate?

经过React.memo。

  • 在渲染的时候,因为须要建立函数,hooks是否更缓慢?

不,在现代浏览器中,与类相比,闭包的原始性能没有显著差别,除了在极端状况下。 此外,考虑到Hooks的设计在如下几个方面更有效:

  1. 钩子避免了类所需的大量开销,例如在构造函数中建立类实例和绑定事件处理程序的成本。
  2. 使用Hooks的惯用代码不须要深层组件树嵌套,这在使用高阶组件,渲染道具和上下文的代码库中很常见。 使用较小的组件树,React的工做量较少。

更多常见的问题,能够参照:reactjs.org/docs/hooks-…

hooks须要遵循的规范

  • hooks只能出如今函数做用域的顶级,不能出如今条件语句、循环语句中、嵌套函数中。
  • 只能从react的函数式组件以及自定义hooks中使用hooks。

代码中使用hooks的时候,最好配合eslint插件eslint-plugin-react-hooks。

// Your ESLint configuration
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
    "react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
  }
}
复制代码

总结

class组件存在三个问题,逻辑复用、组件庞大、难以理解的class。hooks的存在就是为了解决这三个问题的。而且在一个函数组件中,你能够屡次使用相同的hooks。好比:

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

  useEffect(() => { // ... }, []);
  useEffect(() => { // ... }, []);

  return (
    // ...
  )
}
复制代码

将不一样的逻辑放到不一样的hooks,逻辑更加清晰。hooks是class的另外一种写法,在react16.8引入,react并不打算放弃class。

相关文章
相关标签/搜索