Javascript

一文搞定 React Hooks API

本文主要是介绍一文搞定 React Hooks API,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

背景

工作结束,又到了愉快的总结时间。

vue3 beta 版本发布之后,大家都说借鉴了 React Hooks,一直使用 vue 开发项目的我也来跟风学习下。

React 官方文档看起来实在费劲,自己总结了常见的几个 Hook用法,希望能帮助大家理解。

这篇文章主要整理一下 React 中的几个常见 Hook:

  • 状态 useState
  • 副作用 useEffect
    • useLayoutEffect
  • 上下文 useContext
  • useReducer
  • 记忆 useMemo
    • 回调 useCallback
  • 引用 useRef
  • 自定义 Hook

useState

用法

  1. 创建初始值 initialState
  2. [x, setX] = React.useState(initialState);
  3. x 是初始值,setN是操作 x 的函数
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

function App() {
// 0是n的默认值,setN是操作n的函数
  const [n, setN] = React.useState(0);
  return (
    <div className="App">
      <p>{n}</p>
      <p>
        <button onClick={() => setN(n + 1)}>+1</button>
      </p>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);

复制代码

不能局部更新

如果 state 是一个对象,不能部分更新 setState,必须将对象所有属性一起传递。

useReducer

  1. 创建初始值 initialState
  2. 创建所有操作 reducer(state, action)
  3. 传给 useReducer,得到读和写 API
  4. 调用 写({type: '操作类型'})
import React, { useState, useReducer } from "react";
import ReactDOM from "react-dom";
// 1. 创建初始值 initialState
const initial = {
  n: 0
};
// 2. 创建所有操作 reducer(state, action)
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() {
// 3.传给 useReducer,得到读 state 和写 dispatch
  const [state, dispatch] = useReducer(reducer, initial);
  const { n } = state;
  const onClick = () => {
// 4. 调用 写({type: '操作类型'})
    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}>x2</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
复制代码

总的来说,useReducer相当于useState的复杂版。

好处是能够践行Flux/Redux的思想。

那么useState 和 useReducer 应该如何选择呢?

事不过三原则。如果同一个操作重复了3次以上,就使用useReducer。

useContext

使用方法

  1. 使用 C = React.createContext(initial); 创建上下文
  2. 使用 <C.Provider> 圈定作用域
  3. 在作用域内使用 useContext(C) 来使用上下文
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

const C = React.createContext(null);

function App() {
  const [theme, setTheme] = React.useState("red");
  return (
		// C.Provider 表示 value 这个的作用域从这里开始
    <C.Provider value={{theme, setTheme}}>
      <div className={`App ${theme}`}>
        <p>{theme}</p>
        <ChildA />
      </div>
    </C.Provider>
  );
}

function ChildA() {
  const { setTheme } = React.useContext(themeContext);
  return (
    <div>
      <button onClick={() => setTheme("red")}>red</button>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);
复制代码

useContext 翻译为使用上下文,那什么是上下文呢?

上下文

上下文就是运行一段程序,需要的所有变量。

比如我们都知道的全局变量,就是全局的上下文。因为全局代码都需要这个变量。

上下文也是局部的全局变量,在一段代码内,都需要这个变量。

注意事项

useContext 的更新机制不是响应式的,而是重新渲染,从根组件到子组件逐渐通知的过程。

你在一个模块将 C 里的值变化,另一个模块不会感知到这个变化。

当然,这个变化对于我们写代码基本没有影响。

useEffect

effect 翻译为「副作用」,什么是副作用呢?

在函数式编程中,对环境的改变即为副作用。比如修改 document.title。

但我们不一定非要把副作用放在 useEffect 里。

实际上对于没有接触过函数式编程的前端工程,叫做 afterRender 更好,意思是每次 render 后运行。

用途

可以代替三个钩子:

  1. componentDidMount 出生,第一次渲染,[] 作 useEffect 第二个参数。
  2. componentDidUpdate 更新,第2、3...次渲染,第二个参数指定依赖
  3. componentWillUnmount 死亡,销毁前渲染,通过 return

以上三种用途可同时存在。

如果同时存在多个 useEffect,会按照出现顺序依次执行。

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";


function App() {
  const [n, setN] = useState(0);
  const onClick = () => {
    setN(i => i+1)
  }
// 1. [] 里面的变量变化时执行 => 只执行一次,不会再次执行
  useEffect(() => {
    console.log('第一次渲染后执行这句话')
  }, []);
// 2. n 变化时执行
	useEffect(() => {
    console.log('n 变化了')
  }, [n]);
// 3. 销毁前渲染,通过 return
	useEffect(() => {
	  const timer = setInterval(() => {
			console.log('hi')
		}, 1000)  
  }, return () => {
		window.clearInterval(id);
	});
  return (
    <div className="App">
      <h1>n: {n}</h1>
      <button onClick={onClick}>+1</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
复制代码

useLayoutEffect

layout effect 翻译为「布局副作用」。

useEffect 在浏览器渲染完成后执行,useLayoutEffect 在浏览器渲染前执行。 useLayoutEffect 总是比 useEffect 先执行。

经验

为了用户体验,优先使用 useEffect。

因为 useLayoutEffect 在渲染前执行,和 useEffect 相比有更长时间的白屏。

当 useEffect 不能满足你的需求时,再使用 useLayoutEffect。

useLayoutEffect 里的任务最好影响了 Layout(页面布局)。

useMemo

React.memo

要理解 React.useMemo,需要先理解 React.memo。

React 默认有多余的render,比如说 n 变了,但是依赖 m 的组件也刷新了。

代码中的组件,比如 Child 用 React.memo(Child) 代替。

就可以做到 props 不变,不会再执行一个函数组件。

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

  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child data={m}/>
    </div>
  );
}

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

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
复制代码

bug

在 Child 添加了监听函数之后,监听函数每次都会执行。

function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  const onClickChild = () => {
    console.log(m);
  };
  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <!-- {/* Child2 居然又执行了 */}-->
     <Child data={m} onClick={onClickChild} />
    </div>
  );
}

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

复制代码

因为 App 运行时会生成新的函数,新旧函数虽然功能意义,但是地址不一样。

怎么办?用useMemo

useMemo

第一个参数是 () => value,第二个参数是依赖[m,n]

只有当依赖变化,才会计算新的value。 如果依赖不变,那么就重用之前的value。

(和 vue2 的 computed 功能相同哇)

注意

如果你的value是个函数,就要写成 useMemo(() =>(x) => console.log(x))。 这是一个很难返回的函数,于是就有了useCallback。

useCallback

useCallback 只是一个语法糖。 useCallback(x => log(x), [m]) 等价于 useMemo(() =>(x) => console.log(x), [m])

useRef

目的

如果你需要一个值,在组件不断 render 时保持不变。使用 useRef。

但是页面不会自动更新,需要手动刷新。

function App() {
// 初始化
  const count = useRef(0); // {current: 0}

	const onClick = () => {
// 读取:count.current 
// 为什么需要 current?
// 为了保证两次 useRef 是同一个值。不改变 useRef 对象的地址,只改变 count.current 的值
    count.current += 1;
		console.log(count.current)
  }

  return (
    <div className="App">
// 页面不会自动更新,没有 re-render
      <h1>n: {count.current}</h1>
      <button onClick={onClick}>+1</button>
    </div>
  );
}
复制代码

Vue3 的 ref

vue3

import { ref, watchEffect } from 'vue'
// 初始化:const count = ref(0)
const count = ref(0)

function increment() {
// 读取:count.value
  count.value++
}

const renderContext = {
  count,
  increment
}

// 不同点:当count.value 变化时,vue3 会自动 render, 页面变化
watchEffect(() => {
  renderTemplate(
    `<button @click="increment">{{ count }}</button>`,
    renderContext
  )
})
复制代码

forwardRef

如果一个函数组件,想要接受其他组件传递的 ref 参数。 必须使用 React.forwardRef 包裹起来。

因为 props 不包含 ref

function App() {
  const count = useRef(0); // {current: 0}

  return (
    <div className="App">
      <Button3 ref={buttonRef}>按钮</Button3>
    </div>
  );
}

const Button3 = React.forwardRef((props, ref) => {
	return <button ref={ref}></button>
})

复制代码

最牛逼的功能:自定义 Hook

封装数据操作

不管用的什么 hook,只需要将增删改查暴露出去。 使用时直接调用,不用关心内部逻辑。

一个例子

import React, { useRef, useState, useEffect } from "react";
import ReactDOM from "react-dom";
// 自定义 hooks useList
const useList = () => {
  const [list, setList] = useState(null);
  useEffect(() => {
    ajax("/list").then(list => {
      setList(list);
    });
  }, []); // [] 确保只在第一次运行
// 暴露出读写接口
  return {
    list: list,
    setList: setList
  };
};

function App() {
  // 使用 hooks index.js
  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>
  );
}

