在React 的世界中,有容器组件和UI组件之分,在React Hooks出现之前,UI组件我们可以使用函数,无状态组件来展示UI,而对于容器组件,函数组件就显得无能为力,我们依赖于类组件来获取数据,处理数据,并向下传递参数给UI组件进行渲染。在我看来,使用React Hooks相比于从前的类组件有以下几点好处:
1.代码可读性更强,原本同一块功能的代码逻辑被拆分在了不同的生命周期函数中,容易使开发者不利于维护和迭代,通过React Hooks可以将功能代码聚合,方便阅读维护
2.组件树层级变浅,在原本的代码中,我们经常使用HOC/render props等方式来复用组件的状态,增强功能等,无疑增加了组件树层数及渲染,而在React Hooks中,这些功能都可以通过强大的自定义的Hooks来实现
React 在v16.8的版本中推出了React Hooks新特性,虽然社区还没有最佳实践如何基于React Hooks来打造复杂应用(至少我还没有),凭借着阅读社区中大量的关于这方面的文章,下面我将通过十个案例来帮助你认识理解并可以熟练运用React Hooks大部分特性。
1、useState保存组件状态
在类组件中,我们使用this.state来保存组件状态,并对其修改触发组件重新渲染。比如下面这个简单的计数器组件,很好诠释了类组件如何运行:
function UseState(props) {
let [count, setCount] = useState(0)
function hideClick() {
setCount(() => {
return count + 1
})
}
return (
<div>
<span>{count}</span>
<button onClick={hideClick}>++</button>
</div>
)
}
export default UseState
通过传入useState参数后返回一个带有默认状态和改变状态函数的数组。通过传入新状态给函数来改变原本的状态值。值得注意的是useState不帮助你处理状态,相较于setState非覆盖式更新状态,useState覆盖式更新状态,需要开发者自己处理逻辑。(代码如上)
似乎有个useState后,函数组件也可以拥有自己的状态了,但仅仅是这样完全不够。
2、useEffect处理副作用
函数组件能保存状态,但是对于异步请求,副作用的操作还是无能为力,所以React提供了useEffect 来帮助开发者处理函数组件的副作用,在介绍新API之前,我们先来看看类组件是怎么做的:
import React from 'react'
import { useState, useEffect } from 'react'
export default function (props) {
let [data, setData] = useState({ count: 0 })
useEffect(() => {
console.log('effect')
}, [data])
//[] 空数组不依赖任何值,只执行一次
useEffect(() => {
return () => {
console.log('unmount')
}
}, [])
return (
<div>
<div>{data.count}</div>
<button onClick={() => setData(data => ({ count: data.count + 1 }))}>click</button>
</div>
)
}
3、UseContext减少组件层级
通过React Context的语法,在App组件中可以跨过Foo组件给Bar组件传递数据,而在React Hookes中,我们可以使用useContext进行改造。
import React, { createContext, useContext } from 'react'
let nameContext = createContext({ name: 'zhangsan' })
let ageContext = createContext({ age: 12 })
export default function UseContext() {
let name = useContext(nameContext)
let age = useContext(ageContext)
return (
<div>
{name.name} {age.age}
</div>
)
}
4、useReducer
useReducer这个Hooks在使用上几乎跟Redux、React-Redux几乎一样,唯一缺少的就是无法使用redux提供的中间件。
import React, { useReducer } from 'react'
const initState = {
count: 0
}
const reducer = (state, action) => {
switch (action.type) {
case 'add':
return {
count: state.count + 1
}
case 'minus':
return {
count: state.count - 1
}
default:
return state
}
}
export default function UseReducer() {
let [state, dispatch] = useReducer(reducer, initState);
return (
<div>
<button onClick={() => dispatch({ type: 'minus' })}>-</button>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: 'add' })}>+</button>
</div>
)
}
5、useCallback 记忆函数
一版把函数式组件理解为class组件render函数的语法糖,所以每次重新渲染的时候,函数式组件内部所有的代码都会重新执行一遍。所以上述代码中每次render, handleClick都会是一个新的引用,所以也就是说传递给SomeComponent组件的props.onClick一直在变(因为每次都是一个新的引用),所以才会说这种情况下,函数组件在每次渲染的时候如果有传递函数的话都会重渲染子组件。
而有了useCallback 就不一样了,你可以通过useCallback 获得一个记忆后的函数。
function(){
const memoizedHandleClicj = useCallback(()=>{
console.log('Click happend')
},[]); //空数组代表无论什么情况下该函数都不会发生改变
return <SomeComponent onclick={memoizedHandleclick}>click Me</SomeComponent>;
}
第二个参数传入一个数组,数组中的每一项一旦值或者引用发生改变,useCalback就会重新返回一个新的函数提供给后面进行渲染。
这样质押子组件继承了PureComent或者使用Reac.memo就可以有效避免不必要的VDOM渲染。
import React, { useState, useCallback, memo } from 'react'
const Child = memo(function (props) {
console.log('Child run....')
return (
<>
<h1>Hello</h1>
<button onClick={props.onAdd}>add</button>
</>
)
}, () => {
//true false自己定义是否缓存
return true
})
export default function UseCallback() {
console.log('UseCallback run....')
let [count, setCount] = useState(0)
const handleAdd = useCallback(
() => {
console.log('added.')
}, [])
return (
<div>
<div>{count}</div>
<Child onAdd={handleAdd}></Child>
<button onClick={() => { setCount(100) }}>change count</button>
</div>
)
}
6、useMemo 记忆组件
useCallback的功能完全可以由useMemo所取代,如果你想通过使用useMemo返回一个记忆函数也是完全可以的。
usecallback(fn,inputs) is equivalent to useMemo( ( ) => fn,inputs ).
所以前面使用useCallback 的例子可以使用useMemo进行改写:
function App(){
const memoHandleClick = useMeno(()=> ()=>{
console.log('click')
},[]) //空数组代表无论什么情况下该函数都不会发生改变
return <SomeComponent onclick={memoHandleClick}>Click me<SomeComponent/>
}
唯一的区别是: useCallback 不会执行第一个参数函数,而是将它返回给你,而useMemo会执行第一个函数并且将函数执行结果返回给你。所以在前面的例子中,可以返回handleClick 来达到存储函数的目的。
所以useCallback 常用记忆事件函数,生成记忆后的事件函数并传递给子组件使用。而useMemo更适合经过函数计算得到一个确定的值,比如记忆组件。
import React, { useState, useMemo, memo } from 'react';
const ChildHood = function (props) {
return (
<div>Chind Hood</div>
)
}
const Child = memo(function ({ a, b, onAdd }) {
console.log('Child run....')
const Child1 = useMemo(() => () => <ChildHood a={a}></ChildHood>, [a])
const Child2 = useMemo(() => () => <ChildHood b={b}></ChildHood>, [b])
return (
<>
<h1>Hello</h1>
<button onClick={onAdd}>add</button>
<Child1></Child1>
<Child2></Child2>
</>
)
})
7、useRef保存引用值
import React, { createRef, useRef } from 'react'
export default function UseRef() {
// const btn = createRef();
const btn = useRef();
function handleClick() {
console.log(btn)
}
return (
<div>
<button ref={btn} onClick={handleClick}>click</button>
</div>
)
}
export default function App() {
const count = useRef(0)
const handleClick = (num) => {
count.current += num
setTimeout(() => {
console.log("count: " + count.current);
}, 3000)
}
return (
<div>
<p>You clicked {count.current} times</p>
<button onClick={() => handleClick(1)}>增加 count</button>
<button onClick={() => handleClick(-1)}>减少 count</button>
</div>
);
}
8、useImperativeHandle
import React, { useRef, useEffect, useImperativeHandle, forwardRef } from "react";
function ChildInputComponent(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => inputRef.current);
return <input type="text" name="child input" ref={inputRef} />;
}
//forwardRef 高阶组件 ref传到下个组件
const ChildInput = forwardRef(ChildInputComponent);
function App() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<div>
<ChildInput ref={inputRef} />
</div>
);
}
export default App
通过这种方式,App组件可以获得子组件的input的DOM节点。
9、useLayoutEffect同步执行副作用
大部门情况下,使用useEffect就可以处理组件的副作用
10、useEffect和useLayoutEffect有什么区别?
简单来说就是调用时机不同,useLayoutEffect和原来componentDidMount & componentDidupdate一致,在react完成DOM更新后马上同步**调用的代码,会阻塞页面渲染。而'useEffect
是会在整个页面渲染完才会调用的代码。
在实际使用时如果想避免页面抖动(在useEffect里修改DOM很有可能出现)的话,可以把需要操作DOM的代码放在useLayoutEffect里。关于使用useEffect导致页面抖动。
不过useLayoutEffect在服务端渲染时会出现一个warning,要消除的话得用useBffect代替或者推迟渲染时机。