本文内容大部分参考了 overreacted.io 博客一文,同时结合 React Hook 官方 文章,整理并概括一些笔记和输出我的的一些理解javascript
官方介绍:Hook 是 React 16.8 的新增特性。它可让你在不编写 class 的状况下使用 state 以及其余的 React 特性。html
// 传入初始值,做为 state
const [state, setState] = useState(initialState)
// `惰性初始 state`;传入函数,由函数计算出的值做为 state
// 此函数只在初始渲染时被调用
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props)
return initialState
})
复制代码
该 Hook 接收一个包含命令式、且可能有反作用代码的函数.java
在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其余包含反作用的操做都是不被容许的,由于这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。react
使用 useEffect 完成反作用操做,赋值给 useEffect 的函数会在组件渲染到屏幕以后。你能够把 effect 看做从 React 的纯函数式世界通往命令式世界的逃生通道。ios
默认状况下,effect 将在每轮渲染结束后执行,但你能够选择让它 在只有某些值改变的时候才执行。详情见后面。ajax
清除 effect 一般,组件卸载时须要清除 effect 建立的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect 函数需返回一个清除函数。如下就是一个建立订阅的例子:npm
useEffect(() => {
const subscription = props.source.subscribe()
return () => {
// 清除订阅
subscription.unsubscribe()
}
}, [依赖])
复制代码
React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件链接到 store)。若是你使用过 React 一段时间,你也许会熟悉一些解决此类问题的方案,好比 render props 和 高阶组件。可是这类方案须要从新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。若是你在 React DevTools 中观察过 React 应用,你会发现由 providers,consumers,高阶组件,render props 等其余抽象层组成的组件会造成“嵌套地狱”。尽管咱们能够在 DevTools 过滤掉它们,但这说明了一个更深层次的问题:React 须要为共享状态逻辑提供更好的原生途径。json
你可使用 Hook 从组件中提取状态逻辑,使得这些逻辑能够单独测试并复用。Hook 使你在无需修改组件结构的状况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。axios
咱们常常维护一些组件,组件起初很简单,可是逐渐会被状态逻辑和反作用充斥。每一个生命周期经常包含一些不相关的逻辑。例如,组件经常在 componentDidMount 和 componentDidUpdate 中获取数据。可是,同一个 componentDidMount 中可能也包含不少其它的逻辑,如设置事件监听,而以后需在 componentWillUnmount 中清除。相互关联且须要对照修改的代码被进行了拆分,而彻底不相关的代码却在同一个方法中组合在一块儿。如此很容易产生 bug,而且致使逻辑不一致。windows
在多数状况下,不可能将组件拆分为更小的粒度,由于状态逻辑无处不在。这也给测试带来了必定挑战。同时,这也是不少人将 React 与状态管理库结合使用的缘由之一。可是,这每每会引入了不少抽象概念,须要你在不一样的文件之间来回切换,使得复用变得更加困难。
为了解决这个问题,Hook 将组件中相互关联的部分拆分红更小的函数(好比设置订阅或请求数据),而并不是强制按照生命周期划分。你还可使用 reducer 来管理组件的内部状态,使其更加可预测。
下面的代码能够直观的体现出来,在某些场景下,使用 hook 来实现对应的功能,能够节省大部分的代码
对比 Class
组件来讲,清除反作用要简单的多,以下代码,在 useEffect
hook 里面返回一个函数,当咱们的函数组件卸载的时候,就会自动执行这个函数,从而来清除反作用。想一想咱们在 Class
组件里面须要在 componentWillUnmount
生命周期里面去编写对应的代码。
对比二者咱们发现,使用 useEffect
的方式,可以将挂载和卸载的逻辑更加紧密的耦合在一块儿,从而减小 BUG 的发生
useEffect(() => {
const id = setInterval(() => {
setCount(count => count + 1)
}, 1000)
return () => clearInterval(id)
}, [])
// 好比给 windows 挂载监听函数
useEffect(() => {
window.addEventListener('reszie', handleRezie)
return () => {
window.removeEventListener('resize', handleRezie)
}
}, [])
复制代码
使用 useEffect 完成反作用操做,赋值给 useEffect 的函数会在组件渲染到屏幕以后;牢记这句话。
仔细观察以下代码,当函数组件里面,有多个 effect
的时候,默认的 effect
将在每次 UI render 以后被调用。当咱们经过 useEffect
的第二个数组类型参数,指明当前 effect
的依赖,就能避免不相关的执行开销了。
经过启用 eslint-plugin-react-hooks 插件,来强制提醒咱们在使用 effect
的时候,申明所须要的依赖
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
复制代码
const CounterHook = () => {
const [count, setCount] = useState(0)
const [name, setName] = useState('heaven')
useEffect(() => {
document.title = `counterWithHook ${count}`
}, [count])
useEffect(() => {
console.log('you name is', name)
}, [name])
return (
<div> <h3>Counter with Hook</h3> <p>You click {count} times</p> <button onClick={e => setCount(count => count + 1)}>Click me</button> <p> <input placeholder="输入姓名" onChange={e => setName(e.target.value)} /> <br /> your name is {name} </p> </div> ) } 复制代码
对于 useEffect
内部方法,一旦引用外部的函数,那么这个时候须要注意了: 须要把 useEffect 内部引用到的方式,声明为当前 effect 的依赖 在下图的代码中,咱们能够看到,在 effect 函数内部,引入外部的函数,咱们的 eslint-plugin-react-hooks 插件会自动提示咱们须要把对应的函数做为依赖添加进去
不规范示例:这里在安装了插件的状况下,会自动提示咱们将 fetchData
函数移入 effect 内部
const getFetchUrl = () => {
return `https://hn.algolia.com/api/v1/search?query=${query}`
}
const fetchData = async () => {
return axios.get(getFetchUrl())
}
useEffect(() => {
fetchData().then(resp => {
console.log(resp)
setData(resp.data)
})
}, [])
复制代码
正确的写法:
useEffect(() => {
const getFetchUrl = () => {
return `https://hn.algolia.com/api/v1/search?query=${query}`
}
const fetchData = async () => {
return axios.get(getFetchUrl())
}
fetchData().then(resp => {
console.log(resp)
setData(resp.data)
})
}, [query])
复制代码
每一次渲染都有它本身的 Props and State 每一次渲染都有它本身的事件处理函数 每次渲染都有它本身的 Effects
运行以下代码以后,在咱们点击 Show alert
按钮以后,而后点击 Click me
按钮,alert
输出的永远是在点击的那个时刻的 count;
换句话来讲;在 hooks 组件里面,每一次的渲染,都至关于记录当前次的『快照』
import React, { useEffect, useState } from 'react'
const Counter = () => {
const [count, setCount] = useState(0)
const handleAlertClick = () => {
setTimeout(() => {
alert(`Yout clicked me: ${count}`)
}, 3000)
}
useEffect(() => {
setTimeout(() => {
console.log(`Yout clicked ${count} times`)
}, 3000)
})
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> <button onClick={handleAlertClick}>Show alert</button> </div>
)
}
export default Counter
复制代码
经过自定义 Hook,能够将组件逻辑提取到可重用的函数中。
当咱们想在两个函数之间共享逻辑时,咱们会把它提取到第三个函数中。而组件和 Hook 都是函数,因此也一样适用这种方式。
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部能够调用其余的 Hook。
useService.js
自定义的一个 server hook,该 hook 封装了 ajax
请求中的 { loading, error, response }
三个基础逻辑;有了这个 hook
咱们就能很轻松的在每次网络请求里面去处理各类异常逻辑了;详细用法看文章最后的 Table 分页操做实例
import { useEffect, useRef, useState, useCallback } from 'react'
import { isEqual } from 'lodash'
const useService = (service, params) => {
const prevParams = useRef(null)
const [callback, { loading, error, response }] = useServiceCallback(service)
useEffect(() => {
if (!isEqual(prevParams.current, params)) {
prevParams.current = params
callback(params)
}
})
return { loading, error, response }
}
const useServiceCallback = service => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [response, setResponse] = useState(null)
// 使用 useCallback,来判断 service 是否改变
const callback = useCallback(
params => {
setLoading(true)
setError(null)
service(params)
.then(response => {
console.log(response)
setLoading(false)
setResponse(response)
})
.catch(error => {
setLoading(false)
setError(error)
})
},
[service]
)
return [callback, { loading, error, response }]
}
复制代码
以下代码,使用 hook
的方式来实现表格的分页,数据请求操做,
使用 hook
实现一个简易版的跑马灯抽奖逻辑