// mock
function ajax() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([
        { id: 1, name: "Frank" },
        { id: 2, name: "Jack" }
      ]);
    }, 2000);
  });
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
复制代码

Stale Closure

译为过时的闭包。 在使用 useEffect 时,需要注意过时的闭包问题。 具体问题可以点击这里查看,我在这简单描述下。

function createIncrement(i) {
// 一个函数用到了它外面的变量,就是闭包
  let value = 0;
  function increment() {
    value += i;
    console.log(value);
    const message = `Current value is ${value}`;
    return function logValue() {
      console.log(message);
    };
  }
  
  return increment;
}

const inc = createIncrement(1);
const log = inc(); // logs 1 (第一次执行,得到message1)
inc();             // logs 2 (第二次执行,得到message2)
inc();             // logs 3 (第三次执行,得到message3)
log();             // logs "Current value is 1" 因为这里保存了一个过时的 log
复制代码

实际上,第一次执行 inc(),得到 message1;第二次,得到 message... log 保存的是 message,这就是过时的闭包。

如果我们想得到的是 3,应该怎么做?

  1. 使用最新的 log 函数
const inc = createIncrement(1);

inc();  // logs 1
inc();  // logs 2
const latestLog = inc(); // logs 3
// Works!
latestLog(); // logs "Current value is 3"
复制代码
  1. 用最新的 value 第二种方法是直接logValue()利用value。

