React 常见hooks汇总

·  阅读 80

一、useState

  • useState 可以让函数组件也可以拥有 state 状态

  • 语法: const [xxx, setXxx] = React.useState(initValue)

  • useState 接受一个初始值参数,该参数只会在组件初始渲染中起作用,后续渲染时会被忽略,在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state

  • useState 无论调用多少次,相互之间是独立的。react是根据useState出现的顺序来保证多个useState 之间是相互独立

  • setXxx 两种用法:

    1. setXxx(newValue),参数为非函数值, 直接指定新的状态值, 内部用其覆盖原来的状态值。如果多次连续调用,最后只会更新最近一次的值(合并更新一次状态)
    2. setXxx(value => newValue), 参数为函数, 接收原本的状态值, 返回新的状态值, 内部用其覆盖原来的状态值。如果多次连续调用,参数 value 是最新的值,会更新多次状态
  • setXxx 是采用“异步直接赋值”的形式,并不会像类组件中的setState()那样做“异步对比累加赋值”。这里说的异步和类组件中 setState 中的异步是同一个意思,都是为了优化 React 渲染性能而故意为之。也就是无法在 setXxx 之后立即拿到最新的值

// useState 异步回调获取不到最新值及解决方案
import React, { useState, useEffect } from 'react';

const App = () => {
  const [arr, setArr] = useState([0]);

  useEffect(() => {
    console.log(arr); // 此处可以获取最新到的 state 值
  }, [arr]);

  const handleClick = () => {
      setArr([...arr, 1]); 
  }

  return (
    <>
      <button onClick={handleClick}>change</button>
    </>
  );
}

export default App;

复制代码
  • 修改状态时,如果是复杂数据类型则需要先复制出一份,修改某属性后再整体赋值

  • 修改状态时,但是如果新值和当前值完全一样,是不会引发页面重新渲染的,通过React官方文档可以知道,修改状态的时候,Hook会使用 Object.is() 来对比当前值和新值,结果为 true 则不渲染,结果为 false 就会重新渲染

1-1 useState 是相互独立的

let showFruit = true;
function ExampleWithManyStates() {
  const [age, setAge] = useState(42);
  
  if(showFruit) {
    const [name, setName] = useState('hhh');
    showFruit = false;
  }
   const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}


// 测试
// 第一次渲染组件
useState(42);  //将 age 初始化为42
useState('hhh');  //将 name 初始化为 hhh
useState([{ text: 'Learn Hooks' }]); // 将 todos 初始化为 [{ text: 'Learn Hooks' }]

// 重新渲染渲染组件
useState(42);  // 读取状态变量 age 的值(这时候传的参数42直接被忽略)
// useState('banana');  // 重新渲染没有去执行 useState('banana')
useState([{ text: 'Learn Hooks' }]); // 此时读取到的却是状态变量 fruit 的值,导致报错

复制代码

1-2 解决 useState 多次渲染的问题

// 以下代码执行完会导致页面渲染两遍
// 原因是,在React中,同步代码会合并渲染,异步代码不会合并渲染。
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
useEffect(async () => {
  const res = await axios.get("xxx");
  setLoading(false);
  setData(res);
}, []);

// 以下代码只会渲染一次,它会将 setLoading 和 setData 进行合并。这个其实和类组件是一样的,在异步函数中不会合并setState。
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
useEffect(() => {
  setLoading(false);
  setData({ a: 1 });
}, []);

// 解决方案一:多个状态合并到一个状态中
const [request, setRequest] = useState({ loading: true, data: null });
useEffect(async () => {
  const res = await axios.get("xxx");
  setRequest({ loading: false, data: res });
}, []);
// 缺点:如果只想 setState 一个依赖项时,需要将别的依赖项也要传进去,否则这个值会丢失。React内部并不会帮你做去合并
// setRequest({ data: res }); // 就会导致loading值丢失了。
// 解决方案是使用setRequest({ ...request, data: res }) 或者 setRequest((prevState) => ({ ...prevState, data: res }));

// 解决方案二:写一个自定义合并依赖项的hook
const useMergeState = (initialState) => {
  const [state, setState] = useState(initialState);
  const setMergeState = (newState) =>
    setState((prevState) => ({ ...prevState, newState }));
  return [state, setMergeState];
};

const [request, setRequest] = useMegeState({ loading: false, data: null });
useEffect(async () => {
  const res = await axios.get("xxx");
  setRequest({ loading: true, data: res });
  // setRequest({ data: { a: 1 } }); // loading 状态不会丢失,还是 true
}, []);

// 解决方案三:使用 useReducer
const initState = { loading: false, data: null }
function loginReducer(state, action) {
  if(action.type==='login'){
    return {...state,...action.newState}
  }
}
const [state, dispatch] = useReducer(loginReducer,initState);
useEffect(async () => {
  const res = await axios.get("xxx");
  dispatch({ type: 'login', newState: {loading: true, data: res} });
  // setRequest({ data: { a: 1 } }); // loading 状态不会丢失,还是 true
}, []);

复制代码

