入坑React Hook

539 阅读5分钟

Hook出现原因

在实际项目中,充斥着各种生命周期代码,React的生命周期把我们业务逻辑撕裂成各自分割的一部分。而Hook的出现,让我们从面向生命周期编程变成了面向业务逻辑编程。让我们可以不再需要关心React的生命周期,而把注意力放在我们需要关注的业务逻辑上。

// Class Component
import React from 'react'

export default class App extends React.Component {
  state = {
    msg: ''
  }

  componentWillMount () {
    this.setState({ msg: 'hello, world' })
  }
  
  render () {
    const { msg } = this.state
    return (
      <div>{msg}</div>
    )
  }
}
// Function Component
import React, { useState } from 'react'

function App () {
  const [ msg, setMsg ] = useState('')
  return (
    <div>{msg}</div>
  )
}

export defaut App

React提供的常用Hook

useState(状态管理)

在函数组件里调用它来给组件添加一些内部 state。通过传入一个初始值,返回一对值:当前状态和一个让你更新它的函数,你可以在任何需要更新 state 的地方调用这个函数。和 class component 不同的是,这个初始值不一定是个对象。

import React, { useState } from 'react'

function Example() {
  // 声明一个叫 “count” 的 state 变量。
  const [ count, setCount ] = useState(0)
  // 入参传入方法
  const [ count2, setCount2 ] = useState(() => 0)
  // 入参传入字符串
  const [ fruit, setFruit ] = useState('banana')
  // 入参传入对象
  const [ user, setUser ] = useState({ name: '张三', age: 12 })

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

等同于

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      count2: 0,
      fruit: 'banana',
      user: { name: '张三', age: 12 }
    }
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    )
  }
}

useEffect(副作用管理)

给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具有相同的用途,只不过被合并成了一个 API。在开发过程中,请将所有副作用的代码放到useEffect中统一管理。useEffect在浏览器渲染完成后执行。

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

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

  useEffect(() => {
    // 使用浏览器的 API 更新页面标题
    document.title = `You clicked ${count} times`;
  });

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

注意,useEffect尽管可以实现 class 组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 的功能,但是useEffect本质上和他们没有任何关系,使用的时候请不要对号入座:

  • useEffect实现componentDidMount
useEffect(() => {
  // todo
  console.log('实现了componentDidMount')
}, [])

componentDidMount () {
  // todo
}
  • useEffect实现componentDidUpdate
useEffect(() => {
  // todo
  console.log('实现了componentDidUpdate')
})

componentDidUpdate () {
  // todo
}
  • useEffect实现componentWillUnmount
useEffect(() => {
  return () => {
    // todo
    console.log('实现了componentWillUnmount')
  }
}, [])

componentDidUpdate () {
  // todo
}

所以,如果你在页面中使用的定时器,想在组件被移除的时候,关闭定时器,你可以这么做

useEffect(() => {
  const timer = setInterval(() => {
    console.log('我是小溪流,永远向前流,小啊小啊小溪流,永远不停留')
  }, 1000)
  return () => {
    clearInterval(timer)
  }
}, [])

如果你组件中有很多state,但是你在处理副作用逻辑中,只依赖其中一个或几个 state,你可以这么做:

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

