React Hooks

解决的问题

  • 在组件之间复用状态逻辑很难,可能要用到render props和高阶组件,React 需要为共享状态逻辑提供更好的原生途径,Hook 使你在无需修改组件结构的情况下复用状态逻辑
  • 复杂组件变得难以理解,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)

注意事项

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。

useState

  • useState 就是一个 Hook
  • 通过在函数组件里调用它来给组件添加一些内部 state,React 会在重复渲染时保留这个 state
  • useState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并
  • useState 唯一的参数就是初始 state
    1
    const [state, setState] = useState(initialState);

1】 alert会“捕获”点击按钮时候的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, {useState} from 'react';

function Counter(){
const [number,setNumber] = useState(0);
function alertNumber(){
setTimeout(()=>{
alert(number);
},3000);
}
return (
<>
<p>{number}</p>
<button onClick={()=>setNumber(number+1)}>+</button>
<button onClick={alertNumber}>alertNumber</button>
</>
)
}
export default Counter;

点击 alertNumber按钮一次, 再点击 +按钮两次,number变为 2, 3秒钟后alert 0

2】 函数式更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, {useState} from 'react';

function Counter(){
const [number,setNumber] = useState(0);
function alertNumber(){
setTimeout(()=>{
setNumber(number=>number+1) // 函数式更新
},3000);
}
return (
<>
<p>{number}</p>
<button onClick={()=>setNumber(number+1)}>+</button>
<button onClick={alertNumber}>alertNumber</button>
</>
)
}
export default Counter;

如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setNumber。该函数将接收先前的 state,并返回一个更新后的值。

3】惰性初始state

initialState初始状态参数只会在组件初始渲染的时候调用,后续渲染会被忽略
与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果

4】性能优化

Object.is

调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较算法 来比较 state。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, {useState} from 'react';

function Counter(){
const [state,setNumber] = useState(function(){
return {number: 0, name: '计数器'}
});
console.log("Counter render");
return (
<>
<p>{state.name}:{state.number}</p>
<button onClick={()=>setNumber({...state, number: state.number + 1})}>+</button>
<button onClick={()=>setNumber(state)}>old state +</button>
</>
)
}
export default Counter;

点击 + 按钮会输出 Counter render 组件刷新
点击 old state + 不更改数据,会刷新一次,之后都不会刷新组件

减少渲染次数

useCallback

减少渲染次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 情况一
import React, {useState} from 'react';

function Counter(){
const [number,setNumber] = useState(0);
const [name,setName] = useState('test');
// 会在每次渲染的时候都会生成一个新的函数
const addClick = ()=> setNumber(number+1);
return (
<>
<p>{name}:{number}</p>
<button onClick={addClick}>+</button>
</>
)
}
export default Counter;

添加了useCallback之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, {useState, useCallback} from 'react';
let lastAddClick;
function Counter(){
const [number,setNumber] = useState(0);
const [name,setName] = useState('test');
// 添加了 useCallback,后面置了一个空数组
const addClick = useCallback(()=> setNumber(number+1), []);
// 只有在依赖的变量发生变化的时候才会重新生成
// const addClick = useCallback(()=> setNumber(number+1), [number]);

console.log(lastAddClick === addClick);
lastAddClick = addClick;
return (
<>
<p>{name}:{number}</p>
<button onClick={addClick}>+</button>
</>
)
}
export default Counter;

第一次是false,点击按钮可以看到后面都是 true
将line27注释打开,这里的number就是依赖的变量,所以这里当number改变的时候,都会重新生成 addClick

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 情况一
import React, {useState} from 'react';

function Child(props){
console.log("Child render");
return (
<button onClick={props.addClick}>{props.data.number}</button>
)
}

function App(){
const [number, setNumber] = useState(0);
const [name, setName] = useState('test');

const data = {number}
const addClick = ()=>setNumber(number+1);

return (
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<Child addClick={addClick} data={data} />
</div>
)
}
export default App;

