Hook 是 React 16.8 的新增特性,它的动机这里就不做过多的介绍了。下面我们主要说一下几个主要hooks的用法,以及遇到的一些,用起来不是很顺畅的地方。
一、useEffect
你之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。useEffect就是一个Effect Hook 给函数组件增加了操作副作用的能力。
主要从一下两个问题中描述:
- useEffect是否与class组件的生命周期完全一致?
- 如何正确地在useEffect里请求数据?
1、useEffect与生命周期函数异同
例如,React 更新 DOM 后会设置一个页面标题
class组件:
class counter extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
hooks函数组件
import React,{useState,useEffect} from 'react'
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
通过模拟,很明显我们发现,大多数情况下,我们会需要在mount和update中执行副作用,此时hooks的写法相对于class组件要简单明了的多。
不过,看到这个例子,大家也不要认为useEffect和生命周期函数可以完全相等。在hooks内部使用了javascript的闭包机制,useEffect会捕获props和state,在回调函数中拿到的也是初始的props和state.
useEffect的capture value 特性。
即上面说的useEffect中props和state不会改变。
不过大家可能又会疑惑,为什么说props和state不会变,但是每次点击按钮后,显示的次数都不一致呢?!
这事因为,当我们点击按钮更新状态的时候,React会重新渲染组件。每一次渲染都能拿到独立的count 状态,这个状态值是函数中的一个常量。当setCount的时候,react就会带着不同的值,更新DOM。不用着急,看下面这个代码,你就明白了
// During first render
function Counter() {
// ...
useEffect(
// Effect function from first render
() => {
document.title = `You clicked ${0} times`;
}
);
// ...
}
// After a click, our function is called again
function Counter() {
// ...
useEffect(
// Effect function from second render
() => {
document.title = `You clicked ${1} times`;
}
);
// ...
}
由此可知,count在不变的effect中产生变化,主要是因为每一次的渲染effect都不相同。
useEffect的清理
上述,我们知道了useEffect实现了class组件的mount和update方法。那组件卸载呢?
useEffect 返回一个函数(可选),这就是它可选的清除机制
useEffect(()=>{
//...
return ()=>{
//清除操作
}
})
这里需要注意的是,class组件的componentWillUnmount只会在组件卸载的时候执行。useEffect返回的函数会在每次渲染后执行。
useEffect 将产生副作用的操作,都整合在一起,在代码量上简写了很多,也是一大优势。
注意: 一个常见的问题,当组件卸载后,依然会在effect中设置state。
useEffect(()=>{
let isUnmount = false
//...
if(!isUnmount){
setValue(value)
}
return ()=>{
isUnmount = true
//清除操作
}
})
useEffect的依赖项
useEffect还有第一个可选参数,即可以传入函数的依赖项。
我们先看一下在componentWillReciveProps中,我们会判断props和nextProps是都一致 来判断是否需要更新,那么在useEffct是怎么解决的呢?!
import React,{useState,useEffect} from 'react'
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('xixi');
useEffect(() => {
document.title = `You clicked ${name} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
此时,每次点击按钮,name并未改变,都会调用useEffect,重新给document.title赋值。很显然这并不是我们想要的。所以,我们可以给useEffect穿第二个参数,给定该effect的依赖项
useEffect(() => {
document.title = `You clicked ${name} times`;
},[name]);
只有当name更新了,才会执行该effect,反之,则会跳过该effect了。是不是简单清晰又明了呢~
当第二个参数为空数组时,该effect只会执行一次,即模拟了componentDidUnmount效果。
useEffect(()=>{
//...
},[])
需要强调的是,关于依赖项不要企图欺瞒react
举个例子,我们来写一个每秒递增的计数器。在Class组件中,我们的直觉是:“开启一次定时器,清除也是一次”。当我们理所当然地把它用useEffect的方式翻译,直觉上我们会设置依赖为[]。
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
},[]);
上述,useEffect只会执行一次,由于useEffect的capture value 特性,count的值不会更新。所以我们应该如实的给出count这个依赖项。
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
},[count]);
但是这样会导致,定时器在每次count更新后清除和重新设定, 这里我们需要了解下,如何移除依赖? 1、我们需要的每次count+1,那么我们可以采用setCount回调的方式
useEffect(() => {
const id = setInterval(() => {
setCount(c=>c + 1);
}, 1000);
return () => clearInterval(id);
},[]);
上述有一个弊端。现在我们每次固定递增1,当递增的数会发生变化时,我们依然会遇到之前的问题。所以,需要探索其他的解决办法。
2、采用useReducer
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' }); // Instead of setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
effect里不用再关心count的更新,更新的逻辑交给reducer
我现在的项目,虽然用的hooks,但是,依然用redux状态管理,同理,此处也可以将更新操作交给redux。
此处还需要强调一点,依赖项我们不能忘记,但出现以下依赖过多的情况时,需要注意拆分useEffect,避免不必要的更新计算。
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
//...
return () => clearInterval(id);
}, [a,b,c,d,e,f,g,...]);
2.在useEffect中请求数据
先看一个简单的例子
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
});
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
上述代码, 在运行时,我们发现会陷入循环。因为该effect将会在页面加载以及更新时执行,而在该effect存在setData,会导致组件更新,继续执行该effect,从而陷入循环。
如果我们只需要在页面加载时,拉取数据,需要给定useEffect第二个参数为[],
useEffect(async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
},[]);
更改后,我们会发现,控制台会出现警告⚠️,"Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => ...) are not supported, but you can call an async function inside an effect.. " 由此可知useEffect无法返回一个AsyncFunction对象。 改写如下
useEffect( () => {
const fetchData = async()=>{
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
}
fetchData()
},[]);
如何手动触发更新数据?
表单查询中,查询条件发生变化时,useEffect中的处理:
useEffect很明显需要依赖query查询条件
import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
};
fetchData();
}, [query]);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</Fragment>
);
}
export default App;
查看上诉代码会发现,每次input的值发生改变时,useEffect都会执行一次请求数据,有时候,这样的行为并不是我们所需要的。我们需要在【输入完成】后 再请求数据。
这里可以有两种做法,通常我们项目中都会封装Input组件,会提供输入完成函数并返回当前值,这里不做过多说明。另外 我们可以在input失去焦点时请求数据。注意。这时 useEffect 依赖项 有 input 是否失去焦点,但同时 我们也需要判断输入的值与上次是否相同
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [search, setSearch] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
};
fetchData();
}, [query]);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
onBlur={()=>{
setSearch(query)
}}
/>
<!--...-->
</Fragment>
);
如上所写,可以实现我们的需求,但是有一点不舒服的是,search和query存储的值都是一样的,有点迷惑。所以我们可以在search中设置为请求的url,这样看起来正常点。不过,倔强的你,还是可以像上面这样的。
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
'https://hn.algolia.com/api/v1/search?query=redux',
);
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
};
fetchData();
}, [url]);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
onBlur={()=>{
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
}}
/>
<!--...-->
</Fragment>
);
编写自定义获取数据钩子
拉取数据时,一般需要如下操作
- 设置loading状态,
- 请求数据
- 错误处理
以下只是简单的demo,具体根据业务调整
const usefetchData = (initUrl,initData) => {
const [data, setData] = useState({ hits: [] });
const [url, setUrl] = useState(initUrl);
const [isLoading, setIsLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState('');
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
const {ok,result} = await axios(url);
if(!ok){
setErrorMsg(data || data.message)
setIsLoading(false);
}
setData(result.data);
setIsLoading(false);
};
fetchData();
}, [url]);
return [{ data, isLoading, errorMsg }, setUrl];
}
function App() {
const [query, setQuery] = useState('redux');
const [{ data, isLoading, errorMsg }, doFetch] = usefetchData(
'https://hn.algolia.com/api/v1/search?query=redux',
{ hits: [] },
);
return (
<Fragment>
<form onSubmit={event => {
doFetch(`http://hn.algolia.com/api/v1/search?query=${query}`);
event.preventDefault();
}}>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="submit">Search</button>
</form>
{errorMsg && (<div>{errorMsg}</div>)}
{isLoading ? (
<div>Loading ...</div>
) : (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)}
</Fragment>
);
}
另外,数据可以存在useReducer中,同理redux类似。
const usefetchData = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
useEffect(() => {
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
const result = await axios(url);
dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
} catch (error) {
dispatch({ type: 'FETCH_FAILURE' });
}
};
fetchData();
}, [url]);
...
};
const dataFetchReducer = (state, action) => {
switch (action.type) {
case 'FETCH_INIT':
return {
...state,
isLoading: true,
isError: false
};
case 'FETCH_SUCCESS':
return {
...state,
isLoading: false,
isError: false,
data: action.payload,
};
case 'FETCH_FAILURE':
return {
...state,
isLoading: false,
isError: true,
};
default:
throw new Error();
}
};
二、useState
const {value,setValue} = useState('')
const {loading,setLoading} = useState(false)
const {isError,setError} = useState(false)
...
如果觉得需要的变量过多,可以采用对象的形式
const {obj,setObj} = useState({
value:'',
loading:false,
isError:false,
})
useState和class组件中的setState类似,都是【异步】的,这点真的要注意了,很容易被忽略。
(例子不恰当,只是为了说明问题
function Counter() {
const [count, setCount] = useState(0);
const dealCount = ()=>{
setCount(count + 1)
}
const save = ()=>{
console.log(count) //--- 有可能值并未更新
//TODO
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={dealCount}>
Click me
</button>
<button onClick={save}>
保存
</button>
</div>
);
}
useState 依赖调用顺序,注意不要在条件判断等改变调用顺序的代码块中使用useState
具体参考:
overreacted.io/why-do-hook…
三、useRef
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传递的参数(initialValue)。返回的对象将存留在整个组件的生命周期中
- useRef --- DOM引用
function Preview() {
const canvasRef = useRef(null)
const completeCanvasRef = useRef(null)
return (
<div>
<canvas ref={canvasRef} />
<canvas ref={completeCanvasRef} />
</div>
);
}
2.ref 对象可以确保在整个生命周期中值不变,且【同步】更新,是因为 ref 的返回值始终只有一个实例
可以解决一些 由于useState异步 导致未更新的问题 (例子不恰当,只是为了说明问题
function Counter() {
const count = useRef();
const dealCount = ()=>{
count.current += 1
}
const save = ()=>{
console.log(count.current) //--- 更新后的值
//TODO
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={dealCount}>
Click me
</button>
<button onClick={save}>
保存
</button>
</div>
);
}
四、useMemo 与 useCallback
useCallback和useMemo的参数跟useEffect一致,他们之间最大的区别有是useEffect会用于处理副作用,而前两个hooks不能。
useMemo和useCallback都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数。
useMemo:
function Counter() {
const [name,setName] = useState('you');
const [count,setCount] = useState(0);
const dealCount = () =>{
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}
return (
<div>
<p>{name} clicked {dealCount()} times</p>
<button onClick={() => setCount(count + 1)}>click me</button>
<input value={name} onChange={event => setName(event.target.value)}/>
</div>
);
}
当input 发生改变时,此时组件重绘,每次都会调用dealCount重新计算,但是 dealCount只依赖与count的改变,上述代码可以改写为:
function Counter() {
const [name,setName] = useState('you');
const [count,setCount] = useState(0);
const dealCount = useMemo(() =>{
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
},[count])
return (
<div>
<p>{name} clicked {dealCount} times</p>
<button onClick={() => setCount(count + 1)}>click me</button>
<input value={name} onChange={event => setName(event.target.value)}/>
</div>
);
}
useCallback: 涉及到组件通讯时,useCallback会派上用场
function Parent() {
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
const dealCount = useCallback(() => {
//TODO
return count
}, [count]);
return <div>
<h4>{count}</h4>
<Child dealCount={dealCount}/>
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<input value={val} onChange={event => setVal(event.target.value)}/>
</div>
</div>;
}
function Child({ dealCount }) {
useEffect(() => {
dealCount();
}, [dealCount]);
}
如果不采用useCallback,input发生变化时,每次都会调用dealCount重新计算, useCallback(fn, inputs) 等价于 useMemo(() => fn, inputs)。
参考文章:
关于react hooks先介绍这么多。希望大家指正。