function Example() {
  const [ count, setCount ] = useState(0)
  const [ fruit, setFruit ] = useState('apple')
  const [ user, setUser ] = useState({ name: '张三', age: 10 })

  useEffect(() => {
    console.log(`${user.name}爱吃${fruit}`)
  }, [ user, fruit ]);

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

useCallback

把内联回调函数及依赖项数组作为参数传入useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。

import React, { useState, useCallback } from 'react'

function Example() {
  const [count, setCount] = useState(0);
  const [msg, setMsg] = useState('yes');
  // 方法一
  const sayHello = useCallback(() => {
    setCount(count + 1)
  }, [ count ]);

  return (
    <div>
      <p>{msg}</p>
      <button onClick={() => setMsg(msg === 'yes' ? 'no' : 'yes')}>say yes</button>
      <p>{count}</p>
      <Info say={sayHello} />
    </div>
  );
}

const Info = (props) => {
  return (
    <button onClick={props.say}>say hello</button>
  )
}

有同学可能就奇怪了,如果把sayHello写成如下形式,不是也可以正常运行吗?

// 方法二
const sayHello = () => {
  setCount(count + 1)
}

从运行效果上看,确实都可以运行,而且都没有报错。但是你可以在Info组件内部添加props.say方法的监听会发现:

const Info = (props) => {
  useEffect(() => {
    console.log('this is effect')
  }, [ props.say ])
  return (
    <button onClick={props.say}>say hello</button>
  )
}

当你点击 say yes时,你会发现方法二的写法,会在控制台一直打印this is effect,但是方法一并没有打印。因此,在我们开发过程中,对于会传给子组件的方法可以适当考虑用useCallback包裹一下。上面的例子还有没有可优化的空间呢?答案是:yes!

import React, { useState, useCallback } from 'react'

function Example() {
  const [count, setCount] = useState(0);
  const [msg, setMsg] = useState('yes');

  const sayHello = useCallback(() => {
    setCount(count + 1)
  }, [ count ]);

  return (
    <div>
      <p>{msg}</p>
      <button onClick={() => setMsg(msg === 'yes' ? 'no' : 'yes')}>say yes</button>
      <p>{count}</p>
      <Info say={sayHello} />
    </div>
  );
}

const Info = React.memo((props) => {
  useEffect(() => {
    console.log('this is effect')
  })
  return (
    <button onClick={props.say}>say hello</button>
  )
})

这个时候会发现点击 say yes,Info一点副作用都没有了。perfect!
引申一下:如果我想点击 Info组件内部的 say hello方法,useCallback依然缓存回调函数,但是需要 count能取到最新的值,那该怎么办呢?答案是:useRef + useLayoutEffect

import React, { useState, useRef, useLayoutEffect, useEffect, useCallback } from 'react'

function Example() {
  const [count, setCount] = useState(0);
  const [msg, setMsg] = useState('yes');
  const ref = useRef()

  // 当值发生变化时,将最新的值赋值给ref
  useLayoutEffect(() => {
    ref.current = count
  }, [ count ])

  // ref没有发生变化,则一直返回之前缓存的回调,current值变化,不会触发ref变化
  const sayHello = useCallback(() => {
    setCount(ref.current + 1)
  }, [ ref ]);

  return (
    <div>
      <p>{msg}</p>
      <button onClick={() => setMsg(msg === 'yes' ? 'no' : 'yes')}>say yes</button>
      <p>{count}</p>
      <Info say={sayHello} />
    </div>
  );
}

const Info = React.memo((props) => {
  useEffect(() => {
    console.log('this is effect')
  })
  return (
    <button onClick={props.say}>say hello</button>
  )
})

如果此时不理解useRef的用法和useLayoutEffect的用法,可以先放下。这个时候计算你点击 Info 组件的 say hello 按钮,你会发现Info中完全没有副作用,perfect!

useRef

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

const refContainer = useRef(initialValue)
refContainer.current = newValue
import React, { useRef } from 'react'

function TextInputWithFocusButton() {
  const inputEl = useRef(null);

  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

export default TextInputWithFocusButton

useMemo

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。和Vue中的computed作用一致。

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

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

useLayoutEffect

用法和useEffect一样,但是useLayoutEffect在浏览器渲染完成前执行。

function Example() {
  useLayoutEffect(() => {
    console.log("useLayoutEffect ...");
  });

  useEffect(() => {
    console.log("useEffect ...");
  });

  return <div></div>;
}

上述的例子,会先打印 “useLayoutEffect ...” ,再打印 “useLayoutEffect ...”

useContext

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。

import React, { useContext } from 'react'

const theme = {
  light: {
    color: 'white',
    bg: 'black'
  },
  dark: {
    color: 'black',
    bg: 'white'
  }
}

// 创建context,并设置默认值
const ThemeContext = React.createContext(theme.light)

function Example () {
  return (
    <ThemeContext.Provider value={theme.light}>
      <div><Info /></div>
    </ThemeContext.Provider>
  )
}

function Info () {
  const ctx = useContext(ThemeContext)
  return (
    <div style={{ color: ctx.color, backgroundColor: ctx.bg }}>
      主要内容
    </div>
  )
}

export default Example;