React Hook详解
useState
使用状态
- const [n,setN] = React.useState(0)
- const [user,setUser] = React.useState({name:’F’})
注意事项1:不可局部更新
- 如果state是一个对象,能否部分setState?答案是不行。
- 因为setState不会自动合并属性,useReducer也不会合并属性。
注意事项2:地址要变
- setState(obj) 如果obj地址不变,那么React就会认为数据没有变化
useState接收函数
const [state,setState] = useState(()=>{
return initialState
})
该函数返回初识state,且只执行一次
setState 接收函数
- set(i => i+1),什么时候用这种方式
import React, {useState} from "react";
import ReactDOM from "react-dom";
function App() {
const [n, setN] = useState(0)
const onClick = ()=>{
setN(n+1)
setN(n+1) // 你会发现 n 不能加 2
// setN(i=>i+1) //应优先使用此方式
// setN(i=>i+1)
}
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>+2</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
useReducer
用来践行 Flux/Redux 思想
- 创建初始值initialState
- 创建所有操作reducer(state,action)
- 传给useReducer,得到读和写API
- 调用写({type:’操作类型‘})
总的来说useReducer 是 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('type不正确');
}
}
function App() {
const [formData, dispatch] = useReducer(reducer, initFormData);
// const patch = (key, value)=>{
// dispatch({ type: "patch", formData: { [key]: value } })
// }
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);
如何代替Redu
步骤
- 将数据集中在一个store对象
- 将所有操作集中在reducer
- 创建一个context
- 创建对数据的读写API
- 将第四步的内容放到第三步的Context
- 用Context.Provider 将Context提供给所有组件
- 各个组件用 useContext获取读写API
import React, { useReducer, useContext, useEffect } from "react";
import ReactDOM from "react-dom";
const store = {
user: null,
books: null,
movies: null
};
function reducer(state, action) {
switch (action.type) {
case "setUser":
return { ...state, user: action.user };
case "setBooks":
return { ...state, books: action.books };
case "setMovies":
return { ...state, movies: action.movies };
default:
throw new Error();
}
}
const Context = React.createContext(null);
function App() {
const [state, dispatch] = useReducer(reducer, store);
const api = { state, dispatch };
return (
<Context.Provider value={api}>
<User />
<hr />
<Books />
<Movies />
</Context.Provider>
);
}
function User() {
const { state, dispatch } = useContext(Context);
useEffect(() => {
ajax("/user").then(user => {
dispatch({ type: "setUser", user: user });
});
}, []);
return (
<div>
<h1>个人信息</h1>
<div>name: {state.user ? state.user.name : ""}</div>
</div>
);
}
function Books() {
const { state, dispatch } = useContext(Context);
useEffect(() => {
ajax("/books").then(books => {
dispatch({ type: "setBooks", books: books });
});
}, []);
return (
<div>
<h1>我的书籍</h1>
<ol>
{state.books ? state.books.map(book => <li key={book.id}>{book.name}</li>) : "加载中"}
</ol>
</div>
);
}
function Movies() {
const { state, dispatch } = useContext(Context);
useEffect(() => {
ajax("/movies").then(movies => {
dispatch({ type: "setMovies", movies: movies });
});
}, []);
return (
<div>
<h1>我的电影</h1>
<ol>
{state.movies
? state.movies.map(movie => <li key={movie.id}>{movie.name}</li>)
: "加载中"}
</ol>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
// 帮助函数
// 假 ajax
// 两秒钟后,根据 path 返回一个对象,必定成功不会失败
function ajax(path) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (path === "/user") {
resolve({
id: 1,
name: "Frank"
});
} else if (path === "/books") {
resolve([
{
id: 1,
name: "JavaScript 高级程序设计"
},
{
id: 2,
name: "JavaScript 精粹"
}
]);
} else if (path === "/movies") {
resolve([
{
id: 1,
name: "爱在黎明破晓前"
},
{
id: 2,
name: "恋恋笔记本"
}
]);
}
}, 2000);
});
}
useContext
上下文
- 全局变量是全局的上下文
- 上下文是局部的全局变量
使用方法
- 使用C=createContext(initial) 创建上下文
- 使用 (C.provider)圈定作用域
- 在作用域内使用useContext(C)来使用上下文
注意事项
- useContext不是响应式的,你再一个模块将C里面的值改变,另一个模块不会感知到这个变化
useEffect
副作用
- 对环境的改变即为副作用,如修改document.title
- 但是不一定要把副作用放在useEffect里面(每次render之后执行)
用途
- 作为componentDidMount使用,[]作第二个参数
- 作为componentDidUpdate使用,可指定依赖
- 作为componentWillUnmount使用,通过return,以上三种用途可同时存在
特点
- 如果存在多个useEffect,会按照出现次序执行
useLayoutEffect
布局副作用
- useEffect在浏览器渲染完成后执行
- useLayoutEffect在浏览器渲染前执行
import React, {useState, useRef, useLayoutEffect, useEffect} from "react";
import ReactDOM from "react-dom";
function App() {
const [n, setN] = useState(0)
const time = useRef(null)
const onClick = ()=>{
setN(i=>i+1)
time.current = performance.now()
}
useLayoutEffect(()=>{ // 改成 useEffect 试试
if(time.current){
console.log(performance.now() - time.current)
}
})
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>Click</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
特点
- useLayoutEffect 总是比 useEffect先执行
- useLayoutEffect里的任务最后影响了Layout
为了用户体验,优先使用useEffect(优先渲染)
useMemo
特点
- 第一个参数是()=>value
- 第二个参数是依赖[m,n]
- 只有依赖变化时,才会计算出新的value
- 如果依赖不变,那么就重用之前的value
- 类似于Vue2 的computed,使得一些函数可被重用。
注意
- 如果value是个函数,那么就要写成useMemo(()=> (x) => console.log(x))
- 这是一个返回函数的函数
- 有点难用,于是React还提供了一个useCallback
useCallback
用法
- useCallback(x => log(x),[m]) 等价于 useMemo( () => x => console.log(x), [m])
useRef
目的
- 如果需要一个值,在组件不断render时保持不变
- 初始化:const count = useRef(0)
- 读取:count.current
- 为什么需要current?为了保证两次useRef是同一个值(只有引用能做到)
Vue3也有ref,初始化:const count = ref(0); 读取:count.value;不同点:当count.value 变化时,Vue3会自动render
-
useRef能做到变化时自动render吗?不能
-
因为React的理念是 UI=f(data),如果需要资格功能完全可以自己加,监听ref,当ref.current变化时,调用setX即可
forwardRef
- 基本用法:让函数组件Button2支持ref
props无法传递ref属性
import React, { useRef } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const buttonRef = useRef(null);
return (
<div className="App">
<Button2 ref={buttonRef}>按钮</Button2>
{/* 看浏览器控制台的报错 */}
</div>
);
}
const Button2 = props => {
return <button className="red" {...props} />;
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
实现ref传递
import React, { useRef } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const buttonRef = useRef(null);
return (
<div className="App">
<Button3 ref={buttonRef}>按钮</Button3>
</div>
);
}
const Button3 = React.forwardRef((props, ref) => {
return <button className="red" ref={ref} {...props} />;
});
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
两次传递得到button的引用
import React, { useRef, useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const MovableButton = movable(Button2);
const buttonRef = useRef(null);
useEffect(() => {
console.log(buttonRef.curent);
});
return (
<div className="App">
<MovableButton name="email" ref={buttonRef}>
按钮
</MovableButton>
</div>
);
}
// function Button2(props) {
// return <button {...props} />;
// }
const Button2 = React.forwardRef((props, ref) => {
return <button ref={ref} {...props} />;
});
// 仅用于实验目的,不要在公司代码中使用
function movable(Component) {
function Component2(props, ref) {
console.log(props, ref);
const [position, setPosition] = useState([0, 0]);
const lastPosition = useRef(null);
const onMouseDown = e => {
lastPosition.current = [e.clientX, e.clientY];
};
const onMouseMove = e => {
if (lastPosition.current) {
const x = e.clientX - lastPosition.current[0];
const y = e.clientY - lastPosition.current[1];
setPosition([position[0] + x, position[1] + y]);
lastPosition.current = [e.clientX, e.clientY];
}
};
const onMouseUp = () => {
lastPosition.current = null;
};
return (
<div
className="movable"
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseUp={onMouseUp}
style={{ left: position && position[0], top: position && position[1] }}
>
<Component {...props} ref={ref} />
</div>
);
}
return React.forwardRef(Component2);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
useRef
- 可以用来引用DOM对象,也可以用来引用普通对象
forwardRef
- 由于props不包含ref,所以需要forwardRef
- 为什么props不包含ref?因为大部分时候不需要
useImperativeHandle
不使用useImperativeHandle 的代码
import React, { useRef, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
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();
}}
>
x
</button>
</div>
);
}
const Button2 = React.forwardRef((props, ref) => {
return <button ref={ref} {...props} />;
});
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
用了useImperativeHandle的代码
import React, {
useRef,
useState,
useEffect,
useImperativeHandle,
createRef
} from "react";
import ReactDOM from "react-dom";
import "./styles.css";
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.x();
}}
>
x
</button>
</div>
);
}
const Button2 = React.forwardRef((props, ref) => {
const realButton = createRef(null);
const setRef = useImperativeHandle;
setRef(ref, () => {
return {
x: () => {
realButton.current.remove();
},
realButton: realButton
};
});
return <button ref={realButton} {...props} />;
});
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
分析
- 用于自定义ref的属性
自定义Hook
封装数据操作
示例1
// useList.js
import { useState, useEffect } from "react";
const useList = () => {
const [list, setList] = useState(null);
useEffect(() => {
ajax("/list").then(list => {
setList(list);
});
}, []); // [] 确保只在第一次运行
return {
list: list,
setList: setList
};
};
export default useList;
function ajax() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([
{ id: 1, name: "Frank" },
{ id: 2, name: "Jack" },
{ id: 3, name: "Alice" },
{ id: 4, name: "Bob" }
]);
}, 2000);
});
}
// index.js
import React, { useRef, useState, useEffect } from "react";
import ReactDOM from "react-dom";
import useList from "./hooks/useList";
function App() {
const { list, setList } = useList();
return (
<div className="App">
<h1>List</h1>
{list ? (
<ol>
{list.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ol>
) : (
"加载中..."
)}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
示例2
// useList.js
import { useState, useEffect } from "react";
const useList = () => {
const [list, setList] = useState(null);
useEffect(() => {
ajax("/list").then(list => {
setList(list);
});
}, []); // [] 确保只在第一次运行
return {
list: list,
addItem: name => {
setList([...list, { id: Math.random(), name: name }]);
},
deleteIndex: index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}
};
};
export default useList;
function ajax() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([
{ id: "1", name: "Frank" },
{ id: "2", name: "Jack" },
{ id: "3", name: "Alice" },
{ id: "4", name: "Bob" }
]);
}, 2000);
});
}
// index.js
import React, { useRef, useState, useEffect } from "react";
import ReactDOM from "react-dom";
import useList from "./hooks/useList";
function App() {
const { list, deleteIndex } = useList();
return (
<div className="App">
<h1>List</h1>
{list ? (
<ol>
{list.map((item, index) => (
<li key={item.id}>
{item.name}
<button
onClick={() => {
deleteIndex(index);
}}
>
x
</button>
</li>
))}
</ol>
) : (
"加载中..."
)}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
分析
- 还可以在自定义Hook里使用Context
- useState只说了不能在if里,没说不能在函数里运行,只有这个函数在函数组件里运行即可
Stale Closure
过时闭包:函数引用的对象是之前产生的那个对象
解决方法:加个依赖让它自动刷新,还需要清除旧的
function createIncrement(incBy) {
let value = 0;
function increment() {
value += incBy;
console.log(value);
}
function log() {
const message = `Current value is ${value}`;
console.log(message);
}
return [increment, log];
}
const [increment, log] = createIncrement(1);
increment(); // logs 1
increment(); // logs 2
increment(); // logs 3
// Works!
log(); // logs "Current value is 3"