input框中改变input 的值触发onChange,AppChild 都会渲染
点击Child的button,改变number的值,AppChild 都会渲染

子组件Child是一个纯函数,希望只有当props改变的时候才会渲染Child组件

1】 memo
1
2
import React, {useState, memo} from 'react';
Child = memo(Child);
2】 useCallback

这里每次App组件渲染的时候都会重新生成addClick,改变如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
+ import React, {useState, useCallback, memo} from 'react';

let lastAddClick;
function App(){
const [number, setNumber] = useState(0);
const [name, setName] = useState('test');

const data = {number}
+ // 第二个参数表示此函数缓存依赖的变量,如果变量变了,会生成新的函数
+ const addClick = useCallback(()=>setNumber(number+1), [number]);
+ console.log("addClick:", addClick === lastAddClick);
+ lastAddClick = addClick;

return (
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<Child addClick={addClick} data={data} />
</div>
)
}
function Child(props){
console.log("Child render");
return (
<button onClick={props.addClick}>{props.data.number}</button>
)
}
Child = memo(Child);
export default App;

第一次进来的时候 addClick: false,之后改变name的值时 都是true,只有改变 addClick函数缓存依赖的变量number时,才会重新生成,输出true

这里 const data = {number}每次App函数渲染的时候也都重新生成了

3】useMemo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
+ import React, {useState, useCallback, useMemo} from 'react';

let lastAddClick;
+ let lastData;
function App(){
const [number, setNumber] = useState(0);
const [name, setName] = useState('test');

const data = useMemo(()=>({number}),[number]);
const addClick = useCallback(()=>setNumber(number+1),[number]);
console.log("addClick:", lastAddClick === addClick);
lastAddClick = addClick;

+ console.log("data:", lastData === data); // 可以看到,一开始进来false,改变inpu中的值为true,点击按钮改变number 的值为false
+ lastData = data;

return (
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<Child addClick={addClick} data={data} />
</div>
)
};

function Child(props){
console.log("Child render");
return (
<button onClick={props.addClick}>{props.data.number}</button>
)
};
Child = memo(Child);
export default App;

5】注意事项

只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。

因为react内部hook是以链表的形式一个一个按顺序存储的,如下情况,一次if判断,存储 ① ② ③,第二次if判断只存储 ① ② ,它是通过前后存储的hook按顺序判断的,少了一个就会出错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, {useState, useEffect} from 'react';
function App() {
const [number, setNumber] = useState(0);
const [visible, setVisible] = useState(false);
if (number % 2 == 0) {
useEffect(() => {
setVisible(true);
}, [number]);
} else {
useEffect(() => {
setVisible(false);
}, [number]);
}
return (
<div>
<p>{number}</p>
<p>{visible && <div>visible</div>}</p>
<button onClick={() => setNumber(number + 1)}>+</button>
</div>
)
}
export default App;

useReducer

hooks的作者就是redux的作者

  • useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法
  • 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React, {useReducer} from 'react';

let initialState = {number: 0};
const INCREMENT = 'increment';
const DECREMENT = 'decrement';

function reducer(state, action) {
switch (action.type) {
case INCREMENT:
return {number: state.number + 1};
case DECREMENT:
return {number: state.number - 1};
default:
return state;
}
}

function App(){
const [state, dispatch] = useReducer(reducer, initialState);
// 这里的 initialState 就是 state的初始值
return (
<div>
<p>{state.number}</p>
<button onClick={()=>dispatch({type: INCREMENT})}>+</button>
<button onClick={()=>dispatch({type: DECREMENT})}>-</button>
</div>
)
};

export default App;

如上,点+加一,点-减一

useState是基于useReducer实现的,基于上面的useReducer自己实现useState

原生useState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, {useState} from 'react';
let initialState = {number: 0};

function App(){
const [state, setState] = useState(initialState);
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
<button onClick={()=>setState({number: state.number-1})}>-</button>
</div>
)
};

export default App;

自定义useState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, {useCallback, useReducer} from 'react';
let initialState = {number: 0};

