React Hooks 使用介绍

HookReact16.8新增的特性,可以说是扩展了函数式组件的各项功能。有了这个功能,函数式组件和类式组件在实现功能上就没有什么差异了,而且使用hook后,能使代码更加的简洁,代码也更具复用性了。

useState

以一个计数器来介绍:

import React, {useState} from 'react'
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h2>当前计数{count}</h2>
      <button onClick={(e) => setCount(count + 1)}>+1</button>
      <button onClick={(e) => setCount(count - 1)}>-1</button>
    </div>
  );
}

上面的组件就实现了一个计数器,点击按钮就会加减,hook使的函数组件有了自己的状态。**
useState就是一个hook**
参数:初始化值,不设置就为undefined,也可以可以传入数组对象,另外还可以传入一个带有返回值的函数。
**返回值:数组,包含两个元素,一般我们简化书写会使用数组解构语法。

  • 第一个元素使当前状态的值(第一次调用为初始化的值)
  • 第二个元素为设置状态值的函数。比如,上面的例子count的初始值为0 ,可以通过setCount来重新设置count的值,而且也会触发组件的创新渲染。

我们可以在组件中多次使用这个hook来创建多个变量。**
**另外我们多次调用setXxx函数来修改对应状态的时候,其实也和类组件中的setState基本一样:

    //会被合并成一次执行
    setCount(count+10);
    setCount(count+10);
    setCount(count+10);
    setCount(count+10);
   //不会被合并
    setCount((prev) => prev + 10);
    setCount((prev) => prev + 10);
    setCount((prev) => prev + 10);
    setCount((prev) => prev + 10);
    setCount((prev) => prev + 10); 

可以看到使用hook是多么的清爽。比新的一年换上新内裤还爽~~。那么接下来介绍第二个hook

useEffect

函数组件自身没有生命周期这类函数,我们要实现这个功能就可以使用这个hook

现在演示一个需求,页面的title总是显示counter的数字。**
**首先使用类组件进行实现:

class ChangeTitle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }
  componentDidMount() {
    document.title = this.state.count;
  }
  componentDidUpdate() {
    document.title = this.state.count;
  }
  render() {
    return (
      <div>
        <h2>类组件当前标题计数:{this.state.count}</h2>
        <button onClick={(e) => this.setState({ count: this.state.count + 1 })}>
          +1
        </button>
      </div>
    );
  }
}

可以看到更改标题的操作有重复的部分。**
**函数式组件:

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

  //模拟生命周期
  useEffect(() => {
    document.title = count;
  });
  return (
    <div>
      <h2>函数当前计数{count}</h2>
      <button onClick={(e) => setCount(count + 1)}>+1</button>
      <button onClick={(e) => setCount(count - 1)}>-1</button>
    </div>
  );
}

useEffect这个hook可以告知React在渲染好组件后执行某些操作。该hook要求传入一个回调函数,在React执行完DOM更新操作后就会回调这个函数。默认情况下,无论是第一次渲染还是之后的每次更新都会执行一次这个回调函数。

清除effect

我们需要在组件卸载的时候清除componentWillUnmount中进行清除。而useEffect为我们可以这样做,在我们传入的回调函数中,我么们可以指定一个返回值,返回一个回调函数。

这是useEffect可选清除机制,每个useEffect都可以返回一个清除函数。

  useEffect(() => {
    xxx
    return () => {
      console.log('清除了');
    }
  });

React会在组件每次更新和卸载的时候执行清除操作。

而且useEffect也可以多次调用,我们可以根据代码用途来分开调用,使得代码更加明朗。

useEffect性能优化

某些代码我们只希望在组件第一次挂载完毕后执行一次就行了,之后的更新就无需再执行。但是默认情况下useEffect在第一次挂载和之后的更新都会执行回调。这多次的执行一定程度上会导致一些性能问题。

所以useEffect可以传入第二个参数,第二个参数是一个数组,传入该useEffect的依赖项。如果该useEffect不依赖任何内容的话可以传入一个空数组。当传入空数组的时候,useEffect中的回调函数就相当于componentDidMount,回调函数返回的回调函数就相当于componentWillUnmount。当然传入的依赖项如果改变了那就会重新执行回调函数和返回的回调函数。

useContext

没啥好说的,另类的获取context的方式。

const ThemeContext = React.createContext();
function ContextHooksDemo() {
  const theme = useContext(ThemeContext);
  return <div>主题颜色{theme.color}</div>;
}

当最上层的Provider更新的时候,该hook会重新触发渲染并传递新的值。

useReducer

useReducer仅仅是useState的一种替代方案。在某些情况下,state处理比较复杂的逻辑就可以使用这个hook来进行拆分。

//处理逻辑
function counterReducer(state, action) {
  switch (action.type) {
    case "increment":
      return { ...state, counter: state.counter + 1 };
    case "decrement":
      return { ...state, counter: state.counter - 1 };
    default:
      return state;
  }
}
function ReducerHookDemo() {
  const [state, dispatch] = React.useReducer(counterReducer, { counter: 111 });
  return (
    <div>
      {state.counter}
      <button onClick={(e) => dispatch({ type: "increment" })}>+1</button>
      <button onClick={(e) => dispatch({ type: "decrement" })}>-1</button>
    </div>
  );
}

