React Hooks 系列

时间:2020-9-8 作者:admin

1、场景

先理解什么是hook,官网给的是:

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

2、hooks的意义

  • 在组件之间复用状态逻辑很难:React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store),如果你使用过 React 一段时间,你也许会熟悉一些解决此类问题的方案,比如 render props 和 高阶组件。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。React 需要为共享状态逻辑提供更好的原生途径。
  • 复杂组件变得难以理解:我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑
  • 难以理解的 class: JavaScript 中 this 的工作方式
  • 面向生命周期编程变成了面向业务逻辑编程
  • 与时俱进,组件预编译…
  • 渐进策略,Hooks 和现有代码可以同时工作

hooks通过function抽离的方式,实现了复杂逻辑的内部封装:

  • 逻辑代码的复用
  • 减小了代码体积
  • 没有this的烦恼

3、举个例子

规则:

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

(1) useState

// class组件
import React from "react";
import "./styles.css";

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}
// hooks
import React, { useState } from 'react';

function Example() {
  // 声明一个叫 "count" 的 state 变量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
  • 调用 useState 方法的时候做了什么?定义一个 “state 变量”
  • useState 需要哪些参数?唯一的参数就是初始 state
  • useState 方法的返回值是什么?当前 state 以及更新 state 的函数

读取 State

//class
<p>You clicked {this.state.count} times。</p>
// hook
<p>You clicked {count} times</p>

更新 State

//class
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
  Click me
</button>
// hook
<button onClick={() => setCount(count + 1)}>
  Click me
</button>
  • useState的初始值是惰性的,只会在初次渲染组件的时候起作用。
  • useState的更新是替换,而不是合并。
  • useState可以接收函数参数,并将函数的返回值作为初始值(便于复杂计算)

    const [count, setCount] = useState(() => {
      return Math.random() * 10;
    });
  • useState的更新函数可以接收函数作为参数,函数的参数是前一状态的state值。

    setCount((count)=> count + 1)
  • 使用当前的值,对state进行更新不会触发渲染。

    const [state, setState] = useState(0);
    // ...
    // 更新 state 不会触发组件重新渲染,(使用Object.is比较)
    setState(0);
    setState(0);

(2) useEffect

  • useEffect可以让我们在函数组件中执行副作用操作。事件绑定,数据请求,动态修改DOM。
  • useEffect将会在每一次React渲染之后执行。无论是初次挂载时,还是更新。(当然这种行为我们可以控制)
  • 如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合
  • 在class组件中,通常在componentDidMount中添加对事件的监听。在componentWillUnmount中会清除对事件的监听。我们需要在不同的生命周期函数中,拆分我们的逻辑。
  • 而effect可以返回一个函数,当react进行清除时, 会执行这个返回的函数。每当执行本次的effect时,都会对上一个effect进行清除。组件卸载时也会执行进行清除。

(2.1)无需清除的 effect

// class组件
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    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>
    );
  }
}
// hook
import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  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 做了什么? 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作
  • 为什么在组件内部调用 useEffect? 在 effect 中直接访问 state 变量
  • useEffect 会在每次渲染后都执行吗?在第一次渲染之后和每次更新之后都会执行

(2.2)需要清除的 effect

  • 防止引起内存泄露
class FriendStatus extends React.Component {
    constructor(props) {
      super(props);
      this.state = { isOnline: null };
      this.handleStatusChange = this.handleStatusChange.bind(this);
    }

    componentDidMount() {
      ChatAPI.subscribeToFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
      );
    }
    componentWillUnmount() {
      ChatAPI.unsubscribeFromFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
      );
    }
    handleStatusChange(status) {
      this.setState({
        isOnline: status.isOnline
      });
    }

    render() {
      if (this.state.isOnline === null) {
        return 'Loading...';
      }
      return this.state.isOnline ? 'Online' : 'Offline';
    }
  }
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);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
  • 为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。
  • React 何时清除 effect?执行当前 effect 之前对上一个 effect 进行清除

(2.3)使用多个 Effect 实现关注点分离

  • 解决 class 中生命周期函数经常包含不相关的逻辑
class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...
function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  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);
    };
  });
  // ...
}

(2.4)为什么每次更新的时候都要运行 Effect?

componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate(prevProps) {
    // 取消订阅之前的 friend.id
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // 订阅新的 friend.id
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
function FriendStatus(props) {
  // ...
  useEffect(() => {
    // ...
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  //...
  • useEffect 默认就会在调用一个新的 effect 之前对前一个 effect 进行清理
// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // 运行第一个 effect

// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // 运行下一个 effect

// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // 运行下一个 effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一个 effect

(2.5)通过跳过 Effect 进行性能优化

  • 每次执行effect,清除上一次effect可能会造成不必要的性能浪费。我们可以通过effect的第二个参数,控制effect的执行。 第二个参数是useEffect的依赖,只有当依赖发生变化时,useEffect才会更新。
componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

(3) useContext

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

(3.1) 何时使用 Context?

  • Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。
class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}

function Toolbar(props) {
  // Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
  // 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
  // 因为必须将这个值层层传递所有组件。
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}

使用 context, 我们可以避免通过中间元素传递 props:

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // React 会往上找到最近的 theme Provider,然后使用它的值。
  // 在这个例子中,当前的 theme 值为 “dark”。
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}
const ThemeContext = React.createContext("light");
export default function APP() {
  // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
  // 无论多深,任何组件都能读取这个值。
  // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}
function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <div theme={theme}>{theme}</div>;
}

useContext接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。

(4) useReducer

useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

  • 用 reducer 重写 useState 一节的计数器示例:
const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

(4.1)指定初始 state

有两种不同初始化 useReducer state 的方式,你可以根据使用场景选择其中的一种。

  • 将初始 state 作为第二个参数传入 useReducer 是最简单的方法
const [state, dispatch] = useReducer(
    reducer,
    {count: initialCount}
  );
  • 惰性初始化:你可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)。这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利:
function init(initialCount) {
  return {count: initialCount};
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}
  • 跳过 dispatch

如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法 来比较 state。)

(4.2) useReducer&&useContext 简单的代替 Redux

import React, { useReducer, useContext, useEffect } from "react";

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>
    );
}

// 帮助函数
// 两秒钟后,根据 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);
    });
}

(5) useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
  • 传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。
  • 如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
  • 需要先讲 React.memo
  • React默认有多余的render
function App() {
    const [n, setN] = useState(0);
    const [m, setM] = useState(0);
    const onClick = () => {
        setN(n + 1);
    };

    return (
        <div className="App">
            <div>
                {/*点击button会重新执行Child组件*/}
                <button onClick={onClick}>update n {n}</button>
            </div>
            <Child data={m}/>
            {/* <Child2 data={m}/> */}
        </div>
    );
}

function Child(props) {
    console.log("child 执行了");
    console.log('假设这里有大量代码')
    return <div>child: {props.data}</div>;
}

const Child2 = React.memo(Child);

将代码中的 Child 用React.memo(Child) 代替

function App() {
    const [n, setN] = useState(0);
    const [m, setM] = useState(0);
    const onClick = () => {
        setN(n + 1);
    };

    return (
        <div className="App">
            <div>
                {/*点击button会重新执行Child组件*/}
                <button onClick={onClick}>update n {n}</button>
            </div>
            <Child data={m}/>
        </div>
    );
}

const Child = React.memo(props => {
        console.log("child 执行了");
        return <div>child: {props.data}</div>;
});
  • 但是,有一个bug,添加了监听函数之后,一秒破功因为 App 运行时,会再次执行 onClickChild,生成新的函数,新旧函数虽然功能一样,但是地址引用不一样!
function App() {
    const [n, setN] = useState(0);
    const [m, setM] = useState(0);
    const onClick = () => {
        setN(n + 1);
    };
    const onClickChild = () => {}
    return (
        <div className="App">
            <div>
                {/*点击button会重新执行Child组件*/}
                <button onClick={onClick}>update n {n}</button>
            </div>
            {/*但是如果传了一个引用,则React.memo无效。因为引用是不相等的*/}
            <Child data={m} onClick={onClickChild}/>
        </div>
    );
}

//使用React.memo可以解决重新执行Child组件的问题
const Child = React.memo(props => {
        console.log("child 执行了");
        return <div onClick={props.onClick}>child: {props.data}</div>;
});
  • 所以,useMemo
import React, { useState, useMemo } from "react";
import "./styles.css";