// 自定义hooks
function useState(initialState){ // 内部就是用的useReducer
const reducer = useCallback((state,action)=>action.payload);
// 不用useCallback也可以,优化而已
// const reducer = (state,action)=>action.payload;
const [state, dispatch] = useReducer(reducer, initialState); // 这里的initialState就是上一行 (state,action) 中的state
function setState(payload){
// 调dispatch方法 派发action
dispatch({payload});
}
return [state, setState]; // 这里state有,setState需要自己实现
}

function App(){
const [state, setState] = useState(initialState);
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
<button onClick={()=>setState({number: state.number-1})}>-</button>
</div>
)
};

export default App;

useContext

  • 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
  • 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
  • useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>
  • useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context

不使用useContext时的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, {useState, useContext} from 'react';
let MyContext = React.createContext();

function Counter(){
return (
<MyContext.Consumer>
{
value => (
<div>
<p>{value.state.number}</p>
<button onClick={()=>value.setState({number: value.state.number+1})}>+</button>
</div>
)
}
</MyContext.Consumer>
);
}
function App(){
const [state, setState] = useState({number: 0});
return (
<MyContext.Provider value={{state, setState}}>
<Counter />
</MyContext.Provider>
)
};

export default App;

使用useContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, {useState, useContext} from 'react';
let MyContext = React.createContext();

function Counter(){
let {state, setState} = useContext(MyContext);
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
</div>
);
}
function App(){
const [state, setState] = useState({number: 0});
return (
<MyContext.Provider value={{state, setState}}>
<Counter />
</MyContext.Provider>
)
};

export default App;

effect

  • 在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性
  • 使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道
  • useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API
  • 该 Hook 接收一个包含命令式、且可能有副作用代码的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, {useState, useEffect} from 'react';

function App(){
const [state, setState] = useState({number: 0});
// useEffect里的函数会在 componentDidMount、componentDidUpdate后进行调用
useEffect(()=>{
document.title = state.number;
})
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
</div>
)
};

export default App;

点击+页面数字变化,title也跟着变化

1】调过effect进行性能优化

  • 如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可
  • 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, {useState, useEffect} from 'react';

function App(){
const [state, setState] = useState({number: 0});
// 如果没有给第二个参数,函数会在每次执行渲染后调用
useEffect(()=>{
document.title = state.number;
}, [])
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
</div>
)
};

export default App;

如果没有第二个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, {useState, useEffect} from 'react';

function App(){
const [state, setState] = useState({number: 0});

useEffect(()=>{
let timer = setInterval(()=>{
setState(state=>({number: state.number+1}));
},1000)
}, [])
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
</div>
)
};

export default App;

有第二个参数,它是每隔1s加一,没有,就会各种叠加,因为没渲染一次,就会生成一个新的定时器。

2】 清除副作用

  • 副作用函数还可以通过返回一个函数来指定如何清除副作用
  • 为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, {useState, useEffect} from 'react';

function App(){
const [state, setState] = useState({number: 0});

useEffect(()=>{
let timer = setInterval(()=>{
setState(state=>({number: state.number+1}));
},1000)
}, [])
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
</div>
)
};

function Counter(){
const [visible, setVisible] = useState(true);
return (
<div>
{
visible && <App />
}
<button onClick={()=>setVisible(false)}>hide</button>
</div>
)
}

export default App;

点击hide按钮后,组件App不显示,页面报错

1
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

不能在一个已经卸载的组件上执行状态更新。会出现内存泄露。为了修复,需要在useEffect清理函数中取消所有的订阅和异步任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React, {useState, useEffect} from 'react';

function App(){
const [state, setState] = useState({number: 0});

useEffect(()=>{
let timer = setInterval(()=>{
setState(state=>({number: state.number+1}));
},1000)

// useEffect会返回一个清理函数,当组件将要卸载的时候会执行清理函数
return ()=>{
clearInterval(timer);
}
}, [])
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
</div>
)
};

