一、useReducer
1.基本用法
-
引入useReducer以及创建初始值
-
创建所有操作reducer(state,action)
-
传给useReducer,得到读取数据和修改数据的API
-
调用写({type:'操作类型'})
它是useState的替代,如果useState接受的参数复杂,推荐useReducer
import React, { useReducer } from "react";
import ReactDOM from "react-dom";
const initFormData = {
name: "",
age: 18,
nationality: "汉族"
};
function reducer(state, action) {
switch (action.type) {
case "patch":
return { ...state, ...action.formData };
case "reset":
return initFormData;
default:
throw new Error();
}
}
function App() {
//传给useReducer,得到读写数据的API
const [formData, dispatch] = useReducer(reducer, initFormData);
const onSubmit = () => {};
const onReset = () => {
dispatch({ type: "reset" });
};
return (
<form onSubmit={onSubmit} onReset={onReset}>
<div>
<label>
姓名
<input
value={formData.name}
onChange={e =>
dispatch({ type: "patch", formData: { name: e.target.value } })
}
/>
</label>
</div>
<div>
<label>
年龄
<input
value={formData.age}
onChange={e =>
dispatch({ type: "patch", formData: { age: e.target.value } })
}
/>
</label>
</div>
<div>
<label>
民族
<input
value={formData.nationality}
onChange={e =>
dispatch({
type: "patch",
formData: { nationality: e.target.value }
})
}
/>
</label>
</div>
<div>
<button type="submit">提交</button>
<button type="reset">重置</button>
</div>
<hr />
{JSON.stringify(formData)}
</form>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
效果:
2.模拟redux的功能
基本使用:
- 将数据集中到store对象
- 所有操作放到reducer
- 创建一个Context(读写接口可以传给子组件)
- 创建对数据的读写API
- 读写API放到Context
- Context.Provider将Context提供给所有组件
- 各个组件用useContext获取读写API
可以参考示例代码:codesandbox.io/s/priceless…
二、useEffect
作用:用于模拟生命周期DidMount和DidUpdate,如果同时存在多个useEffect,会按照出现次数执行
import React, { useState, useEffect } from 'react'
function LifeCycles() {
const [count, setCount] = useState(0)
// // 模拟 class 组件的 DidMount 和 DidUpdate
// useEffect(() => {
// console.log('在此发送一个 ajax 请求')
// })
// // 模拟 class 组件的 DidMount
// useEffect(() => {
// console.log('加载完了')
// }, []) // 第二个参数是 [] (不依赖于任何 state)
// // 模拟 class 组件的 DidUpdate
// useEffect(() => {
// console.log('更新了')
// }, [count, name]) // 第二个参数就是依赖的 state
// 模拟 class 组件的 DidMount
useEffect(() => {
let timerId = window.setInterval(() => {
console.log(Date.now())
}, 1000)
// 返回一个函数
// 模拟 WillUnMount
return () => {
window.clearInterval(timerId)
}
}, [])
function clickHandler() {
setCount(count + 1)
}
return <div>
<p>你点击了 {count} 次</p>
<button onClick={clickHandler}>点击</button>
</div>
}
export default LifeCycles
总结:
- 模拟componentDidMount - useEffect依赖[]
- 模拟componentDidUpdate - useEffect依赖[a,b]
- 模拟componentDidMount和componentDidUpdate - useEffect无依赖
- 模拟componentWillUnMount - useEffect中返回一个函数
useEffect让纯函数有了副作用
- 默认情况下,执行纯函数,输入参数,返回结果,无副作用
- 所谓副作用就是对函数之外造成影响,如设置全局定时任务
- 而组件需要副作用,所以需要useEffect
【注意】模拟WillUnMount,但不是说完全相等
import React, { useState, useEffect } from 'react'
function FriendStatus({ friendId }) {
const [status, setStatus] = useState(false)
// DidMount 和 DidUpdate
useEffect(() => {
console.log(`开始监听 ${friendId} 在线状态`)
// 【特别注意】
// 此处并不完全等同于 WillUnMount
// props 发生变化,即更新,也会执行结束监听
// 准确的说:返回的函数,会在下一次 effect 执行之前,被执行
return () => {
console.log(`结束监听 ${friendId} 在线状态`)
}
})
return <div>
好友 {friendId} 在线状态:{status.toString()}
</div>
}
export default FriendStatus
useEffect中返回函数fn:
- useEffect依赖为[],组件销毁执行fn,等于WillUnMount
- useEffect无依赖或者依赖[a,b],组件更新时执行fn
三、useLayoutEffect
- useEffect在浏览器渲染完成之后执行
- useLayoutEffect在浏览器渲染之前执行,如果执行的代码过多,会延长用户看到画面的时间
- 当你的useEffect里面的操作需要处理DOM,并且会改变页面的样式,就需要用useLayoutEffect
四、useMemo
1.React.memo
React会有多余的render,父组件的数据改变,虽然传给子组件的数据(Props)没变,但会再次渲染子组件
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () => {
setN(n + 1);
};
return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
</div>
<Child data={m}/>
</div>
);
}
function Child(props) {
console.log("child 执行了");
console.log('假设这里有大量代码')
return <div>child: {props.data}</div>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
可以看到父组件的n改变了,虽然传递给子组件的m没有变化,但仍然触发子组件的重新渲染
解决方式:使用React.memo包装子组件
...
function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () => {
setN(n + 1);
};
return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
</div>
<Child2 data={m}/>
</div>
);
}
function Child(props) {
console.log("child 执行了");
console.log('假设这里有大量代码')
return <div>child: {props.data}</div>;
}
const Child2 = React.memo(Child);
...
但是这种方法仍然存在问题:如果添加监听函数,APP组件运行再次执行,生成新的函数,新旧函数虽然功能相同,但地址不一样,触发重新渲染,此时子组件也会再次渲染
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () => {
setN(n + 1);
};
//添加函数,每次点击打印m的值
const onClickChild = () => {
console.log(m);
};
return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
</div>
<Child2 data={m} onClick={onClickChild} />
</div>
);
}
function Child(props) {
console.log("child 执行了");
console.log("假设这里有大量代码");
return <div onClick={props.onClick}>child: {props.data}</div>;
}
const Child2 = React.memo(Child);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
效果:
此时可以考虑搭配useMemo配合使用
2.useMemo和useCallback
- 第一个参数是
()=>value - 第二个参数是依赖
[m,n] - 只有当依赖变化时候,才会计算出新的value,如果依赖不变,那么就重用之前的value
- 如果你的value是一个函数,那么就要写成一个返回函数的函数
useMemo(()=>(x)=>console.log(x),[m])
这样就可以使用useCallback
usecallBack(x=>console.log(x),[m])
对上面的案例使用useMemo进行优化
import React, { useMemo } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [n, setN] = React.useState(0);
const [m, setM] = React.useState(0);
const onClick = () => {
setN(n + 1);
};
const onClick2 = () => {
setM(m + 1);
};
const onClickChild = useMemo(() => {
const fn = (div) => {
console.log("on click child, m: " + m);
console.log(div);
};
return fn;
}, [m]);
return (
<div className="App">
<div>
<button onClick={onClick}>update n {n}</button>
<button onClick={onClick2}>update m {m}</button>
</div>
<Child2 data={m} onClick={onClickChild} />
</div>
);
}
function Child(props) {
console.log("child 执行了");
console.log("假设这里有大量代码");
return (
<div onClick={(e) => props.onClick(e.target)}>child: {props.data}</div>
);
}
const Child2 = React.memo(Child);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
效果:
总结:
- React默认更新所有子组件
- class组件使用SCU和PureComponent做优化
- Hooks中使用useMemo,优化的原理相同
五、forwardRef
props是不支持传递ref的
如果一个函数组件需要接受传递过来的ref,那么需要把自己用forwarRef包装起来
import React, { useRef } from "react";
import ReactDOM from "react-dom";
function App() {
const inputRef = useRef(null);
const onClick = () => {
inputRef.current.focus();
};
return (
<div className="App">
<!--父组件向子组件传递Ref-->
<Button3 ref={inputRef}></Button3>
<button onClick={onClick}>父组件点击聚焦</button>
</div>
);
}
//子组件用forwardRef包裹起来,然后通过参数获取父组件传递过来的inputRef
const Button3 = React.forwardRef((props, inputRef) => {
const onClick = () => {
inputRef.current.focus();
};
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={onClick}>子组件点击聚焦</button>
</div>
);
});
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
效果:
六、useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
- ref:定义 current 对象的 ref
- createHandle:一个函数,返回值是一个对象,即这个 ref 的 current
- 对象 [deps]:即依赖列表,当监听的依赖发生变化,useImperativeHandle 才会重新将子组件的实例属性输出到父组件
可以理解为setRef,对传递过来的ref进行设置
import React, {
useRef,
useState,
useEffect,
useImperativeHandle,
createRef
} from "react";
import ReactDOM from "react-dom";
function App() {
const buttonRef = useRef(null);
useEffect(() => {
console.log(buttonRef.current);
});
return (
<div className="App">
<Button2 ref={buttonRef}>按钮</Button2>
<button
className="close"
onClick={() => {
console.log(buttonRef);
buttonRef.current.remove();
}}
>
remove
</button>
</div>
);
}
const Button2 = React.forwardRef((props, ref) => {
const realButton = createRef(null);
const setRef = useImperativeHandle;
setRef(ref, () => {
return {
remove: () => {
realButton.current.remove();
},
realButton: realButton
};
});
return <button ref={realButton} {...props} />;
});
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement)
可以理解为子组件通过useImperativeHandle内部设置真正的ref,然后通过useImperativeHandle设置假的ref给父组件引用
七、Hooks的踩坑
1.useState的初始化值,只有第一次有效
函数组件第一次执行的时候,会初始化state的值,如果组件再次执行(比如说数据变化),只恢复初始化的state的值(即不能通过初始化的方式来修改state的值),要修改state的值只能通过setState来修改state的值
2.依赖为[]的话,useEffect内部不能修改state的值
依赖为[],useEffect模拟的是DidMount,数据变化的时候不会重新执行effect函数
可以设置useRef来解决
import React, { useState, useRef, useEffect } from 'react'
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
function App() {
const [count, setCount] = useState(0)
// 模拟 DidMount
const countRef = useRef(0)
useEffect(() => {
console.log('useEffect...', count)
// 定时任务
const timer = setInterval(() => {
console.log('setInterval...', countRef.current)
// setCount(count + 1)
setCount(++countRef.current)
}, 1000)
// 清除定时任务
return () => clearTimeout(timer)
}, [])
// 依赖为 [] 时: re-render 不会重新执行 effect 函数
// 没有依赖:re-render 会重新执行 effect 函数
return <div>count: {count}</div>
}
ReactDOM.render(<App />, rootElement);
3.useEffect可能出现死循环
useEffect的依赖如果是数组或者对象,可能会造成死循环(引用类型,例如两个空对象或空数组之间是不相等的,所以依赖会一直在变化),所以要把数据或者对象中的属性拆出来放到依赖中