文章目录
- Hook
- useState
- useEffect
- useRef
- useReducer
- useContext
- 自定义Hook
- Hook使用规则
Hook
Hooks 是react 16.8版本新增的一项特性,可以在不编写class的情况下使用state以及其他的react特性
useState
用于引入类组件的state特性
import React, {useState, useEffect} from 'react'
export default function Counter() {
// 使用解构接收useState()的值
const [count, setCount] = useState(0) // 使用useState创建一个默认值,用解构接收结果,第一个接收默认值,第二个接收更改state的方法
// 监听类似于生命周期中的componentDidMout() 在初次挂载和状态更新时会执行 需要传入一个回调函数
useEffect(() => {
document.title = `当前数量为${count}` // 模板字符
})
return (
<button onClick={() => {setCount(count - 1) }}>-</button>
<span>{count}</span>
<button onClick={() => {setCount(count + 1) }}>+</button>
</div>
)
}
修改 state 的方法
const [count, setCount] = useState(0)
...
// 第一种用法,直接接收一个新的state结果
setCount(count + 1)
// 第二种用法,接收一个方法,该方法返回一个新的state结果
setCount(() => {
// 其他操作
return count + 1
})
修改一个组件的state值会令组件重新渲染,但需要注意的是,如果对方是一个引用类型,修改引用类型内部的值,实际上组件对state这个引用类型的引用地址是不会发生改变的,因此会判断为state没有发生改变,不会出现渲染组件
// 修改obj内部的count时,即使内部已经发生改变,但是state的obj地址没有发生改变,因此不会重新渲染
import React, { useState } from 'react';
const Demo = () => {
const [obj, setCout] = useState({count:0})
const [num, setNum] = useState(0) // num的值发生变化时会重新渲染
const changeState = () => {
return setCout(() => {
obj.count += 1
return obj
})
}
return(
<div>
<div>当前count的值为:{obj.count}</div>
<button onClick={changeState}>自增按钮</button>
<button onClick={() => setNum(num + 1)}>更新渲染按钮</button>
</div>
)
}
export default Demo
假如一个组件有多个状态值
function ExampleWithManyStates() {
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}
useEffect
用于引入类组件的生命周期特性
可以直接在函数组件内处理生命周期事件,它是 componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合(组件渲染,更新,销毁)
第一个参数为一个函数,它会在组件渲染时执行函数内的内容,如果该函数A返回了一个函数B,则会在组件销毁前执行B函数(如果函数B想声明再返回一个函数C,会报错)
// LifecycleDemo.tsx
import React, {useEffect} from 'react'
const LifecycleDemo = () => {
useEffect(() => {
console.log('渲染')
return () => {
console.log('销毁')
}
},)
return <div>LifecycleDemo组件</div>
}
export default LifecycleDemo
// App.tsx
import React, {useState} from "react";
import LifecycleDemo from "./LifecycleDemo";
function App() {
// 使用一个状态来控制组件的渲染和销毁
const [isShow, setIsShow] = useState(true)
const toggle = () => {
setIsShow(!isShow)
}
return (
<div className="App">
<button onClick={toggle}>控制组件渲染/销毁开关</button>
{isShow && <LifecycleDemo />} {/* 如果isShow为true则渲染该组件的简易写法 */}
</div>
);
}
export default App;
上面代码的效果是:
在组件渲染时,打印:'渲染'
在组件销毁时,打印:'销毁'
此时会存在一个现象:当组件并非渲染和销毁阶段,而是更新阶段时,则会打印:销毁 → 渲染
,这说明 useEffect
在每次渲染后运行(默认情况下),并且可以选择在再次运行之前自行清理
注:这里的更新指的是所有能触发 LifecycleDemo 组件重新渲染的情况,如上面的代码,如果App的某个state状态发生改变,会导致整个 App 组件重新渲染,因此也会导致 LifecycleDemo 重新渲染(有时候会忽略这点)
因此与其将 useEffect
看作一个函数来完成3个独立生命周期的工作,不如将它简单地看作是在渲染之后执行副作用的一种方式,包括在每次渲染之前和卸载之前咱们希望执行的需要清理的东西
阻止每次重新渲染都会执行useEffect
useEffect( )
的第二个参数为可选参数,类型为一个数组,它的作用是比较上一次作用的状态值,如果两次状态值没有发生改变,则不会执行本次的函数,实现了性能优化
这个值一般指 props 和 state,因为这个两个发生变化会重新渲染导致触发 useEffect,当然存一个能跨周期储存的普通变量也可以
const [value, setValue] = useState(xxx);
useEffect(() => {
...
}, [value]) // 仅在value更改时执行
这个数组可以传递任意多项,任意一项的数组发生改变时,都会执行 useEffect
const [value, setValue] = useState(xxx);
useEffect(() => {
...
}, [value1, value2, value3 ...]) // 仅在value更改时执行
绑定事件时需要在更新组件时销毁事件
这点可以扩展,不只是时间,如计时器这种也是需要清除的
import React, { useState, useEffect } from 'react';
const Demo: React.FC = () => {
const [positions, setPositions] = useState({x:0, y:0})
useEffect(() => {
const updataMouse = (e: MouseEvent) => {
console.log('inner')
setPositions({x: e.clientX, y: e.clientY})
}
document.addEventListener('click', updataMouse)
// 如果不清除事件,则每次更新state时都会生成新的事件,会指数级增长,大大影响性能
return () => {
document.removeEventListener('click', updataMouse)
}
})
return(
<p>X: {positions.x}, Y:{positions.y}</p>
)
}
export default Demo
执行顺序
import React, { useState, useEffect, useRef } from 'react';
const Demo = () => {
const [count, setCout] = useState(0);
useEffect(() => {
console.log('执行useEffect')
ref.current = count
})
Promise.resolve().then(() => {
console.log('执行Promise')
})
setTimeout(() => {
console.log('执行setTimeout')
})
return(
<div>
<button onClick={() => setCout(count + 1) }>自增按钮</button>
</div>
)
}
export default Demo
打印顺序:执行Promise → 执行useEffect → 执行setTimeout
可以看出 useEffect
是在组件挂载完毕后才执行的异步函数(即晚于return内组件的渲染),晚于 Promise
执行,早于 setTimeout
执行
由于是渲染后才执行,因此可以利用这个特性,在渲染更新后还能拿到上一次渲染前的数据
import React, { useState, useEffect, useRef } from 'react';
const Demo = () => {
const [count, setCout] = useState(0);
const ref = useRef<number>()
useEffect(() => {
console.log('执行useEffect')
ref.current = count
})
return(
<div>
<div>前一个count的值为:{ref.current}</div>
<button onClick={() => setCout(count + 1) }>自增按钮</button>
</div>
)
}
export default Demo
原理:因为组件渲染后才执行,因此渲染 <div>前一个count的值为:{ref.current}</div>
时,拿到的 ref.current
是未执行 useEffect
方法时的数据
仅在挂载和卸载的时候执行
如果想执行只运行一次的 effect
(仅在组件挂载和卸载时执行),可以传递一个空数组 []
作为第二个参数,相当于 effect
不依赖于 props
或 state
中的任何值,所以它永远都不需要重复执行
const [value, setValue] = useState(xxx);
useEffect(() => {
...
}, []) // 不会在更新组件时执行
使用useEffect 获取数据并显示
在类组件中,一般通过将此代码放在 componentDidMount
方法中实现,函数式组件中,可以使用 useEffect hook
来实现
// 从Reddit网站获取帖子并显示它们的组件
import React, {useEffect, useState} from 'react'
const LifecycleDemo = () => {
const [posts, setPosts] = useState([]);
useEffect(() => {
// 在useEffect使用异步时应该这么写,即声明后立即调用,不能只声明不调用
(async () => {
const res = await fetch("https://www.reddit.com/r/reactjs.json")
const json = await res.json();
setPosts(json.data.children.map(c => c.data));
})()
})
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default LifecycleDemo
控制获取文章的更新
import React, {useEffect, useState} from 'react'
interface IHelloProps {
subreddit: string
}
// 假设从上级获取到请求的具体的路径
const LifecycleDemo:React.FC<IHelloProps> = ({subreddit}) => {
const [posts, setPosts] = useState([]);
useEffect(() => {
(async () => {
const res = await fetch(`https://www.reddit.com/r/${subreddit}.json`)
const json = await res.json();
setPosts(json.data.children.map(c => c.data));
})()
}, [subreddit]) // 每次subreddit发生更改时从新获取文章,否则不会执行effect方法重新获取文章
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default LifecycleDemo
useRef
作用:
它像一个变量, 类似于 this , 可以存放任何东西,和 createRef
一样可用来获取标签的 dom,也可以用来保存跨周期的数据信息
// 点击按钮 state 发生变化,然后每次重新渲染 Demo 组件时, 又会执行一遍 `let num = 0`,num 又会被重新赋值为 0,因此 num 会一直为 0,即一般在组件内无法跨周期进行保存
import React, { useState, useEffect } from 'react';
const Demo: React.FC = () => {
const [numState, setNumState] = useState(0)
let num = 0
function change() {
num += 1
setNumState(numState + 1)
}
console.log(num) // 始终显示0
console.log(numState) // 会持续自增
return(
<div>
<button onClick={change}></button>
</div>
)
}
export default Demo
使用:
//在ts中使用useRef方法需要指定类型,否则会报错
const ref = useRef(null) // 方法一:指定默认值,编译器自动类型推论指定类型
const ref = useRef<any>() // 方法二:指定具体泛型类型
// 用useRef创建了couterRef对象,并将其赋给了button的ref属性,这样就能通过访问couterRef.current就可以访问到button对应的DOM对象
import React, {useRef} from 'react'
const eve =(ref:any) => {
return () => {
ref.current.disabled =true
}
}
const Demo = () => {
const buttonRef = useRef(null) // 注意,如果这里是使用typescript来写的话要传入一个null值,否则类型报错
return (
<button ref={buttonRef} onClick={eve(buttonRef)}>button</button>
)
}
export default Demo
与createRef的区别:
createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用,由于这个特性,因此修改它的值不会触发重新渲染
即:useRef.current
一旦被赋值,在更新渲染时,这个值不会被消除,但是 createRef.current
会在更新阶段时重新渲染并重新绑定值
import React, { useState, useRef, createRef } from 'react';
const Demo = () => {
const [renderIndex, setRenderIndex] = useState(1)
const refFromUseRef = useRef<any>()
const refFromCreateRef = createRef<any>()
// 测试每次更新渲染是否会丢失其useRef.current值
if(!refFromUseRef.current) {
console.log('useRef被渲染了')
}
// 测试每次更新渲染是否会丢失其createRef.current值
if(!refFromCreateRef.current) {
console.log('createRef被渲染了')
}
return(
<>
<div ref={refFromCreateRef}>测试createRef</div>
<div ref={refFromUseRef}>测试UseRef</div>
{/* 通过更改state值让组件更新数据重新渲染 */}
<button onClick={()=> setRenderIndex(renderIndex + 1)}>测试按钮</button>
</>
)
}
export default Demo
// 输出结果:
// 首次渲染时会打印 `useRef被渲染了`和 `createRef被渲染了`
// 更新渲染时只会打印 `createRef被渲染了`
useRef.current
的值可以被赋予任意的值而不是一定是dom,createRef.current
的值仅在通过标签 ref
获得,且是只读的,不可更改
const refFromUseRef = useRef<any>()
const refFromCreateRef = createRef<any>()
refFromUseRef.current = 'jack' // 成立
refFromCreateRef.current = 'mary' // 报错
因此 useRef
的特性为:可以存储数据,且不会在更新渲染时丢失这些数据
使用场景:
一:
import React, { useState } from 'react';
const Demo = () => {
const [count, setCout] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert(count)
},3000)
}
return(
<div>
<div>当前count的值为:{count}</div>
<button onClick={() => setCout(count + 1) }>自增按钮</button>
<button onClick={handleAlertClick}>弹窗按钮</button>
</div>
)
}
export default Demo
- 代码功能:点击自增按钮实现
count
增加,点击弹窗按钮延时3秒显示当前的count
- 存在问题:当点击弹窗按钮后不再次点击自增按钮时,延时后能显示和当前
count
一样的数值,但是当点击弹窗按钮后再继续点击自增按钮时,延时后弹窗的数值和当前的count
是不能对应上的 - 原因:React 会重新渲染组件, 每一次渲染都会拿到独立的 count 状态, 并重新渲染一个 handleAlertClick 函数. 每一个 handleAlertClick 里面都有它自己的 count
可以理解为:按钮事件在点击时就已经确定,开始事件时会将当前生命周期(发生更新渲染前)的state和其他数据传入(如果有用到这些数据)
如果不使用 state 而使用一个普通的变量来进行传值,是可以获取到实时的数值的,原因是事件拿到的是当前生命周期的数据,而不触发更新渲染时,延时完执行时也能做出拿到当前的数据值,如下代码所示:
import React, { useState } from 'react';
const Demo = () => {
let count = 0
function handleAlertClick() {
setTimeout(() => {
alert(count)
},3000)
}
function changeNum() {
count += 1
console.log(count)
}
return(
<div>
<button onClick={changeNum}>自增按钮</button>
<button onClick={handleAlertClick}>弹窗按钮</button>
</div>
)
}
export default Demo
使用 useRef 解决问题:
import React, { useState, useEffect, useRef } from 'react';
const Demo = () => {
const [count, setCout] = useState(0);
const latestCount = useRef(count)
useEffect(() => {
latestCount.current = count
})
function handleAlertClick() {
setTimeout(() => {
alert(latestCount.current)
},3000)
}
return(
<div>
<div>当前count的值为:{count}</div>
<button onClick={() => setCout(count + 1) }>自增按钮</button>
<button onClick={handleAlertClick}>弹窗按钮</button>
</div>
)
}
export default Demo
注意的是如果state是一个对象,事件使用的是这个对象里的数据,在触发事件时传递的只是这个 state 的引用地址,而非对象内部具体的值,相当于只传 state 的栈数据不传堆数据
useRef 每次都会返回同一个引用,所以每一次渲染点击事件拿到的 state 都是一个引用地址,等到需要取数据的时候再从这个引用地址里去取数据,因此它可以达到实时的效果
这里能获取到当前count值而非上一个原因是点击事件执行时,异步useEffect函数已完成count的赋值
可用引用类型的方式写:功能是一样的,也能获取到实时的数据,原理一样
import React, { useState } from 'react';
const Demo = () => {
const [obj, setCout] = useState({count:0});
const [num, setNum] = useState(0)
function handleAlertClick() {
setTimeout(() => {
alert(obj.count)
},3000)
}
// 点击自增按钮同时更改obj.count 和 num
function changeState(){
setCout(() => {
obj.count += 1
return obj
})
setNum(num + 1)
}
return(
<div>
<div>当前count的值为:{num}</div>
<button onClick={changeState}>自增按钮</button>
<button onClick={handleAlertClick}>弹窗按钮</button>
</div>
)
}
export default Demo
二:
渲染周期之间共享数据的存储
state不能存储跨渲染周期的组件,因为state的参数每一次保存都会触发组件的重渲染
import React, { useState, useEffect, useRef } from 'react';
const Demo = () => {
const [count, setCount] = useState(0);
// 把定时器设置成全局变量使用useRef挂载到current上
const timer = useRef<any>();
// 首次加载useEffect方法执行一次设置定时器
useEffect(() => {
timer.current = setInterval(() => {
setCount(count => count + 1);
}, 1000);
}, []);
// count每次更新都会执行这个副作用,当count > 5时,清除定时器
useEffect(() => {
if (count > 5) {
clearInterval(timer.current);
}
});
return <h1>count: {count}</h1>;
}
export default Demo
useReducer
引入 Reducer 功能
const [state, dispatch] = useReducer(reducer, initialState);
import React, {useReducer} from 'react'
// reducer
const myReducer = (state, action) => {
switch(action.type) {
case('countUp'):
return {
...state,
count: state.count + 1
}
default:
return state;
}
}
// 组件
function App() {
const [state, dispatch] = useReducer(myReducer, { count: 0 });
return (
<div className="App">
<button onClick={() => dispatch({ type: 'countUp' })}>
+1
</button>
<p>Count: {state.count}</p>
</div>
);
}
useContext
和 Context 差不多,还是要使用 createContext()
createContext()返回一个对象,对象里有Provider和Consumer两个属性,前者用于提供状态,后者用于接收状态
可传递一个参数作为默认的Provider值,当向上层组件找不到Provider值时则使用默认值
useContext()
接收的参数为 createContext()
返回的对象,该方法的返回值为 Provider 传递的 value 值
// App.tsx
import React, { createContext } from 'react';
import Demo from './Demo'
interface ITemeProps {
[key: string]: {color: string; background: string;}
}
const themes: ITemeProps = {
light: {
color: '#000',
background: '#eee'
},
dark: {
color: '#fff',
background: '#222'
}
}
export const ThemeContext = createContext(themes.light)
const App: React.FC = () => {
return (
<div>
{/* 使用Provider的value传递值,需要将要传递数据的组件包在内 */}
{/* 子组件使用数据时,只需要useContext()去接受createContext()返回的对象便可拿到传递的value值 */}
<ThemeContext.Provider value={themes.light} >
<Demo />
</ThemeContext.Provider>
</div>
)
}
export default App
// Demo.tsx
import React, { useState, useEffect, useContext} from 'react';
import {ThemeContext} from './App'
const Demo:React.FC = () => {
const theme = useContext(ThemeContext)
// 拿到传递的value值
const style = {
background: theme.background,
color: theme.color
}
return <div style={style}></div>
}
export default Demo
自定义Hook
复用逻辑代码
- 是一个函数式组件,使用
use
开头命名表示是一个 hook - 返回值不是 html 标签,而是给其他组件进行使用的数据
import { useState, useEffect } from 'react';
const useMousePostion = () => {
const [positions, setPositions] = useState({x:0, y:0})
useEffect(() => {
const updataMouse = (e: MouseEvent) => {
console.log('inner')
setPositions({x: e.clientX, y: e.clientY})
}
document.addEventListener('click', updataMouse)
return () => {
document.removeEventListener('click', updataMouse)
}
},[])
return positions
}
export default useMousePostion
import useMousePostion from './useMousePostion'
const App:React.FC = () => {
const positions = useMousePostion()
return(
<p>
{positions.x, positions.y}
</p>
)
}
hook解决的问题
可用 hook 去代替高阶组件去为组件添加要多次使用的额外功能
高阶组件缺点:代码沉重(如每次需要返回新的无用 html 标签);代码可读性不高等
▼例子
设置一个可复用的请求 loading 功能,在请求开始时设置 loading 状态为 true,
import React, { useState, useEffect } from 'react';
import axios from 'axios'
const useURLLoader = (url: string, deps: any[] = []) => {
const [data, setData] = useState<any>(null)
const [loading, setLoading] = useState(false)
useEffect(() => {
setLoading(true)
axios.get(url).then(result => {
setData(result.data)
setLoading(false)
})
}, deps)
return [data, loading]
}
export default useURLLoader
给组件复用:不像高阶函数那样需要给组件套方法导出,直接拿过来用即可
import useURLLoader from './useURLLoader'
const App: React.FC = () => {
const [isRequest, setIsRequest] = useState(false)
const [data, loading] = useURLLoader('https://www.baidu.com', [isRequest])
return (
<div>
<button onclick={() => setIsRequest(!isRequest)}>点击重新请求数据</button>
{
loading
?
<div>获取的数据为{data}</div>
:
<div>Loading...</div>
}
</divdiv>
)
}
Hook使用规则
- 只在最顶层使用Hook
即不在条件,循环,函数嵌套中使用hook,确保hook在函数组件的最顶层调用,这样能确保hook在渲染中能按相同的顺序调用 - 只在 React 函数中调用 Hook