function Counter(){
const [visible, setVisible] = useState(true);
return (
<div>
{
visible && <App />
}
<button onClick={()=>setVisible(false)}>hide</button>
</div>
)
}
export default Counter;

3】useRef

  • useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)
  • 返回的 ref 对象在组件的整个生命周期内保持不变
1
const refContainer = useRef(initialValue);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React, {useState, useRef} from 'react';

let lastRef;
function Child(){
// let refObj = React.createRef();
let refObj = useRef();
console.log(lastRef === refObj);
lastRef = refObj;

function getFocus(){
refObj.current.focus()
}
return (
<div>
<input ref={refObj} />
<button onClick={getFocus}>focus</button>
</div>
)
}
function App(){
const [state, setState] = useState({number: 0});
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
<Child />
</div>
)
};
export default App;

React.createRef时, 每次渲染Child组件都会重新生成 refObj
useRef时,返回的ref对象在组件的整个生命周期内保持不变。

实现useRef

1
2
3
4
5
6
7
let currentRefObject;
function useRef(){
if(!currentRefObject){
currentRefObject = {current: null};
}
return currentRefObject;
}

4】forwardRef

现在想把获取焦点按钮放到App组件中,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, {useRef} from 'react';

function Child(props){
return (
<div>
<input ref={props.ref} />
</div>
)
}
function App(){
let refObj = useRef();
return (
<div>
<Child ref={refObj} />
<button onClick={()=>refObj.current.focus()}>focus</button>
</div>
)
};
export default App;

页面会报错

1
Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

使用React.forwardRef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
+import React, {useRef} from 'react';

+function Child(props,ref){
return (
<div>
+ <input ref={ref} />
</div>
)
}
+let ForwardRefChild = React.forwardRef(Child);
function App(){
let refObj = useRef();
function getFocus(){
refObj.current.focus()
}
return (
<div>
<ForwardRefChild ref={refObj} />
<button onClick={getFocus}>focus</button>
</div>
)
};
export default App;

直接这样传递的话,Child组件的输入框对象直接暴露给了父组件,也破坏了封装的原则,很危险

useImperativeHandle

  • useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值
  • 在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, {useRef, useImperativeHandle} from 'react';

function Child(props,ref){
let refObj = useRef();
useImperativeHandle(ref, () => ({
focus(){
refObj.current.focus();
}
}),
)
return (
<div>
<input ref={refObj} />
</div>
)
}
let ForwardRefChild = React.forwardRef(Child);
function App(){
let refObj = useRef();
function getFocus(){
refObj.current.focus();
refObj.current.value = 'xxxx';
}
return (
<div>
<ForwardRefChild ref={refObj} />
<button onClick={getFocus}>focus</button>
</div>
)
};
export default App;

如果不用useImperativeHandle,子组件直接用父组件传递过来的<input ref={ref} />,当父组件App执行到line22时,就会直接取修改子组件input中的value,现在父组件中getFocus方法只有line21是生效的,因为它调用的focus方法是子组件有暴露给外部的。

useLayoutEffect

  • 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect
  • 可以使用它来读取 DOM 布局并同步触发重渲染
  • 在浏览器执行绘制之前useLayoutEffect内部的更新计划将被同步刷新
  • 尽可能使用标准的 useEffect 以避免阻塞视图更新

如图是浏览器呈现一张页面的过程。
在红色分割线左边界面并没有绘制,绿色圈中是布局,蓝色圈中是绘制,页面上能看到效果。
如图可以看到 useLayoutEffect 和 useEffect 的执行顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, {useState, useEffect, useLayoutEffect} from 'react';

function App(){
const [color, setColor] = useState('red');
useEffect(()=>{
console.log("useEffect", color);
})
useLayoutEffect(()=>{
alert("useLayoutEffect");
})
return (
<div>
<p style={{backgroundColor: color}}>背景色</p>
<button onClick={()=>setColor('red')}>红</button>
<button onClick={()=>setColor('yellow')}>黄</button>
<button onClick={()=>setColor('blue')}>蓝</button>
</div>
)
};
export default App;