二、useEffect

  • useEffect 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子),副作用指的是置身于组件渲染之外,绑定事件,发起网络请求,访问修改dom都称为副作用

  • 发送网络请求,访问修改dom,记录日志,这些副作用都是在组件卸载前无需清除的,因为在执行完这些操作之后,就可以忽略;但是像绑定事件,设置定时器,订阅外部数据这些副作用时候必须清除的,否者会引起内存泄漏

  • 语法:useEffect(effect,[deps])

  • 第一个参数为定义的执行函数(可以在此处执行副作用),第2个参数是依赖关系(可选参数)。若一个函数组件中定义了多个useEffect,那么他们实际执行顺序是按照在代码中定义的先后顺序来执行的

  • useEffect 第一次被调用是 render 之后,相当于 componentDidMount ,之后的调用相当于componentDidUpdate, useEffect 会返回一个回调函数,这个回调函数的执行时机很重要,他的作用是清除上一次作用遗留下来的状态,比如一个组件在第三次,第五次,第七次渲染之后执行useEffect中的执行函数,那么回调函数就会在就会在组件第四次,第六次,第八次渲染之前执行,就是为了清除上一次渲染遗留下来的状态,如果 useEffect 只在第一次渲染之后执行,那么他返回的回调函数只会在组件卸载之前调用相当于 componentWillUnmount

  • useEffect 意义:关注点分离,不同的事情分开放

import React, { useState, useEffect } from 'react'

export default function App() {
    const [count, setCount] = useState(0)
    const [size, setSize] = useState({
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight
    })

    const onResize = () => {
        setSize({
            width: document.documentElement.clientWidth,
            height: document.documentElement.clientHeight
        })
    }
    // 相当于 componentDidMount 和 componentDidUpdate
    useEffect(() => {
        document.title = count
    })
    // 相当于 componentDidMount 和 componentWillUnmount
    useEffect(() => {
        window.addEventListener('resize', onResize, false)
        return () => {
            window.removeEventListener('resize', onResize, false)
        }
    }, [])
    return (
        <div>
            <button
                onClick={() => { setCount(count + 1) }}
            >
                count+1
            </button>
            <h1>{size.height}||{size.width}</h1>
        </div>
    )
}
复制代码

2-1 如何判断多个值都改变了才执行 useEffect 内部方法?

import React, { useState, useEffect, useRef } from "react"

const One = () => {
    const [a, setA] = useState(0)
    const [b, setB] = useState(0)
    const ref = useRef({ a, b })

    useEffect(() => {
        let { a: prevA, b: prevB } = ref.current
        console.log("更新前:", prevA, prevB)
        console.log("更新后:", a, b)

        if (prevA !== a && prevB !== b) {
            console.log("update!")
            ref.current = { a, b }
        }
    }, [a, b])

    return (
        <>
            <h1>{a + b}</h1>
            <button onClick={(_) => setA((d) => d + 1)}>Chang A</button>
            <button onClick={(_) => setB((d) => d + 1)}>Chang B</button>
        </>
    )
}

export default One

复制代码

三、useRef

  • useRef 两大作用:
    1. 获取DOM元素的节点,获取子组件的实例
    2. 渲染周期之间共享数据的存储(state不能存储跨渲染周期的数据,因为state的保存会触发组件重渲染)
  • 语法: const ref = React.useRef(initialValue)
    useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue);返回的 ref 对象在组件的整个生命周期内保持不变。可以保存任何类型的值包括dom、对象等任何可辨值

3-1 获取dom元素节点

const RefDemo = () => {
    const domRef = useRef(null)
    useEffect(() => {
      	// domRef.current 指向已挂载到 DOM 上的文本输入元素
        console.log("ref:deom-init", domRef, domRef.current)
    })
    return (
        <div>
            <div
                onClick={() => {
                    console.log("ref:deom", domRef, domRef.current)
                    domRef.current.focus()
                    domRef.current.value = "hh"
                }}
            >
                <label>这是一个dom节点</label>
                <input ref={domRef} />
            </div>
        </div>
    )
}
复制代码

3-2 获取子组件实例

  • useImperativeHandle(ref,createHandle,[deps])可以自定义暴露给父组件的实例值。如果不使用,父组件的ref(chidlRef)访问不到任何值(childRef.current==null)

  • useImperativeHandle 应该与 forwradRef 搭配使用

  • 因为函数组件没有实例。所以需要通过 React.forwardRef 会创建一个 React 组件,这个组件能够将其接受的ref属性转发到其组件树下的另一个组件中

  • React.forward 接受渲染函数作为参数,React 将使用 prop 和 ref 作为参数来调用此函数

const Child = forwardRef((props, ref) => {
    useImperativeHandle(ref, () => ({
        say: sayHello,
    }))
    const sayHello = () => {
        alert("hello,我是子组件")
    }
    return <h3>子组件</h3>
})
const RefDemo = () => {
    const childRef = useRef(null)
    useEffect(() => {
        console.log("ref:child-init", childRef, childRef.current)
    })
    const showChild = () => {
        console.log("ref:child", childRef, childRef.current)
        childRef.current.say()
    }
    return (
        <div style={{ margin: "100px", border: "2px dashed", padding: "20px" }}>
            <p onClick={showChild} style={{ marginTop: "20px" }}>
                  点我看子组件
            </p>
            <Child ref={childRef} />
        </div>
    )
}
复制代码

3-3 渲染周期之间共享存储的数据

// 把定时器设置成全局变量使用 useRef 挂载到 current 上
import React, { useState, useEffect, useRef } from "react";
function App() {
  const [count, setCount] = useState(0);
  // 因为函数组件只要更新了,timer 就会被重新为 null,所以函数组件需要借助 useRef 存储变量 
  // const timer = null
  
  // 把定时器设置成全局变量使用 useRef 挂载到 current 上
  const timer = useRef();
  
  // 首次加载 useEffect 方法执行一次设置定时器
  useEffect(() => {
    timer.current = setInterval(() => {
      setCount(count => count + 1);
    }, 1000);
  }, []);
  
  // count 每次更新都会执行这个副作用,当 count > 5 时,清除定时器
  useEffect(() => {
    if (count > 5) {
      clearInterval(timer.current);
    }
  });
  return <h1>count: {count}</h1>;
}
export default App;
复制代码
分类:
前端
分类:
前端
收藏成功!
已添加到「」, 点击更改