另外这里通过useReducer创建的数据是不能共享的,所以不能代替Redux,只是用法有点类似。

useCallback

这个hook也是为了进行性能的优化。

函数有两个参数,第一个传入一个回调函数,第二个传入数组(依赖项)。这个hook会返回一个函数的memoized

下面展示两个案例:

例一:使用useCallback和不使用useCallback定义一个函数是否会带来性能的优化。

function CallbackDemo01() {
  const [count, setCount] = useState(0);
  function increment() {
    console.log("执行");
    setCount(count + 1);
  }
  //下面使用usecallback不能带来性能优化,和上面的差不多
  const increment2 = React.useCallback(
    function () {
      console.log("执行");
      setCount(count + 1);
    },
    [count]
  );
  return (
    <div>
      <h2>CallbackDemo01</h2>
      <h2>{count}</h2>
      <button onClick={increment}>+1</button>
      <button onClick={increment2}>useCallback优化+1</button>
    </div>
  );
}

单纯从上面这个案例看不出来什么性能优化,因为这两个函数在组件创建的时候都会重新定义,count更新也会重新定义,所以结合下面这个案例来进行理解。

案例二:使用useCallback和不使用useCallback定义一个函数传递给子组件是否会带来性能的优化。

const Mybutton = React.memo(function (props) {
  console.log("MyButton重新渲染:" + props.title);
  return <button onClick={props.increment}>+1</button>;
});
function CallbackDemo02() {
  console.log("callbackDemo02重新渲染");
  const [count, setCount] = useState(0);
  const [show, setShow] = useState(false);

  const increment1 = () => {
    console.log("执行increment1");
    setCount(count + 1);
  };
  //当执行show切换时,父组件会重新渲染,子组件默认会处理。如果子组件使用memo进行包裹,那么子组件会对props进行浅层比较,相同就不需要重新渲染,但是这里两个函数在父组件重新渲染也会重新创建,所以传递给父组件的两次props是不同的,所以也会更新,
  //但实际上函数依赖的count并没有发生变动,组件更新是没有必要的,所以使用usecallback就避免了这个问题
  const increment2 = React.useCallback(() => {
    console.log("执行increment2");
    setCount(count + 1);
  }, [count]);
  return (
    <div>
      <h2>callbackdemo02{count}</h2>
      <Mybutton title="btn1" increment={increment1}></Mybutton>
      <Mybutton title="btn2" increment={increment2}></Mybutton>
      <button onClick={(e) => setShow(!show)}>切换</button>
    </div>
  );
}

总之通常使用useCallback的目的是不希望子组件进行多次渲染。另外要注意子组件一定要使用memo包裹,否则使用useCallback是无效的。

useMemo

这个hook也是为了性能优化。

useMemo返回的也是一个memoized,在依赖不变的情况下,多次定义返回的都是同一个值。

案例展示:**
**有时候我们定义一个函数,组件内其他的状态改变会引起这个组件的重新渲染,然后内部的函数就会重新在定义或者执行一次,如果函数的依赖状态没有改变,那这次的重新定义和执行是没有必要的。

function calcNumber(count) {
  console.log("执行计算");
  let total = 0;
  for (let i = 1; i <= count; i++) {
    total += i;
  }
  return total;
}

//useMemo 也是返回一个memoized

const MyInfo = React.memo((props) => {
  console.log("My重新渲染");
  return (
    <h2>
      名字:{props.info.name}年龄:{props.info.age}
    </h2>
  );
});
function MemoHookDemo01() {
  const [count, setCount] = useState(10);
  const [show, setShow] = useState(true);
  const info = React.useMemo(() => {
    return { name: "zhangsan", age: 18 };
  }, []);

  //计算1到count的和,当count不变时该函数不会重新执行
  const total = React.useMemo(() => {
    return calcNumber(count);
  }, [count]);
  return (
    <div>
      <h2>memoHook</h2>
      <h2>计算的和为{total}</h2>
      <button onClick={(e) => setCount(count + 1)}>+1</button>
      <MyInfo info={info}></MyInfo>
      <button onClick={(e) => setShow(!show)}>切换</button>
    </div>
  );
}

这个有点类似vue中的computed,当然原理完全不一样。

useRef

hook返回一个ref对象,返回的ref对象在整个生命周期保持不变。

常见有两种用法:

用法一:引入DOM元素,或者组件(需要为class组件)。

用法二:保存一个数据整个对象在整个生命周期中可以保持不变。