点击按钮颜色切换时,先alert useLayoutEffect,界面变色,再console.log出 useEffect

自定义hooks

  • 有时候我们会想要在组件之间重用一些状态逻辑
  • 自定义 Hook 可以让你在不增加组件的情况下达到同样的目的
  • Hook 是一种复用状态逻辑的方式,它不复用 state 本身
  • 事实上 Hook 的每次调用都有一个完全独立的 state
  • 自定义 Hook 更像是一种约定,而不是一种功能。如果函数的名字以 use 开头,并且调用了其他的 Hook,则就称其为一个自定义 Hook

示例

当有2个逻辑相同的组件时,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React, {useState, useEffect} from 'react';
import ReactDOM from 'react-dom';
function Counter1(){
const [number, setNumber] = useState(0);
useEffect(()=>{
let timer = setInterval(()=>{
setNumber(number+1);
}, 1000);
return ()=>{
clearInterval(timer);
}
})
return (
<div>
{number}
</div>
)
}
function Counter2(){
const [number, setNumber] = useState(0);
useEffect(()=>{
let timer = setInterval(()=>{
setNumber(number+1);
}, 1000);
return ()=>{
clearInterval(timer);
}
})
return (
<div>
{number}
</div>
)
}

ReactDOM.render(
<>
<Counter1 />
<Counter2 />
</>,
document.getElementById('root')
);

页面上同时展示 每隔1秒 number + 1

使用自定义hooks(函数的名字以 use 开头,并且调用了其他的 Hook),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React, {useState, useEffect} from 'react';
import ReactDOM from 'react-dom';

function useCounter(){
const [number, setNumber] = useState(0);
useEffect(()=>{
let timer = setInterval(()=>{
setNumber(Math.random());
}, 1000);
return ()=>{
clearInterval(timer);
}
})
return number;
}
function Counter1(){
let number = useCounter();
return (
<div>
{number}
</div>
)
}
function Counter2(){
let number = useCounter();
return (
<div>
{number}
</div>
)
}

ReactDOM.render(
<>
<Counter1 />
<Counter2 />
</>,
document.getElementById('root')
);

这里line8,将number生成了随机数来展示说明,Hook是一种复用状态逻辑的方式,它不复用state本身,事实上Hook的每次调用都有一个完全独立的state

中间件

logger

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React, {useReducer, useEffect} from 'react';

let initialState = {number: 0};
const INCREMENT = 'increment';
const DECREMENT = 'decrement';

function reducer(state, action) {
switch (action.type) {
case INCREMENT:
return {number: state.number + 1};
case DECREMENT:
return {number: state.number - 1};
default:
return state;
}
}
+// 实现redux-logger 在每次状态变更后打印新的状态值;
+// redux中间件,是用新的dispatch替换老的dispatch
+function useLogger(reducer, initialState){
+ const [state, dispatch] = useReducer(reducer, initialState);
+ function loggerDispatch(action){
+ console.log("老状态:", state);
+ dispatch(action);
+ }
+ useEffect(()=>console.log("新状态", state));
+ return [state, loggerDispatch]
+}
function App(){
+ const [state, dispatch] = useLogger(reducer, initialState); // 这里调用 useLogger
return (
<div>
<p>{state.number}</p>
<button onClick={()=>dispatch({type: INCREMENT})}>+</button>
<button onClick={()=>dispatch({type: DECREMENT})}>-</button>
</div>
)
};

export default App;

thunk

thunk里面派发的是函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import React, {useReducer, useEffect} from 'react';

let initialState = {number: 0};
const INCREMENT = 'increment';
const DECREMENT = 'decrement';

function reducer(state, action) {
switch (action.type) {
case INCREMENT:
return {number: state.number + 1};
case DECREMENT:
return {number: state.number - 1};
default:
return state;
}
}

