类组件是一种面向对象思想的体现,类组件之间的状态会随着功能增强而变得越来越臃肿,代码维护成本也比较高,而且不利于后期 tree shaking。所以有必要做出一套函数组件代替类组件的方案,于是 Hooks 也就理所当然的诞生了。
hooks诞生的原因
- 组件之间的状态复用困难
- 类组件理解成本较高,业务逻辑分散在生命周期中
- class和this的特性较为复杂
- 解决函数组件无状态只能用于简单组件的痛点,使其拥有持续化的状态
- 拥抱函数式编程
使用hook注意事项
- 只在只在最顶层使用Hook
- 不要在循环, 条件或嵌套函数中调用 Hook
常用hooks
1. useState
解决函数组件无状态只能用于简单组件的痛点,使其拥有持续化的状态。
- 用法
const [value, setValue] = useState(initValue);
setValue(newValue);
setValue((value) => newValue);
- 注意
- 可以在同一个函数组件中被调用多次
- 在hook中,state状态值不再要求一定是一个对象,可以是数字等简单类型
- state的类型是对象时,不会自动合并
- 无回调函数
- 多个不同的state,为了更好的复用,建议使用多个useState
2. useEffect
副作用函数,在依赖的数据发生变化时执行,可用来监听状态变化,模拟生命周期等
- 用法
useEffect(() => {
// 依赖发生变化了...
return () => {
// 下一次useEffect运行前被执行
};
}, [依赖的状态]);
-
注意
- useEffect可以在同一个函数组件中被调用多次
- 第一个参数是函数,返回值函数在下一次执行前执行
- 第二个参数是数组,可以指定绑定的关联数据,不传每次更新都会执行,为空数组则只执行一次
- useEffect不能被打断
- useEffect不能被判断包裹
- 可用useEffect模拟生命周期
-
模拟生命周期
useEffect(() => {
// componentsWillUpdate
// componentMounted (仅在依赖数组为空时)
return () => {
// componentWillUnmount (仅在依赖数组为空时)
// 这里常用来解绑事件,取消订阅
};
}, [依赖的状态]);
- useEffect 里面使用到的state的值, 固定在了useEffect内部,不会被改变,除非useEffect刷新,重新固定state的值。在依赖中添加count即可触发刷新
const [count, setCount] = useState(0)
useEffect(() => {
console.log('use effect...',count)
const timer = setInterval(() => {
console.log('timer...count:', count) // 一直是
setCount(count + 1)
}, 1000)
return ()=> clearInterval(timer)
},[])
3. useRef
希望可以实时拿到最新的值,并且不需要添加到依赖不会触发useEffect重新执行,可用来获取dom等。
useEffect 里面使用到的state的值, 固定在了useEffect内部, 不会被改变,除非useEffect刷新,重新固定state的值
- 用法
import React, { useRef, forwardRef, useImperativeHandle } from 'react'
// 转发透传ref
const ForwardRefIndex = React.forwardRef((props, ref) => <Input {...props} ref={ref} />)
function Home(){
const ref = useRef(null)
useEffect(() => {
console.log(ref.current)
}, [])
return <Input ref={ref} />;
}
// 父组件调用子函数组件的方法
const JMInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
},
}));
return <input type="text" ref={inputRef} />;
})
function ImperativeHandleDemo() {
const inputRef = useRef();
return (
<div>
<button onClick={() => inputRef.current.focus()}>聚焦</button>
<JMInput ref={inputRef} />
</div>
)
}
4. useContext
解决通用状态在所有子组件中共享的问题,可以在所有子孙组件中使用状态
- 使用
const ThemeContext = React.createContext(null)
// 消费者
const Son = () => {
const { color, background } = useContext(ThemeContext);
return <div style={{ color, background, padding: '20px' }}>这是子组件</div>;
};
// 提供者
const ThemeProvider = ThemeContext.Provider;
export default function ProviderDemo(){
const [ contextValue , setContextValue ] = React.useState({ color:'#ccc', background:'pink' })
return <div>
<ThemeProvider value={ contextValue } >
<Son />
</ThemeProvider>
</div>
}
5. useMemo
使用缓存避免数据并未更新但是重新render减少渲染
- 使用
const Child = memo(({data}) =>{
console.log('child render...', data.name)
return (
<div>
<div>child</div>
<div>{data.name}</div>
</div>
);
})
const Hook = ()=>{
console.log('Hook render...')
const [count, setCount] = useState(0)
const [name, setName] = useState('rose')
const data = { name }
return(
<div>
<div>{count}</div>
<button onClick={()=>setCount(count+1)}>update count </button>
<Child data={data}/>
</div>
)
}
当点击按钮更新count的时候,Effect组件会render,执行到const data = {name}这一行代码会生成有新的内存地址的对象,那么就算带着memo的Child组件,也会跟着重新render, 尽管最后其实Child使用到的值没有改变,重复render。
使用useMemo进行缓存,仅在name更新时再重新触发子组件渲染。
const data = useMemo(()=>{
return { name };
},[name])
6. useCallback
useCallback 解决了函数未发生变化触发子组件更新的问题,
-
注意
- useMemo 是缓存值的
- useCallback 是缓存函数的
-
使用
const Hook = ()=>{
console.log('Hook render...')
const [count, setCount] = useState(0)
const [name, setName] = useState('rose')
const data = { name }
const handleChange = (name) => setName(name);
return(
<div>
<div>{count}</div>
<button onClick={()=>setCount(count+1)}>update count </button>
<Child data={data} onChange={handleChange}/>
</div>
)
}
7. useReducer
让函数式组件可以像类组件一样集中式管理状态。
- 可以使用reducer的场景:
- 如果你的state是一个数组或者对象
- 如果你的state变化很复杂,经常一个操作需要修改很多state
- 如果你希望构建自动化测试用例来保证程序的稳定性
- 如果你需要在深层子组件里面去修改一些状态(关于这点我们下篇文章会详细介绍)
- 如果你用应用程序比较大,希望UI和业务能够分开维护
const initState = {
name: '',
pwd: '',
isLoading: false,
error: '',
isLoggedIn: false,
}
function loginReducer(state, action) {
switch(action.type) {
case 'login':
return {
...state,
isLoading: true,
error: '',
}
case 'success':
return {
...state,
isLoggedIn: true,
isLoading: false,
}
case 'error':
return {
...state,
error: action.payload.error,
name: '',
pwd: '',
isLoading: false,
}
default:
return state;
}
}
function LoginPage() {
const [state, dispatch] = useReducer(loginReducer, initState);
const { name, pwd, isLoading, error, isLoggedIn } = state;
const login = (event) => {
event.preventDefault();
dispatch({ type: 'login' });
login({ name, pwd })
.then(() => {
dispatch({ type: 'success' });
})
.catch((error) => {
dispatch({
type: 'error'
payload: { error: error.message }
});
});
}
return (
// 返回页面JSX Element
)
}
自定义hook
使用react自带的基本hooks封装在业务中可复用的逻辑。
- 使用useXXX 的命名规范
- 遵循Hooks规则
1. useDebounceState:节流修改状态
import { useState, useMemo } from 'react';
export function debounce(fn, time) {
let timer: any = null;
return (...arg) => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, arg);
}, time);
};
}
export function useDebounceState(defaultValue, time = 300) {
const [value, setValue] = useState(defaultValue);
const handleChange = useMemo(() => debounce(setValue, time), [time]);
return [value, handleChange];
}
// 使用
export default function Index(){
const [ value , setValue ] = useDebounceState('', 300)
return <div>
{value}
<input value={value} onChange={(e)=>setValue(e.target.value)} />
</div>
}
2. useUserInfo:userId变化自动获取用户信息
import { useEffect, useState, countRef } from 'react';
const sleep = (time = 0) =>
new Promise((resolve: any) => setTimeout(() => resolve(), time));
const fetchUserInfo = async (id: string) => {
await sleep(400);
return {
id,
name: 'alan',
age: 18,
};
};
export const useUserInfo = (id: string) => {
const [userInfo, setUserInfo] = useState(null as any);
const hashRef = useRef('');
useEffect(() => {
async function run() {
const hash = Math.random().toString();
hashRef.current = hash;
const info = await fetchUserInfo(id);
if (hashRef.current !== hash) {
return;
}
setUserInfo(info);
}
run();
}, [id]);
return userInfo;
};