React Hooks:
状态 useState
Redux useReducer
副作用 useEffect -- useLayoutEffect
上下文 useContext
记忆 useMemo -- 回调useCallback
引用 useRef -- uselmperativeHandle
自定义 Hook -- useDebugValue
useState
状态
使用步骤
const [n,setN] = React.useState(0)
const [user,setUser] = React.useState({name:'F'})
- 注意事项1:不可局部更新 如果state是一个对象,则不能局部setState,因为useState不会帮我们合并属性【useReducer也不会合并属性】,需要用...复制属性
- 注意事项2:地址要变 setState(obj)如果obj地址不变,那么React就认为数据没有变化
想要user地址发生变化,只需要新建setUser对象
- 特殊使用:函数 useState接受函数,该函数返回初始state且只执行一次【写成对象则js引擎每次都会解析一遍】。该方法使用较少
const [state, setState] = useState(()=>{
return initialState
})
setState接收函数(示例)
setN(i => i + 1)
setN(i => i + 1)
useReducer
- useReducer是useState的复杂版,用来践行Flux/Redux思想
使用步骤:
- 创建初始值initialState
- 创建所有操作reducer(state, action)
- 传给useReducer,得到读写API
- 调用写({type:'操作类型'})
import React, { useReducer } from "react";
import ReactDOM from "react-dom";
const initial = {
n: 0
};
const reducer = (state, action) => {
if (action.type === "add") {
return { n: state.n + action.number };
} else if (action.type === "multi") {
return { n: state.n * 2 };
} else {
throw new Error("unknown type");
}
};
function App() {
const [state, dispatch] = useReducer(reducer, initial);
const { n } = state;
const onClick = () => {
dispatch({ type: "add", number: 1 });
};
const onClick2 = () => {
dispatch({ type: "multi", number: 2 });
};
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>+1</button>
<button onClick={onClick2}>*2</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
代替Redux
useContext
全局变量是全局的上下文;上下文是局部的全局变量。useContext不是响应式,是通过逐级传递的参数
使用方法:
- 使用
C = createContext(initial)创建上下文- 使用
<C.provider>圈定作用域- 在作用域内使用
useCotext(C)来使用上下文
import React, { createContext, useState, useContext } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const C = createContext(null);
function App() {
console.log("App 执行了");
const [n, setN] = useState(0);
return (
<C.Provider value={{ n, setN }}>
<div className="App">
<Baba />
</div>
</C.Provider>
);
}
function Baba() {
const { n, setN } = useContext(C);
return (
<div>
我是爸爸 n: {n} <Child />
</div>
);
}
function Child() {
const { n, setN } = useContext(C);
const onClick = () => {
setN(i => i + 1);
};
return (
<div>
我是儿子 我得到的 n: {n}
<button onClick={onClick}>+1</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
useEffect
副作用,即对环境的修改,如document.title。实际叫做afterRender更好,每次render之后运行
使用方法(这三种可同时存在):
- 作为
componentDidMount使用,[]作为第二个参数 - 作为
componentDidupdate使用,可指定依赖 - 作为
componentWillUnmount使用,通过render垃圾回收
特点:如果出现多个useEffect,会按照出现的次序执行
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
function App() {
const [n, setN] = useState(0);
const onClick = () => {
setN((n) => n + 1);
};
const afterRender = useEffect;
afterRender(() => {
console.log("第一次渲染后执行这句话");
}, []);
// []里的变量变化时再次执行,即[]为空不再执行
afterRender(() => {
console.log("第1,2,3。。。次渲染后执行");
});
// 每次都会执行
afterRender(() => {
if (n !== 0) {
console.log("n变化后渲染后执行");
}
}, [n]);
// []里的变量变化时再次执行
return (
<div>
n: {n}
<button onClick={onClick}>+1</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
useLayoutEffect(优先使用useEffect,因为会影响渲染的效率)
特点:
useLayoutEffect总是比useEffect优先执行useLayoutEffect里的任务最好影响了Layout
布局副作用与副作用的区别:useEffect会在浏览器第一次render之后执行,useLayoutEffect会在VNDOM生成DOM之后浏览器render之前执行
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const BlinkyRender = () => {
const [value, setValue] = useState(0);
useEffect(() => {
document.querySelector('#x').innerText = `value: 1000`
}, [value]);
return (
<div id="x" onClick={() => setValue(0)}>value: {value}</div>
);
};
ReactDOM.render(
<BlinkyRender />,
document.querySelector("#root")
);
以上代码,useEffect和useLayoutEffect看到的不同效果:前者会出现value由0变1000的闪烁画面,后者直接渲染出1000
useMemo
特点:
- 一般和
React.memo()一起使用,用于在props变化时执行的操作 - 第一个参数是
() => value,()是固定写法 - 第二个参数是[m,n]
- 只有当依赖变化时,才会计算新的value
- 依赖不变,就重用之前的value
注意:
- 如果你的value是一个函数,就要写成返回函数的函数
useMemo(() => (x) => console.log(x)) - 以上写法如此丑陋,这样就引出了
useCallback
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]); // 这里呃 [m] 改成 [n] 就会打印出旧的 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 Child2 = React.memo((props) => {
console.log("child 执行了");
console.log("假设这里有大量代码");
return <div onClick={e => props.onClick(e.target)}>child: {props.data}</div>;
});
*/
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
实现效果:只有初次渲染和改变依赖的props(m),才会执行函数
useCallback(useMemo的语法糖)
- 用法:
useMemo(() => x => log(x), [m])等价于useCallback(x => log(x), [m]
useRef
不同于state,useRef每次都是同一个值,在组件不断render时保持不变
用法:
- 初始化:
const count = useRef(0) - 读取:
count.current,其中current保证了两次ref为同一个值
import React, { useRef, useEffect, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [n,setN] = useState(0);
const count = useRef(0);
const onClick = () => {
setN(n + 10);
}
useEffect(() => {
count.current += 1;
console.log(count.current)
})
return (
<div className="App">
<div>
<button onClick={onClick}>update: {n}</button>
</div>
</div>
)
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
实现效果:点击打印count的值
实现原理:每次render后count是不同的,但指向同一个内存对象current,保证了ref为同一值
*更新UI
方法一:useEffect更新UI【见上面的案例】
方法二:使用useState
当不使用setN这个方法来改变数据,而直接进行数据变化操作时,重新渲染UI可以用setN手动更新
import React, { useRef, useState } from "react";
import ReactDOM from "react-dom";
function App() {
const [n, _setN] = useState(0);
const count = useRef(0);
const onClick = () => {
count.current += 1;
_setN(Math.random())
console.log(count.current)
}
return (
<div className="App">
<div>
<button onClick={onClick}>update: {count.current}</button>
</div>
</div>
)
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
实现效果:点击更新UI
小结
useState和useReducer: n每次都变
useMemo和useCallback: [m]变化时,fn变
useEffect:第二个参数为空每次都变,不为空参数变化时fn变
useRef永远不变
forwardRef
ref不能直接当作porps传给函数组件,只能传给类组件
因此需要 forwardRef 让 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">
<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);
uselmperativeHandle
uselmperativeHandle 实际叫做 setRef 更好,用于自定义 ref 的属性
自定义hook
可以用于封装数据,将原生hook封装到自定义hook里