React Hooks入门: 基本

媒介

  起首迎接人人关注我的Github博客,也算是对我的一点勉励,毕竟写东西没法取得变现,能坚持下去也是靠的是本身的热忱和人人的勉励,愿望人人多多关注呀!React 16.8中新增了Hooks特征,而且在React官方文档中新增加了Hooks模块引见新特征,可见React对Hooks的注重水平,假如你还不清晰Hooks是什么,强烈发起你相识一下,毕竟这可以真的是React将来的发展方向。
  

劈头

  React一直以来有两种建立组件的体式格局: Function Components(函数组件)与Class Components(类组件)。函数组件只是一个平常的JavaScript函数,接收props对象并返回React Element。在我看来,函数组件更相符React的头脑,数据驱动视图,不含有任何的副作用和状况。在应用递次中,平常只要异常基础的组件才会运用函数组件,而且你会发明跟着营业的增进和变化,组件内部可以必需要包括状况和其他副作用,因而你不得不将之前的函数组件改写为类组件。但事变每每并没有这么简朴,类组件也没有我们设想的那末优美,除了徒增事情量以外,还存在其他各种的题目。

  起首类组件共用状况逻辑异常贫苦。比方我们借用官方文档中的一个场景,FriendStatus组件用来显现朋侪列表中该用户是不是在线。

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  
  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

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

  上面FriendStatus组件会在建立时主动定阅用户状况,并在卸载时会退订状况防备形成内存泄漏。假定又涌现了一个组件也须要去定阅用户在线状况,假如想用复用该逻辑,我们平常会运用render props和高阶组件来完成状况逻辑的复用。

// 采纳render props的体式格局复用状况逻辑
class OnlineStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    const {isOnline } = this.state;
    return this.props.children({isOnline})
  }
}

class FriendStatus extends React.Component{
  render(){
    return (
      <OnlineStatus friend={this.props.friend}>
        {
          ({isOnline}) => {
            if (isOnline === null) {
              return 'Loading...';
            }
            return isOnline ? 'Online' : 'Offline';
          }
        }
      </OnlineStatus>
    );
  }
}
// 采纳高阶组件的体式格局复用状况逻辑
function withSubscription(WrappedComponent) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = { isOnline: null };
      this.handleStatusChange = this.handleStatusChange.bind(this);
    }

    componentDidMount() {
      ChatAPI.subscribeToFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
      );
    }

    componentWillUnmount() {
      ChatAPI.unsubscribeFromFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
      );
    }

    handleStatusChange(status) {
      this.setState({
        isOnline: status.isOnline
      });
    }

    render() {
      return <WrappedComponent isOnline={this.state.isOnline}/>
    }
  }
}

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

  上面两种复用状况逻辑的体式格局不仅须要费时辛苦地重构组件,而且Devtools检察组件的条理构造时,会发明组件层级构造变深,当复用的状况逻辑过多时,也会堕入组件嵌套地狱(wrapper hell)的状况。可见上述两种体式格局并不能圆满处置惩罚状况逻辑复用的题目。

  不仅云云,跟着类组件中营业逻辑逐渐庞杂,保护难度也会逐渐提拔,由于状况逻辑会被分割到差别的性命周期函数中,比方定阅状况逻辑位于componentDidMount,作废定阅逻辑位于componentWillUnmount中,相干逻辑的代码互相分裂,而逻辑不相干的代码反而有可以集合在一同,团体都是不利于保护的。而且比拟方函数式组件,类组件进修越发庞杂,你须要时候防范this在组件中的圈套,永久不能忘了为事宜处置惩罚递次绑定this。云云各种,看来函数组件照样有特有的上风的。

Hooks

  函数式组件一直以来都缺少类组件诸如状况、性命周期等各种特征,而Hooks的涌现就是让函数式组件具有类组件的特征。官方定义:

Hooks are functions that let you “hook into” React state and lifecycle features from function components.

  要让函数组件具有类组件的特征,起首就要完成状况state的逻辑。

State: useState useReducer

  useState就是React供应最基础、最经常使用的Hook,重要用来定义当地状况,我们以一个最简朴的计数器为例:

import React, { useState } from 'react'