让我们将行const message = ...;移到logValue()函数主体中:

function createIncrementFixed(i) {
  let value = 0;
  function increment() {
    value += i;
    console.log(value);
    return function logValue() {
//******************
      const message = `Current value is ${value}`;
      console.log(message);
//******************
    };
  }
 
  return increment;
}

const inc = createIncrementFixed(1);
const log = inc(); // logs 1
inc();             // logs 2
// Works!
log();             // logs "Current value is 3"
复制代码

useEffect

让我们研究一下使用 useEffect() 时过时关闭的情况。

钩子在组件内部 useEffect() 记录以下值count:

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

  useEffect(function() {
    setInterval(function log() {
      console.log(`Count is: ${count}`);
    }, 2000);
  }, []);

  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1) }> Increase </button>
    </div>
  );
}
复制代码

然后单击几次增加按钮。然后看一下控制台,每隔2秒钟就会出现一次Count is: 0。

为什么会发生?

第一次渲染时,闭包log()将count变量捕获为0。 后来,即使count增加,log()仍然从最初的渲染使用count:0。log()是一个过时的闭包。

解决方案是让useEffect()闭包log()依赖count:

useEffect(function() {
    setInterval(function log() {
      console.log(`Count is: ${count}`);
    }, 2000);
// 正确设置依赖 项count 后,请useEffect()在count更改后立即更新闭包。
  }, [count]);
复制代码
这篇关于一文搞定 React Hooks API的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!