React Hooks-Getting Started
文章目录

React: React-Hook

React >= 16.8 才可以使用

Basic

State Hook

Example

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

function App() {
// Declare a new state variable, which we'll call"count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}

export default App;

关键点就在于 useState() 方法:

  • useState() 返回一个数组
    • 第一个值是当前 state 的值
    • 第二个值是可以修改 第一个值 的函数方法
  • useState() 的参数是 初始值
    • 这个初始值的类型随意
    • 上方传入了 0 并且 0 会赋给 count , 注意随后的 count 的使用并没有写成 this.count
      1
      2
      3
      4
      5
      6
      7
      function ExampleWithManyStates() {
      // Declare multiple state variables!
      const [age, setAge] = useState(42);
      const [fruit, setFruit] = useState("banana");
      const [todos, setTodos] = useState([{ text: "Learn Hooks" }]);
      // ...
      }

特性

  • 不需要 Class 就能实现

Effect Hook

以前的一些 fetching, subscriptions 以及 DOM 操作都被称作 side effects , 简称 effects , 这些操作可以通过 Effect Hook 来替代

可以被替代的方法包括 componentDidMount , componentDidUpdate , 以及 componentWillUnmount , 可以简单地将 Effect Hook 看成这三者的结合体

Example

Old Pattern with react lifecycle

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
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
componentDidMount() {
//Mount 后会执行这部分
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
//Update 后会执行这部分
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p> You clicked {this.state.count}times </p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
New pattern with Effect Hook
1
2
3
4
5
6
7
8
9
10
11
12
13
import React, {useState, useEffect} from "react";
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p> You clicked {count}times </p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}

特性

关键点就在于 useEffect() 方法:

  • useEffect() 直接调用即可, 默认情况下 component 每次 render 都会调用这些 Effects
    • 第一次 render 也会执行这些 Effects
    • 可以将这个方法认定成 afterRender()
  • 返回值可以返回一个 类似 Thunk 的函数, 包含 clean up 时执行的方法
    • 以前的一些 didMount 方法里面我们可能会添加一些 subscriptions, 并且需要在 component unmount 的时候 unregister 掉, 而在 useEffect() 方法中, 只需要在返回值里面返回一个类似 thunk 的东西并且进行 unregister 即可
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      useEffect(() => {
      ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
      // 下方 return 一个 thunk, 进行 unsubscribe
      return () => {
      ChatAPI.unsubscribeFromFriendStatus(
      props.friend.id,
      handleStatusChange
      );
      };
      });
  • 当然这部分方法也可以进行定制化, 比如特定情况下才执行 thunk 里面的东西之类的, 详细查看: tell React to skip re-subscribing
  • 同一个 component 里面可以注册多个 Effect
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function FriendStatusWithCounter(props) {
    // 第一个
    const [count, setCount] = useState(0);
    useEffect(() => {
    document.title = `You clicked ${count} times` ;
    });

    // 第二个
    const [isOnline, setIsOnline] = useState(null);
    useEffect(() => {

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
    });

    function handleStatusChange(status) {
    setIsOnline(status.isOnline);
    }
    // ...
  • 每次 render 之后执行的 function 其实是不同的 Effect 实例, 这个是故意这样设计的
    1
    2
    3
    useEffect(() => {
    document.title = `You clicked ${count} times` ;
    });

Tips for Using Effects

  • 如果 Effect 返回一个 cleanup, 那么每次执行都会先 cleanup 再重新执行 Effect, 方便 subscribe 以及 unsubscribe 的实现

  • 跳过 特定 Effect 的方法:

    1
    2
    3
    4
    5
    componentDidUpdate(prevProps, prevState) {
    if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times` ;
    }
    }
  • 条件性执行 Effect 的方法

    1
    2
    3
    useEffect(() => {
    document.title = `You clicked ${count} times`;
    }, [count]); // Only re-run the effect if count changes
  • 传一个数组, 这个数组里面包含了一些参数, 只要这些参数不变就不会执行这个 Effect

  • 另外如果这里传一个 空数组, 那么这个 Effect 就只会单纯执行一次

  • 这种情况下就类似于绑定了 componentDidMountcomponentWillUnmount 方法

Additional Hooks

useMemo & useCallback

1
2
3
const memoizedValue = useMemo(() => computeExpensiveValue(dep), [dep]);

const memoizedCallback = useCallback(() => { doSomething(dep); }, [dep] );

几个要点:

  1. useMemo() 里面的方法会进行暂存

    • useEffect() 行为类似
    • 因为会暂存数据, 可以解决 useEffect() 中一些 console.log() 会重复输出很多次的问题
    • 异步请求则不能用 useMemo(), 要使用 useEffect()
  2. useCallback(fn, deps) 等同于 useMemo(() => fn, deps)

  3. 依赖数组不会当做参数传给里面的 callback

    1
    2
    3
    4
    5
    6
    const memoizedValue = useMemo(
    /* useMemo 第一个参数是 callback, 这个 callback 的参数是不用写的!!!!! */
    // (dep) => computeExpensiveValue(dep),
    () => computeExpensiveValue(dep),
    [dep]
    );

    useRef

通过 hook 访问对应的 DOM

(TODO: 下次用到了再来补充)

Rules of Hooks

  • √ 在 Top Level 里面执行 Hooks
    • × 不要在循环, 条件判断或者 nested function 里面使用 hooks
  • √ 在 react 的 function 里面调用 hooks
    • × 不要在原生的函数里面调用 hooks
    • √ 也可以从 Custom Hooks 里面调用 Hooks

在 Top Level 里面执行 Hooks

不要在循环, 条件判断或者 nested function 里面使用 hooks

1
2
3
4
5
6
// 🔴 We're breaking the first rule by using a Hook in a condition
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
1
2
3
4
5
6
useEffect(function persistForm() {
// 👍 We're not breaking the first rule anymore
if (name !== '') {
localStorage.setItem('formData', name);
}
});

Building Custom Hook

如果多个 Component 里面的几个 lifecycle 相关事件要处理相同的事情, 那么就可以自己写一个 Custom Hook 并且使用进去

  • Custom Hooks 是一个函数, 可以从里面调用 普通 Hooks
  • Custom Hooks 类似于将 render 时候执行的非 UI 操作包装起来.
  • Custom Hooks 参考文档: https://reactjs.org/docs/hooks-custom.html

写一个 Custom Hook

下方两段代码里面标记了的内容都是相似的, 功能也是相似的: 就是 subscribe 一个好友的状态, 我们接下来将把最部分提取出来这部分提取出来,写成一个 Custom Hook

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 FriendStatus(props) {
///////////////////
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
/////////////////////
if (isOnline === null) {
return "Loading...";
}
return isOnline ? "Online" : "Offline";
}

第二个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, {useState, useEffect} from "react";
function FriendListItem(props) {
///////////////////
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
///////////////////
return (
<li style={{ color: isOnline ? "green" : "black"}}>{props.friend.name}</li>
);
}
实际操作
  • 我们的目的肯定是要重构上面重复的这段代码
  • 新建的 Hook 尽可能用 use 作为方法名前缀
  • 虽然下方调用了同一个 Hook, 每一次调用 Hook 里面的 State 都是独立的, Hook 与 Hook 之间不相互影
  • 实际在下面这个 hooks 里面我们所做的操作:
    • 传入一个对应的 freiendId
    • 使用 useEffect 来获取 friend 状态
    • 然后将所获得的状态返回给外部
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, {useState, useEffect} from "react";

function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);

function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
使用 Custom Hook
1
2
3
4
5
6
7
8
9
function FriendStatus(props) {
/////////
const isOnline = useFriendStatus(props.friend.id);
///////////
if (isOnline === null) {
return "Loading...";
}
return isOnline ? "Online" : "Offline";
}

以及

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function FriendListItem(props) {
/////////
const isOnline = useFriendStatus(props.friend.id);
///////
return (
<li
style={{
color: isOnline ? "green" : "black",
}}
>
{" "}
{props.friend.name}{" "}
</li>
);
}

使用 useState 实现 useReducer

首先我们写一个 Reducer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function todosReducer(state, action) {
switch (action.type) {
case "add":
return [
...state,
{
text: action.text,
completed: false,
},
];
// ... other actions ...
default:
return state;
}
}

然后写一个 Hook:

1
2
3
4
5
6
7
8
9
10
11
12
13
function useReducer(reducer, initialState) {
/**
第一个参数是 reducer
第二个参数是 初始 State
*/
const [state, setState] = useState(initialState);
function dispatch(action) {
// 执行 Reducer 获取下一个 State
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}

实际使用:

1
2
3
4
5
6
7
8
function Todos() {
const [todos, dispatch] = useReducer(todosReducer, []); // 这里 todosReducer 就是上面定义的 Reducer, dispatch 就是发送 state 到 reducer
function handleAddClick(text) {
// onClick 事件里面调用 dispatch 发送 state 到 reducer
dispatch({type: "add", text});
}
// ...
}

Deep Thinking in React Hooks

合并多个 Hooks

1
2
3
4
const [width, setWidth] = useState(100);
const [height, setHeight] = useState(100);
const [left, setLeft] = useState(0);
const [top, setTop] = useState(0);

有的时候如果 state 写出来特别多, 就可以将几个相互之间有关联的放在一起

1
2
3
4
5
6
7
8
const [metric, setMetric] = useState({
width: 100,
height: 100,
});
const [pos, setPos] = useState({
left: 10,
top: 5,
});

相同依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
const dataA = useMemo(() => {
return getDataA();
}, [A, B]);

const dataB = useMemo(() => {
return getDataB();
}, [A, B]);

// 应该合并为

const [dataA, dataB] = useMemo(() => {
return [getDataA(), getDataB()]
}, [A, B]);