function Example() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <span>{count}</span>
      <button onClick={()=> setCount(count + 1)}>+</button>
      <button onClick={() => setCount((count) => count - 1)}>-</button>
    </div>
  );
}

  useState可以用来定义一个状况,与state差别的是,状况不仅仅可以是对象,而且可以是基础范例值,比方上面的Number范例的变量。useState返回的是一个数组,第一个是当前状况的现实值,第二个用于变动该状况的函数,相似于setState。更新函数与setState雷同的是都可以接收值和函数两种范例的参数,与useState差别的是,更新函数会将状况替代(replace)而不是兼并(merge)。

  函数组件中假如存在多个状况,既可以经由过程一个useState声明对象范例的状况,也可以经由过程useState屡次声明状况。

// 声明对象范例的状况
const [count, setCount] = useState({
    count1: 0,
    count2: 0
});

// 屡次声明
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);

  比拟于声明对象范例的状况,显著屡次声明状况的体式格局越发轻易,重要是由于更新函数是采纳的替代的体式格局,因而你必需给参数中增加未变化的属性,异常的贫苦。须要注重的是,React是经由过程Hook挪用的序次来纪录各个内部状况的,因而Hook不能在前提语句(如if)或许轮回语句中挪用,并在须要注重的是,我们仅可以在函数组件中挪用Hook,不能在组件和平常函数中(除自定义Hook)挪用Hook。

  当我们要在函数组件中处置惩罚庞杂多层数据逻辑时,运用useState就最先力不从心,值得光荣的是,React为我们供应了useReducer来处置惩罚函数组件中庞杂状况逻辑。假如你运用过Redux,那末useReducer可谓是异常的亲热,让我们用useReducer重写之前的计数器例子:

import React, { useReducer } from 'react'

const reducer = function (state, action) {
  switch (action.type) {
    case "increment":
      return { count : state.count + 1};
    case "decrement":
      return { count: state.count - 1};
    default:
      return { count: state.count }
  }
}

function Example() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  const {count} = state;
  return (
    <div>
      <span>{count}</span>
      <button onClick={()=> dispatch({ type: "increment"})}>+</button>
      <button onClick={() => dispatch({ type: "decrement"})}>-</button>
    </div>
  );
}

  useReducer接收两个参数: reducer函数和默许值,并返回当前状况state和dispatch函数的数组,其逻辑与Redux基础一致。useReducer和Redux的辨别在于默许值,Redux的默许值是经由过程给reducer函数赋值默许参数的体式格局给定,比方:

// Redux的默许值逻辑
const reducer = function (state = { count: 0 }, action) {
  switch (action.type) {
    case "increment":
      return { count : state.count + 1};
    case "decrement":
      return { count: state.count - 1};
    default:
      return { count: state.count }
  }
}

  useReducer之所以没有采纳Redux的逻辑是由于React以为state的默许值多是来自于函数组件的props,比方:

function Example({initialState = 0}) {
  const [state, dispatch] = useReducer(reducer, { count: initialState });
  // 省略...
}

  如许就可以完成经由过程通报props来决议state的默许值,固然React虽然不引荐Redux的默许值体式格局,但也许可你相似Redux的体式格局去赋值默许值。这就要打仗useReducer的第三个参数: initialization。

  望文生义,第三个参数initialization是用来初始化状况,当useReducer初始化状况时,会将第二个参数initialState通报initialization函数,initialState函数返回的值就是state的初始状况,这也就许可在reducer外笼统出一个函数特地担任盘算state的初始状况。比方:

const initialization = (initialState) => ({ count: initialState })

function Example({initialState = 0}) {
  const [state, dispatch] = useReducer(reducer, initialState, initialization);
  // 省略...
}

  所以借助于initialization函数,我们就可以够模仿Redux的初始值体式格局:

import React, { useReducer } from 'react'

const reducer = function (state = {count: 0}, action) {
  // 省略...
}

function Example({initialState = 0}) {
  const [state, dispatch] = useReducer(reducer, undefined, reducer());
  // 省略...
}

Side Effects: useEffect useLayoutEffect

  处置惩罚了函数组件中内部状况的定义,接下来亟待处置惩罚的函数组件中性命周期函数的题目。在函数式头脑的React中,性命周期函数是沟通函数式和敕令式的桥梁,你可以在性命周期中实行相干的副作用(Side Effects),比方: 要求数据、操纵DOM等。React供应了useEffect来处置惩罚副作用。比方:

import React, { useState, useEffect } from 'react';

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

  useEffect(() => {
    document.title = `You clicked ${count} times`
    return () => {
      console.log('clean up!')
    }
  });

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

  在上面的例子中我们给useEffect传入了一个函数,并在函数内根据count值更新网页题目。我们会发明每次组件更新时,useEffect中的回调函数都邑被挪用。因而我们可以以为useEffect是componentDidMount和componentDidUpdate结合体。当组件装置(Mounted)和更新(Updated)时,回调函数都邑被挪用。视察上面的例中,回调函数返回了一个函数,这个函数就是特地用来消灭副作用,我们晓得相似监听事宜的副作用在组件卸载时应当实时被消灭,否则会形成内存泄漏。消灭函数会在每次组件从新衬着前挪用,因而实行递次是:

render -> effect callback -> re-render -> clean callback -> effect callback

  因而我们可以运用useEffect模仿componentDidMount、componentDidUpdate、componentWillUnmount行动。之前我们提到过,恰是由于性命周期函数,我们必不得已将相干的代码拆分到差别的性命周期函数,反而将不相干的代码安排在同一个性命周期函数,之所以会涌现这个状况,重要题目在于我们并非根据于营业逻辑誊写代码,而是经由过程实行时间编码。为相识决这个题目,我们可以经由过程建立多个Hook,将相干逻辑代码安排在同一个Hook来处置惩罚上述题目:

import React, { useState, useEffect } from 'react';

