useState
用法
const [state, setState] = useState(initialState)
更新方法
-
函数式更新
setState((prevValue) => PrevValue + 1)
参数为前一个值 -
普通更新
setState(value)
初始化方法
-
普通的初始化方法
useState(initialState)
-
函数式的初始化方法
const [state, setState] = useState(() => { const initialState = someExpensiveComputation(props); return initialState; });
⚠️ 使用这种方式的好处是
someExpensiveComputation
函数值会在初始化的时候,调用一次。我们其实也可以这么做。
const initialState = someExpensiveComputation(props); const [state, setState] = useState(initialState);
这样的缺点就是,每次函数组件渲染的时候,都会调用一次``someExpensiveComputation`,造成不必要的性能浪费。
首先useState
是一个函数,每次运行useState
,它的第一个参数都是修改后的状态值,这个值应该是存在固定的指针下, useState
只是去同一个地方取值而已。
需要注意的地方
每次运行useState
返回的setState
指向同一个函数。这也就是为什么不需要在useEffect
或useCallback
中指定依赖。
import React, { useState, useEffect } from 'react';
function Hello () {
const [top, setScrollTop] = useState(1);
useEffect(() => {
window.addEventListener('scroll', (e) => {
setScrollTop(e.target.scrollTop);
}, false)
}, [])
return <h1>top: {top}</h1>
}
假设我们有这样一个场景,需要监听滚动,并且把滚动的距离输出。 如果每次返回的setScrollTop
不是指向同一个函数,而是随着top
进行更新的话,那我们就需要在每次组件更新的时候,去重新绑定滚动事件。
代码会是这样:
import React, { useState, useEffect } from 'react';
function Hello () {
const [top, setScrollTop] = useState(1);
useEffect(() => {
window.addEventListener('scroll', (e) => {
setScrollTop(e.target.scrollTop);
}, false)
}, [setScrollTop])
return <h1>top: {top}</h1>
}
如果在useEffect
中调用设置状态的方法,会不会导致组件无限刷新,考虑下面这样的代码
function Hello () {
const [top, setScrollTop] = useState(1);
useEffect(() => {
console.log('do it');
setScrollTop(10)
})
return <h1>top: {top}</h1>
}
结果是输出两次do it
。并不会无限刷新组件。 在组件第一次挂载的时候,会执行useEffect
输出do it
, 此时执行setScrollTop
, top
会被设置成10。 此时react会对比原值和当前值即1!==10
, 此时刷新组件。 当dom渲染完成之后,再次执行useEffect
, 输出do it
, 此时react会对比原值和当前值即10 === 10
, 不再更新组件。
useEffect
用法
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清除订阅
subscription.unsubscribe();
};
});
可以用它来代替react类组件中的生命周期,componentDidMount
, componentDidMount
, componentWillUnmount
。一般用来执行一些有副作用的操作。
需要注意的地方
-
组件在更新的时候,也会执行清除函数。
-
useEffect
的执行时机是在浏览器完成布局与绘制之后。所以可以在
useEffect
获取渲染完成之后的dom节点。类似``componentDidMount`
如何使用useEffect
来实现类组件中的生命周期呢?
useEffect
有一个可选参数,通过控制第二个参数,可以实现componentDidMount
和componentDidUpdate
生命周期。 第二个参数表示``useEffect要在哪些值发生变化后执行, 作用很相当于
vue中的
watch`
-
实现
componentDidMount
, 第二个参数传入空数组,表示不依赖任何值,那么只会在组件初次渲染的时候的执行。useEffect(() =>{}, [])
-
实现
componentDidUpdate
, 并且比componentDidUpdate
更细致,例如一个常见的需求,当id
发生变化时,去重新请求数据。// hooks版本 function Hello ({id, age}) { const [list, setList] = useState([]); useEffect(async () => { const data = await api.query(id); setList(data); }, [id]) // 当id发生变化的时候去请求数据 return (<div> {list.map(i => (<span>{i}</span>))} </div>) } // 类版本 async componentDidUpdate (prevProps) { // 需要自己判断id是否相等,去执行请求的数据, 尤其是依赖多个条件的时候,还需要挨个判断, if (prevProps.id !== props.id) { const data = await api.query(id); setList(data); } }
-
实现
componentDidUpdate
生命周期, 只需要在传入的函数中返回一个卸载时需要执行的函数即可useEffect(() =>{ return function clear () {} })
⚠️ 使用useEffect
最重要的一点是它可以把相关逻辑放到一起,例如我们经常会在组件挂载或者卸载的使用进行一些操作。例如常见的设置事件监听和移除事件监听。
// 类写法
handleScroll () {}
componentDidMount () {
window.addEventListener('scroll', this.handleScroll, false);
}
componentWillUnmount () {
window.addEventListener('scroll', this.handleScroll, false);
}
function handleScroll () {}
// hooks写法, 相关逻辑被放到了一起
useEffect(() =>{
window.addEventListener('scroll', handleScroll, false);
return window.removeEventListener('scroll', handleScroll);
}, [])
useContext
订阅context
的变化,感觉就是对于获取context
的值换了一种写法而已。相对于之前的写法,在函数组件中添加context
更加简单。
用法
const context = React.createContext({})
const { Provider, Consumer } = context;
// hooks的写法
class App extends React.Component {
return (
<Provider
value={{
name: 'li'
}}
>
<Hello/>
</Provider>
</div>
}
function Hello () {
const value = useContext(context);
return <h1>value: {value.name}</h1>
}
// 原本的写法
function Hello (props) {
function render ({name}) {
return <h1>value: {value.name}</h1>
}
return (
<Consumer>
{render}
</Consumer>
)
}
useReducer
类似于redux那样的状态更新方案。
使用场景(基本上就是redux的应用场景)
- 管理的状态值是对象,并且键值较多。
- state每个key修改的逻辑比较复杂,需要单独放到一个文件里面管理。
用法
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
useCallback
仅在指定的依赖项发生变化时,会返回一个新的函数引用,函数体并木有发生变化。
用法
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
这样使用的好处
-
不会在每次组件render的时候,重新生成一个函数,节省开销。例如
function f () { const cacheCallback = useCallback( () => { doSomething(a, b); }, [a, b], ) // 和下面这样的形式相比, 每次组件渲染的时候,都会重新创建一个doSometing函数 function doSometing (a,b) {} }
-
可以保持函数的引用保持不变。我们都知道在类组件,事件处理函数基本上都是通过
this.method
的方式绑定的,这样做的方式有一个好处,对方法的引用一直保持不变。 那么在函数组件就可以通过使用useCallback
来实现。 -
可以实现在子组件把该回调作为依赖处理。
function Parent ({a, b}) { const cacheCallback = useCallback( () => { doSometing(a, b); }, [a, b] ) return <Child handler={cacheCallback}/> } function Child ({ handler }) { useEffect(() => { handler(); }, [handler]) }
useMeno
类似于vue
的computed
,在依赖发生变化的时候重新计算缓存值。其实自己实现起来也很容易,和vue
的计算属性不同的是,vue
的计算属性是自动收集依赖的,而使用useMeno
需要手动在数组种传入依赖项。
用法
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useRef
故名思义,该hook主要是用来获取组件实例或者或者dom节点。 但是它更有用的地方,是可以返回一个在组件生命周期内,引用不变的对象。
用法
function f () {
const elRef = uesRef(null);
return <div ref={elRef}></div>
}
用来存储数据的话,考虑下面的场景。
let handler = () => {}; // 事件处理函数
// 不使用useRef, 可以使用函数外部的一个变量来存储数据
function f () {
useEffect(() => {
window.addEventListener('scroll', handler)
}, [])
const moveScroll = useCallback(
() => {
window.removeEventListener('scorll', handler)
},
[]
)
return <div onClick={moveScroll} ref={elRef}>移除scroll监听</div>
}
// 使用useRef的版本,可以使代码更加内聚。但是前提是必须要理解useRef这个hooks。
function f () {
const handler = useRef(null);
handler.current = () => {} // 事件处理
useEffect(() => {
window.addEventListener('scroll', handler.current)
}, [])
const moveScroll = useCallback(
() => {
window.removeEventListener('scorll', handler.current)
},
[]
)
return <div onClick={moveScroll} ref={elRef}>移除scroll监听</div>
}
useImperativeHandle
useImperativeHandle
可以让你在使用 ref
时自定义暴露给父组件的实例值。
用法
const Fancy = React.forwardRef((props, ref) => {
return <div>
<input type="text" ref={ref}/>
</div>
})
function Hello () {
const ref = useRef(null);
useEffect(() => {
console.log('current', ref); // { current: Input }
}, [])
return <Fancy ref={ref}/>
}
useLayoutEffect
函数签名和useEffect是一样的, 可以使用它来读取 DOM 布局并同步触发重渲染。
useDebugValue
用来给hooks添加上打印信息。