+function useThunk(reducer, initialState){
+ const [state, dispatch] = useReducer(reducer, initialState);
+ function thunkDispatch(action){
+ // 判断是否为函数,这里模拟的是redux-logger,line23的参数是根据line35中的来,这里传入新的dispatch,也就是thunkDispatch
+ if(typeof action === 'function'){
+ action(thunkDispatch, ()=>state);
+ }else{
+ dispatch(action);
+ }
+ }
+ return [state, thunkDispatch]
+}
function App(){
+ const [state, dispatch] = useThunk(reducer, initialState);
return (
<div>
<p>{state.number}</p>
+ <button onClick={()=>dispatch(function(dispatch, getState){
+ dispatch({type: INCREMENT})
+ })}>+</button>
<button onClick={()=>dispatch({type: DECREMENT})}>-</button>
</div>
)
};

export default App;

promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import React, {useReducer, useEffect} from 'react';

let initialState = {number: 0};
const INCREMENT = 'increment';
const DECREMENT = 'decrement';

function reducer(state, action) {
switch (action.type) {
case INCREMENT:
return {number: state.number + 1};
case DECREMENT:
return {number: state.number - 1};
default:
return state;
}
}

+function usePromise(reducer, initialState){
+ const [state, dispatch] = useReducer(reducer, initialState);
+ function promiseDispatch(action){
+ if(typeof action.then === 'function'){
+ action.then(promiseDispatch); // 点击按钮 1秒 后加1
+ }else{
+ dispatch(action);
+ }
+ }
+ return [state, promiseDispatch]
+}
function App(){
+ const [state, dispatch] = usePromise(reducer, initialState);
return (
<div>
<p>{state.number}</p>
+ <button onClick={()=>dispatch(new Promise(function(resolve){
+ setTimeout(()=>{
+ resolve({type: INCREMENT})
+ },1000)
+ }))}>+</button>
<button onClick={()=>dispatch({type: DECREMENT})}>-</button>
</div>
)
};

export default App;

ajax

新建api.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let express = require('express');
let app = express();
app.use(function (req, res, next) {
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
next();
});
app.get('/api/users', function (req, res) {
let offset = parseInt(req.query.offset);// 偏移量
let limit = parseInt(req.query.limit); // 每页的条数
let result = [];
for (let i = offset; i < offset + limit; i++) {
result.push({ id: i + 1, name: 'name' + (i + 1) });
}
res.json(result);
});
app.listen(8000);

npm install express
node ./api.js 运行后台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { useState, useEffect, useLayoutEffect } from 'react';
import ReactDOM from 'react-dom';
function useRequest(url) {
let limit = 5;
let [offset, setOffset] = useState(0);
let [data, setData] = useState([]);
async function loadMore() {
setData(null);
let pageData = await fetch(`${url}?offset=${offset}&limit=${limit}`)
.then(response => response.json());
setData([...data, ...pageData]);
setOffset(offset + pageData.length);
}
useEffect(loadMore, []);
return [data, loadMore];
}

function App() {
const [users, loadMore] = useRequest('http://localhost:8000/api/users');
if (users === null) {
return <div>正在加载中....</div>
}
return (
<>
<ul>
{
users.map((item, index) => <li key={index}>{item.id}:{item.name}</li>)
}
</ul>
<button onClick={loadMore}>加载更多</button>
</>
)
}
ReactDOM.render(<App />, document.getElementById('root'));

小结

useMemouseCallbackuseRef
本质上都是为了缓存,这些东西在没有hooks之前,在以前我们都用类组件,一但创建就有类的实例,上面的属性也可以存在。但是现在我们用hooks,hooks只能用在函数组件里,函数组件没有this,就没有实例,就没有办法在实例上挂属性和状态。现在就要靠 useMemouseCallbackuseRef 实现缓存。

官方文档

https://zh-hans.reactjs.org/docs/hooks-intro.html

1、React 需要为共享状态逻辑提供更好的原生途径。
2、你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。

相关文章

https://zh-hans.reactjs.org/docs/hooks-intro.html
http://www.zhufengpeixun.cn/2020/html/62.5.react-hooks.html