function Example() {
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  
  useEffect(() => {
    otherAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return function cleanup() {
      otherAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // 省略...
}

  我们经由过程多个Hook来集合逻辑关注点,防止不相干的代码糅杂而涌现的逻辑杂沓。然则随之而来就碰到一个题目,假定我们的某个行动确定是要在辨别componentDidUpdate或许componentDidMount时才实行,useEffect是不是能辨别。幸亏useEffect为我们供应了第二个参数,假如第二个参数传入一个数组,仅当从新衬着时数组中的值发作转变时,useEffect中的回调函数才会实行。因而假如我们向其传入一个空数组,则可以模仿性命周期componentDidMount。然则假如你想仅模仿componentDidUpdate,现在临时未发明什么好的要领。

  useEffect与类组件性命周期差别的是,componentDidUpdatecomponentDidMount都是在DOM更新后同步实行的,但useEffect并不会在DOM更新后同步实行,也不会壅塞更新界面。假如须要模仿性命周期同步结果,则须要运用useLayoutEffect,其运用要领和useEffect雷同,地区只在于实行时间上。

Context:useContext

  借助Hook:useContext,我们也可以在函数组件中运用context。比拟于在类组件中须要经由过程render props的体式格局运用,useContext的运用则相称轻易。

import { createContext } from 'react'

const ThemeContext = createContext({ color: 'color', background: 'black'});

function Example() {
    const theme = useContext(Conext);
    return (
        <p style={{color: theme.color}}>Hello World!</p>
    );
}

class App extends Component {
  state = {
    color: "red",
    background: "black"
  };

  render() {
    return (
        <Context.Provider value={{ color: this.state.color, background: this.state.background}}>
          <Example/>
          <button onClick={() => this.setState({color: 'blue'})}>color</button>
          <button onClick={() => this.setState({background: 'blue'})}>backgroud</button>
        </Context.Provider>
    );
  }
}

  useContext接收函数React.createContext返回的context对象作为参数,返回当前context中值。每当Provider中的值发作转变时,函数组件就会从新衬着,须要注重的是,纵然的context的未运用的值发作转变时,函数组件也会从新衬着,正如上面的例子,Example组件中纵然没有运用过background,但background发作转变时,Example也会从新衬着。因而必要时,假如Example组件还含有子组件,你可以须要增加shouldComponentUpdate防备不必要的衬着糟蹋机能。

Ref: useRef useImperativeHandle

  useRef经常使用在接见子元素的实例:

function Example() {
    const inputEl = useRef();
    const onButtonClick = () => {
        inputEl.current.focus();
    };
    return (
        <>
            <input ref={inputEl} type="text" />
            <button onClick={onButtonClick}>Focus the input</button>
        </>
    );
}

  上面我们说了useRef经常使用在ref属性上,现实上useRef的作用不止于此

const refContainer = useRef(initialValue)

  useRef可以接收一个默许值,并返回一个含有current属性的可变对象,该可变对象会将延续悉数组件的性命周期。因而可以将其当作类组件的属性一样运用。

  useImperativeHandle用于自定义暴露给父组件的ref属性。须要合营forwardRef一同运用。

function Example(props, ref) {
    const inputRef = useRef();
    useImperativeHandle(ref, () => ({
        focus: () => {
            inputRef.current.focus();
        }
    }));
    return <input ref={inputRef} />;
}

export default forwardRef(Example);
class App extends Component {
  constructor(props){
      super(props);
      this.inputRef = createRef()
  }
  
  render() {
    return (
        <>
            <Example ref={this.inputRef}/>
            <button onClick={() => {this.inputRef.current.focus()}}>Click</button>
        </>
    );
  }
}

New Feature: useCallback useMemo

  熟习React的同砚见过相似的场景:
  

class Example extends React.PureComponent{
    render(){
        // ......
    }
}

class App extends Component{
    render(){
        return <Example onChange={() => this.setState()}/>
    }
}

  其实在这类场景下,虽然Example继续了PureComponent,但现实上并不可以优化机能,缘由在于每次App组件传入的onChange属性都是一个新的函数实例,因而每次Example都邑从新衬着。平常我们为相识决这个状况,平常会采纳下面的要领:

class App extends Component{
    constructor(props){
        super(props);
        this.onChange = this.onChange.bind(this);
    }

    render(){
        return <Example onChange={this.onChange}/>
    }
    
    onChange(){
        // ...
    }
}

  经由过程上面的要领一并处置惩罚了两个题目,起首保证了每次衬着时传给Example组件的onChange属性都是同一个函数实例,而且处置惩罚了回调函数this的绑定。那末怎样处置惩罚函数组件中存在的该题目呢?React供应useCallback函数,对事宜句柄举行缓存。

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

  useCallback接收函数和一个数组输入,并返回的一个缓存版本的回调函数,仅当从新衬着时数组中的值发作转变时,才会返回新的函数实例,这也就处置惩罚我们上面提到的优化子组件机能的题目,而且也不会有上面烦琐的步骤。

useCallback相似的是,useMemo返回的是一个缓存的值。

const memoizedValue = useMemo(
  () => complexComputed(),
  [a, b],
);

  也就是仅当从新衬着时数组中的值发作转变时,回调函数才会从新盘算缓存数据,这可以使得我们防止在每次从新衬着时都举行庞杂的数据盘算。因而我们可以以为:

useCallback(fn, input) 等同于
useMemo(() => fn, input)

  假如没有给useMemo传入第二个参数,则useMemo仅会在收到新的函数实例时,才从新盘算,须要注重的是,React官方文档提醒我们,useMemo仅可以作为一种优化机能的手腕,不能当作语义上的保证,这就是说,也会React在某些状况下,纵然数组中的数据未发作转变,也会从新实行。

自定义Hook

  我们前面讲过,Hook只能在函数组件的顶部挪用,不能再轮回、前提、平常函数中运用。我们前面讲过,类组件想要同享状况逻辑异常贫苦,必需要借助于render props和HOC,异常的烦琐。比拟于次,React许可我们建立自定义Hook来封装同享状况逻辑。所谓的自定义Hook是指以函数名以use开首并挪用其他Hook的函数。我们用自定义Hook来重写刚最先的定阅用户状况的例子:

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

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

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

    return isOnline;
}

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

  我们用自定义Hook重写了之前的定阅用户在线状况的例子,比拟于render prop和HOC庞杂的逻辑,自定义Hook越发的简约,不仅于此,自定义Hook也不会引发之前我们说提到过的组件嵌套地狱(wrapper hell)的状况。文雅的处置惩罚了之前类组件复用状况逻辑难题的状况。

总结

  借助于Hooks,函数组件已能基础完成绝大部分的类组件的功用,不仅于此,Hooks在同享状况逻辑、提高组件可保护性上有具有肯定的上风。可以预感的是,Hooks很有多是React可预感将来大的方向。React官方对Hook采纳的是逐渐采纳战略(Gradual Adoption Strategy),并示意现在没有设计会将class从React中剔除,可见Hooks会很长时间内和我们的现有代码并行事情,React并不发起我们悉数用Hooks重写之前的类组件,而是发起我们在新的组件或许非关键性组件中运用Hooks。
  
 若有表述不周的地方,谦虚接收批评指教。愿人人一同提高!

    原文作者:请叫我王磊同学
    原文地址: https://segmentfault.com/a/1190000018363838
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