function RefHookDemo01() {
  //获取dom
  const titleRef = React.useRef();
  const inputRef = React.useRef();
  function changeDom() {
    titleRef.current.innerHTML = "hello refhook";
    inputRef.current.focus();
  }

  //可以保存上一次的state值,因为countRef更新不会引起dom的更新,所以当界面更新完时count和countRef.current值一样,但实际dom中countRef.current展示的为上一次的值
  const [count, setCount] = useState(0);
  const countRef = React.useRef(count);
  useEffect(() => {
    countRef.current = count;
    console.log(countRef.current);
  }, [count]);
  return (
    <div>
      <h2 ref={titleRef}>RefHookDemo01</h2>
      <input ref={inputRef} />
      <button onClick={(e) => changeDom()}>修改dom</button>
      <MyInfo info={{ name: "nihao", age: 20 }}></MyInfo>
      <h2>上一次count:{countRef.current}</h2>
      <h2>这一次的count:{count}</h2>
      <button onClick={(e) => setCount(count + 10)}>+10</button>
    </div>
  );
}

useImperativeHandle

这个hook不是那么好理解,这里举一个使用例子:我们通过ref要获取函数式组件中的dom元素时,不能直接获取,需要ref的转发,就是利用到forwardRef这个API对函数式组件进行包裹,然后子组件拿到父组件的ref绑定到某一个元素上,这样父组件就有这个元素的完全使用权了。但是我们只希望父组件能够操作这个元素某些属性,比如只能操作focus这个事件。那么useImperativeHandle这个hook就是用来限制传递给父组件操作的。

hook有三个参数,第一个为父组件传递过来的ref,第二个为回调函数,内部返回一个对象,对象中的方法就是暴露给父组件能操作的,第三个为依赖项`

const MyInput = React.forwardRef(function (props, ref) {
  //有时候我们并不像向父组件暴露过多的属性
  const inputRef = React.useRef();
  React.useImperativeHandle(
    ref,
    () => ({
      focus: () => {
        inputRef.current.focus();
      },
    }),
    [inputRef]
  );
  return <input ref={inputRef} />;
});
function ImperativeHookDemo() {
  const inputRef = React.useRef();
  return (
    <div>
      <MyInput ref={inputRef} />
      <button onClick={(e) => inputRef.current.focus()}>获取焦点</button>
    </div>
  );
}

useLayoutEffect

这个hookuseEffect很相似,只有一点点区别,useEffect会在组件内容更行到DOM后执行,不会阻塞DOM的更新。而useLayoutEffect则是再组件内容更新到DOM上之前执行,会阻塞DOM更新。所以视情况来选择hook

function LayoutEffectHookDemo() {
  const [count, setCount] = useState(10);
  //这里使用useEffect可能会出现先从o再变为随机数的过程在网页上可能会闪一下,使用useLayoutEffect就不会
  React.useLayoutEffect(() => {
    if (count === 0) {
      setCount(Math.random());
    }
  }, [count]);
  return (
    <div>
      <h2>layoutEffect:{count}</h2>
      <button onClick={(e) => setCount(0)}>改变</button>
    </div>
  );
}

以上的案例可以尝试体会下二者的区别。

当然还有一些hook没有提到,大部分常用的hook都已经介绍了,剩下的可以查看官网。

自定义hook

本质上自定义hook只是一种函数代码逻辑的抽取,并不算React特性,只是为了代码和逻辑的复用。

以下演示几个自定义hook的案例。注意自定义的hook函数的命名必须以use开头。

函数创建和销毁时打印一些东西:

//可以提到一个单独的文件中
function useLoggingLife(name) {
  React.useEffect(() => {
    console.log(`${name}组件创建出来`);
    return () => {
      console.log(`${name}销毁了`);
    };
  }, [name]);
}

function CustomHookDemo01() {
  useLoggingLife("CustomHookDemo01");
  return (
    <div>
      <h2>CustomHookDemo01</h2>
      <Home></Home>
      <About></About>
    </div>
  );
}

context的共享:

import React from "react";
import { UserContext, TokenContext } from "./App";

export default function useContext() {
  const user = React.useContext(UserContext);
  const token = React.useContext(TokenContext);
  return [user, token];
}
import React from "react";
import useContextHook from "./useContextHook";
export default function CustomContextShareHook() {
  const [token, user] = useContextHook();
  console.log(token, user);
  return (
    <div>
      <h2>获取context</h2>
    </div>
  );
}
function App() {
  return (
    <div className="App">
      <UserContext.Provider value={{ name: "zhangsan", age: 19 }}>
        <TokenContext.Provider value="abckdfhafh">
          <CustomContextShareHook></CustomContextShareHook>
        </TokenContext.Provider>
      </UserContext.Provider>
    </div>
  );
}

本地缓存:

import React from "react";

export default function useLocalStore(key) {
    //获取
  const [name, setName] = React.useState(() => {
    const name = JSON.parse(window.localStorage.getItem(key));
    return name;
  });
    //设置
  React.useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(name));
  }, [name]);

  return [name, setName];
}
import React from "react";
import useLocalStore from './useLocalStore'
export default function LocalStore() {

  const [name, setName] = useLocalStore('saber')

  return (
    <div>
      <h2>获取设置name:{name}</h2>
      <button onClick={(e) => setName("zhangsan")}>设置name</button>
    </div>
  );
}

//todo useState源码

//todo redux hooks

Last modification:March 7, 2022
如果觉得我的文章对你有用,请随意赞赏