export default function App() {
  const [n, setN] = useState(0);
  const [m, setM] = useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  const onClick1 = () => {
    setM(m + 1);
  };
  // const onClickChild = () => {};
  const onClickChild1 = useMemo(() => {
    return () => {
      console.log(`on click child m: ${m}`);
    };
  }, [m]);
  return (
    <div className="App">
      <div>
        {/*点击button会重新执行Child组件*/}
        <button onClick={onClick}>update n {n}</button>
        <button onClick={onClick1}>update m {m}</button>
      </div>
      {/*但是如果传了一个引用,则React.memo无效。因为引用是不相等的*/}
      {/*<Child data={m} onClick={onClickChild}/>*/}
      {/*onClickChild1使用useMemo可以消除此bug*/}
      <Child data={m} onClick={onClickChild1} />
    </div>
  );
}

//使用React.memo可以解决重新执行Child组件的问题
const Child = React.memo((props) => {
  console.log("child 执行了");
  return <div onClick={props.onClick}>child: {props.data}</div>;
});
  • 第一个参数是 () => value
  • 第二个参数是依赖 [m, n]
  • 只有当依赖变化时,才会计算出新的 value
  • 如果依赖不变,那么就重用之前的 value
  • 如果你的 value 是一个函数,于是就有了useCallback

(6) useCallback

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

(7) useRef

const refContainer = useRef(initialValue);
  • useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
  • 本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。
  • ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以 <div ref={myRef}></div>形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。
  • useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: …} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。
  • 当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return <div ref={this.myRef} />;
  }
}
function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
  • 另外一个使用场景是获取 previous props 或 previous state:
import React, { useState, useEffect, useRef } from "react";
import "./styles.css";

export default function APP() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();

  const prevCount = prevCountRef.current;

  useEffect(() => {
    prevCountRef.current = count;
  });

  const onClick = () => {
    setCount(count + 1);
  };

  return (
    <>
      <h1>
        Now: {count}, before: {prevCount}
      </h1>
      <div onClick={onClick}>onClick div</div>
    </>
  );
}
  • 使用useRef来跨越渲染周期存储数据,而且对它修改也不会引起组件渲染
import React, { useState, useEffect, useMemo, useRef } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  const doubleCount = useMemo(() => {
    return 2 * count;
  }, [count]);

  const timerID = useRef();

  useEffect(() => {
    timerID.current = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
  }, []);

  useEffect(() => {
    if (count > 10) {
      clearInterval(timerID.current);
    }
  });

  return (
    <>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Count: {count}, double: {doubleCount}
      </button>
    </>
  );
}

(8) useLayoutEffect

  • 可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
  • useLayoutEffect 在浏览器渲染前执行
  • useEffect 在浏览器渲染完成后执行
const [count, setCount] = useState(0);

useLayoutEffect(() => {
  if (count === 0) {
    setCount(10 + Math.random()*200);
  }
}, [count]);

return (
    <div onClick={() => setCount(0)}>{count}</div>
);
const [count, setCount] = useState(0);

useEffect(() => {
  if (count === 0) {
    setCount(10 + Math.random()*200);
  }
}, [count]);

return (
    <div onClick={() => setCount(0)}>{count}</div>
);
  • useLayoutEffect 里的任务最好影响了 Layout
  • 如果正在将代码从 class 组件迁移到使用 Hook 的函数组件,则需要注意 useLayoutEffect 与 componentDidMount、componentDidUpdate 的调用阶段是一样的。但是,推荐一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffect
  • 如果使用服务端渲染,请记住,无论 useLayoutEffect 还是 useEffect 都无法在 Javascript 代码加载完成之前执行。这就是为什么在服务端渲染组件中引入 useLayoutEffect 代码时会触发 React 告警。解决这个问题,需要将代码逻辑移至 useEffect 中(如果首次渲染不需要这段逻辑的情况下),或是将该组件延迟到客户端渲染完成后再显示(如果直到 useLayoutEffect 执行之前 HTML 都显示错乱的情况下)。

(9) 自定义 Hook

通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。

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';
}
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

当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。 例如,下面的 useFriendStatus 是我们第一个自定义的 Hook:

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}
function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

4、FAQ

  • Hook 能否覆盖 class 的所有使用场景?

    目前暂时还没有对应不常用的 getSnapshotBeforeUpdate,getDerivedStateFromError 和 componentDidCatch 生命周期的 Hook 等价写法

  • 可以只在更新时运行 effect 吗?

    可以使用一个可变的 ref 手动存储一个布尔值来表示是首次渲染还是后续渲染,然后在你的 effect 中检查这个标识。

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。