文章目录
Hook是什么
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
使用Hook的原因
- 高阶组件为了复用,导致代码层级复杂
- 生命周期的复杂
- 写成functional组件,无状态组件 ,因为需要状态,又改成了class,成本高
Hook使用规则
- 只能在函数外层调用 Hook,不要在循环、条件判断或者子函数中调用
- 只能在 React 的函数组件和自定义 Hook 中调用 Hook。不要在其他 JavaScript 函数中调用
useState(保持组件状态)
useState 用于在函数组件中调用给组件添加一些内部状态 state,正常情况下纯函数不能存在状态副作用,通过调用该 Hook 函数可以给函数组件注入状态 state
useState 唯一的参数就是初始 state,会返回当前状态和一个状态更新函数,并且 useState 返回的状态更新函数不会把新的 state 和旧的 state 进行合并,如需合并可使用 ES6 的对象结构语法进行手动合并
const [state, setstate] = useState(initialState)
import React, { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count - 1)}>-</button>
<input type="text" value={count} onChange={(e) => setCount(e.target.value)} />
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
等价class
useState 返回的状态类似于 class 组件在构造函数中定义 this.state,返回的状态更新函数类似于 class 组件的 this.setState
import React from 'react';
export default class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<button onClick={() => this.setState({ count: this.state.count - 1 })}>-</button>
<input type="text" value={this.state.count} onChange={(e) => this.setState({ count: e.target.value })} />
<button onClick={() => this.setState({ count: this.state.count + 1 })}>+</button>
</div>
);
}
}
函数式更新
如果新的 state 需要通过使用先前的 state 计算得出,可以往 setState 传递函数,该函数将接收先前的 state,并返回一个更新后的值
import React, { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0);
const lazyAdd = () => {
setTimeout(() => {
// 每次执行都会最新的state,而不是使用事件触发时的state
setCount(count => count + 1);
}, 3000);
}
return (
<div>
<p>the count now is {count}</p>
<button onClick={() => setCount(count + 1)}>add</button>
<button onClick={lazyAdd}>lazyAdd</button>
</div>
);
}
useEffect,useLayoutEffect
useEffect Hook 的使用则是用于完成此类副作用操作。useEffect 接收一个包含命令式、且可能有副作用代码的函数
useEffect函数会在浏览器完成布局和绘制之后,下一次重新渲染之前执行,保证不会阻塞浏览器对屏幕的更新
useEffect(处理副作用)和useLayoutEffect (同步执行副作用)
不要对 Dependencies 撒谎, 如果你明明使用了某个变量,却没有申明在依赖中,你等于向 React 撒了谎,后果就是,当依赖的变量改变时,useEffect 也不会再次执行, eslint会报警告
Preview页面改造成函数式组件,在路径上从id=1切换到id=2也会自动重新加载,比class组件方便
let id = props.match.params.myid
useEffect(()=>{
axios.get(`/articles/${id}`).then(res => {
settitle(res.data.title)
setcontent(res.data.content)
setcategory(res.data.category)
})
},[id])
useEffect和useLayoutEffect有什么区别?
简单来说就是调用时机不同,useLayoutEffect 和原来componentDidMount &react完成DOM更新后马上同调用的代码,会阻塞页面渲染。而componentDidUpdate 一致,在useEffect 是会在整个页面渲染完才会调用的
代码。
使用时如果想避免页面抖动(在放在useEffect 里修改DOM很有可能出现)的话,可以把需要操作DOM的代码useLayoutEffect 里。在这里做点dom操作,这些dom修改会和 react 做出的更改一起被一次性渲染到屏幕上,只有一次回流、重绘的代价。
Function Component 不存在生命周期,所以不要把 Class Component 的生命周期概念搬过来试图对号入座
useEffect(() => {
//effect
return () => {
//cleanup
};
}, [依赖的状态;空数组,表示不依赖])
如何使用
import React, { useState, useEffect } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
// useEffect 内的回调函数会在初次渲染后和更新完成后执行
// 相当于 componentDidMount 和 componentDidUpdate
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>count now is {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
等价的class
useEffect Hook 函数执行时机类似于 class 组件的
componentDidMount、componentDidUpdate 生命周期,不同的是传给 useEffect
的函数会在浏览器完成布局和绘制之后进行异步执行
import React from 'react';
export default class Counter extends React.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>count now is {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>+</button>
</div>
);
}
}
如何清除effrct
通常情况下,组件卸载时需要清除 effect 创建的副作用操作,useEffect Hook函数可以返回一个清除函数,清除函数会在组件卸载前执行。组件在多次渲染中都会在执行下一个 effect 之前,执行该函数进行清除上一个effect
清除函数的执行时机类似于 class 组件 componentDidUnmount 生命周期,这的话使用 useEffect函数可以将组件中互相关联的部分拆分成更小的函数,防止遗忘导致不必要的内存泄漏
import React, { useState, useEffect } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('start an interval timer')
const timer = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
// 返回一个清除函数,在组件卸载前和下一个effect执行前执行
return () => {
console.log('destroy effect');
clearInterval(timer);
};
}, []);
return (
<div>
<p>count now is {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
useCallback(记忆函数)
防止因为组件重新渲染,导致方法被重新创建,起到缓存作用; 只有第二个参数变化了,才重新声明一次
useCallback
用于创建返回一个回调函数,该回调函数只会在某个依赖项发生改变时才会更新,可以把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染的子组件,在props 属性相同情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果
var handleClick = useCallback(()=>{
console.log(name)
},[name])
<button onClick={()=>handleClick()}>hello</button>
//只有name改变后, 这个函数才会重新声明一次,
//如果传入空数组, 那么就是第一次创建后就被缓存, 如果name后期改变了,拿到的还是老的name。
//如果不传第二个参数,每次都会重新声明一次,拿到的就是最新的name.
import React, { useState, useCallback } from 'react';
function SubmitButton(props) {
const { onButtonClick, children } = props;
console.log(`${children} updated`);
return (
<button onClick={onButtonClick}>{children}</button>
);
}
// 使用 React.memo 检查 props 变更,复用最近一次渲染结果
SubmitButton = React.memo(submitButton);
export default function CallbackForm() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const handleAdd1 = () => {
setCount1(count1 + 1);
}
// 调用 useCallback 返回一个 memoized 回调,该回调在依赖项更新时才会更新
const handleAdd2 = useCallback(() => {
setCount2(count2 + 1);
}, [count2]);
return (
<>
<div>
<p>count1: {count1}</p>
<SubmitButton onButtonClick={handleAdd1}>button1</SubmitButton>
</div>
<div>
<p>count2: {count2}</p>
<SubmitButton onButtonClick={handleAdd2}>button2</SubmitButton>
</div>
</>
)
}
useCallback(fn, deps) 相当于 useMemo(() => fn, deps),以上 useCallback 可替换成 useMemo 结果如下:
const handleAdd2 = useMemo(() => {
return () => setCount2(count2 + 1);
}, [count2]);
useMemo 记忆组件
useCallback 的功能完全可以由 useMemo 所取代,如果你想通过使用 useMemo 返回一个记忆函数也是完全可以的。
useCallback 不会执行第一个参数函数,而是将它返回给你,而 useMemo
会执行第一个函数并且将函数执行结果返回给你。所以在前面的例子中,可以返回 handleClick 来达到存储函数的目的。 所以
useCallback 常用记忆事件函数,生成记忆后的事件函数并传递给子组件使用。而 useMemo
更适合经过函数计算得到一个确定的值,比如记忆组件。
传入 useMemo 的函数会在渲染期间执行,不要在这个函数内部执行与渲染无关的操作
如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值
import React, { useState, useMemo } from 'react';
function counterText({ countInfo }) {
console.log(`${countInfo.name} updated`);
return (
<p>{countInfo.name}: {countInfo.number}</p>
);
}
// // 使用 React.memo 检查 props 变更,复用最近一次渲染结果
const CounterText = React.memo(counterText);
export default function Counter() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const countInfo1 = {
name: 'count1',
number: count1
};
// 使用 useMemo 缓存最近一次计算结果,会在依赖项改变时才重新计算
const countInfo2 = useMemo(() => ({
name: 'count2',
number: count2
}), [count2]);
return (
<>
<div>
<CounterText countInfo={countInfo1} />
<button onClick={() => setCount1(count1 + 1)}>Add count1</button>
</div>
<div>
<CounterText countInfo={countInfo2} />
<button onClick={() => setCount2(count2 + 1)}>Add count2</button>
</div>
</>
);
}
useRef(保存引用值)
useRef 用于返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue) useRef
创建的 ref 对象就是一个普通的 JavaScript 对象,而 useRef() 和自建一个 {current: …}
对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象
const myswiper = useRef(null);
<Swiper ref={myswiper}/>
useContext
Context 提供了一个无需为每层组件手动添加 props ,就能在组件树间进行数据传递的方法,useContext
用于函数组件中订阅上层 context 的变更,可以获取上层 context 传递的 value prop 值 useContext
接收一个 context 对象(React.createContext的返回值)并返回 context 的当前值,当前的 context
值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
const value = useContext(MyContext);
如何使用
import React, { useContext, useState } from 'react';
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
// 为当前 theme 创建一个 context
const ThemeContext = React.createContext();
export default function Toolbar(props) {
const [theme, setTheme] = useState(themes.dark);
const toggleTheme = () => {
setTheme(currentTheme => (
currentTheme === themes.dark
? themes.light
: themes.dark
));
};
return (
// 使用 Provider 将当前 props.value 传递给内部组件
<ThemeContext.Provider value={{theme, toggleTheme}}>
<ThemeButton />
</ThemeContext.Provider>
);
}
function ThemeButton() {
// 通过 useContext 获取当前 context 值
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button style={{background: theme.background, color: theme.foreground }} onClick={toggleTheme}>
Change the button's theme
</button>
);
}
class组件使用
import React from 'react';
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function ThemeButton() {
return (
<ThemeContext.Consumer>
{
({theme, toggleTheme}) => (
<button style={{background: theme.background, color: theme.foreground }} onClick={toggleTheme}>
Change the button's theme
</button>
)
}
</ThemeContext.Consumer>
);
}
export default class Toolbar extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: themes.light
};
this.toggleTheme = this.toggleTheme.bind(this);
}
toggleTheme() {
this.setState(state => ({
theme:
state.theme === themes.dark
? themes.light
: themes.dark
}));
}
render() {
return (
<ThemeContext.Provider value={{ theme: this.state.theme, toggleTheme: this.toggleTheme }}>
<ThemeButton />
</ThemeContext.Provider>
)
}
}
useReducer
useReducer 作为 useState 的代替方案,在某些场景下使用更加适合,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。
const [state, dispatch] = useReducer(reducer, initialArg, init);
如何使用:
import React, { useReducer } from 'react'
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();
}
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
初始化state
// 方式1
const [state, dispatch] = useReducer(
reducer,
{count: initialCount}
);
// 方式2
function init(initialClunt) {
return {count: initialClunt};
}
const [state, dispatch] = useReducer(reducer, initialCount, init);
自定义Hook
通过自定义 Hook,可以将组件逻辑提取到可重用的函数中,在 Hook 特性之前,React 中有两种流行的方式来共享组件之间的状态逻辑:render props和高阶组件,但此类解决方案会导致组件树的层级冗余等问题。而自定义Hook 的使用可以很好的解决此类问题
创建自定义 Hook
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。以下就是实时获取鼠标位置的自定义 Hook 实现:
import { useEffect, useState } from "react"
export const useMouse = () => {
const [position, setPosition] = useState({
x: null,
y: null
});
useEffect(() => {
const moveHandler = (e) => {
setPosition({
x: e.screenX,
y: e.screenY
});
};
document.addEventListener('mousemove', moveHandler);
return () => {
document.removeEventListener('mousemove', moveHandler);
};
}, []);
return position;
}
使用
import React from 'react';
import { useMouse } from '../hooks/useMouse';
export default function MouseMove() {
const { x, y } = useMouse();
return (
<>
<p>Move mouse to see changes</p>
<p>x position: {x}</p>
<p>y position: {y}</p>
</>
);
}