likes
comments
collection
share

React之常用Hooks

作者站长头像
站长
· 阅读数 12

React Hooks 是 React v16.8 引入的特性,它使你可以在无需使用类组件的情况下,在函数组件中使用状态和其他 React 特性。这个特性提供了一系列的 Hook 函数,用于处理组件中的状态管理、副作用(比如订阅事件或发起网络请求)等。

useState

useState用于在函数组件中创建和管理状态(state)。它接收一个初始值作为参数,并返回一个包含两个元素的数组:当前状态的值和更新状态的函数

基本使用

使用 useState 的一般步骤如下:

  1. 导入 useState 函数:
import React, { useState } from 'react';
  1. 在函数组件中调用 useState 并传入初始值:
const [state, setState] = useState(initialValue);

这里的 state 是状态的当前值,setState 是用于更新状态的函数。initialValue 是状态的初始值,可以是任何合法的JavaScript值。

  1. 使用状态:
const App = () => {
  // 创建名为 count 的状态,并设置初始值为 0
  const [count, setCount] = useState(0);

  // 更新状态的函数
  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

在上面的代码中,声明了一个名为 count 的状态,并通过数组解构赋值将其当前值绑定到 count,将更新状态的函数绑定到 setCount。每次点击按钮时,调用 increment 函数来更新状态。

useState 可以多次调用,以创建和管理多个不同的状态。

需要注意的是,使用 useState 创建的状态是独立的,每次重新渲染组件时都会被重新初始化。此外,更新状态时使用的是类似合并的方式,而不是替换整个状态对象。

注意事项

  • 在函数组件的顶层使用 useState:不要将其放在条件语句或嵌套函数中。这样可以保证每次渲染时状态的正确性和一致性。不要在循环中使用 useState:避免在循环中直接调用 useState 来创建多个相似的状态。这会导致每次循环迭代时都会创建新的状态,可能会引起性能问题。

  • 更新状态

    • 不依赖前一个状态,更新状态时,useState 的更新函数可以接收一个新值直接替换旧值;如上述示例;
    • 依赖前一个状态,应该使用回调函数形式的更新方式,而不是直接传递计算后的值。
    setCount(prevCount => prevCount + 1);
    

同时,不要在同一个方法中多次使用同一个 setState(使用直接替换旧值:setCount(count + 1);),这样会出现只有最后一个 setState方法生效;要解决这种问题可以使用回调函数形式的更新方式:

 import React, { useState } from 'react';
const App = () => {
  // 创建名为 count 的状态,并设置初始值为 0
  const [count, setCount] = useState(0);

  // 更新状态的函数
  const increment = () => {
    //不推荐
    // setCount(count + 1)
    // console.log(count)
    // setCount(count + 2)
    // console.log(count)
    //推荐
    setCount(count => count + 1)
    // 一些操作
    setCount(count => count + 2)
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

export default App;
  • useState 的参数只会在初始化阶段被调用一次:useState 函数的参数只会在组件的初始渲染时被调用一次,并且对于后续的渲染,参数将被忽略。这意味着,如果参数是一个函数调用,那么该函数只会在初始渲染时执行一次,不会再次执行。
const [count, setCount] = useState(getInitialCount()); // getInitialCount() 只会在初始渲染时被调用一次
  • useState 更新对象树形值:
 import React, { useState } from 'react';
const App = () => {
  const [user, setUser] = React.useState({
    name: '小明',
    age: 18
  })

  // 每次点击都将年龄 + 1
  const plus = () => {
    //这种方法可以看到浏览器控制台中更新了值,但是页面没渲染,不推荐
    user.age += 1
    setUser(user)
    console.log('点击打印', user) // 每次点击打印

    //只更新某一个属性值,这样会导致原对象的其他属性会被置空;不推荐
    setUser({ age: user.age + 1 })
    console.log('点击打印', user) // 每次点击打印

    //正确用法
    setUser({
      ...user,
      age: user.age + 1
    })
    console.log('点击打印', user) // 每次点击打印
    //或者
    setUser(prevState => ({
      ...prevState,
      nage: user.age + 1
    }))
    console.log('点击打印', user) // 每次点击打印
  }

  return (
    <div style={{ margin: 100 }}>
      <div style={{ marginBottom: 16 }}>姓名:{user.name}</div>
      <div style={{ marginBottom: 16 }}>年龄:{user.age}</div>
      <button onClick={plus}>长大一岁</button>
    </div>
  )
}

export default App;

参考文章

03|在 React 中正确使用 useState的姿势

useEffect

useEffect 是 React Hooks 中用于处理副作用操作的一个钩子函数。副作用操作指的是那些不直接与组件渲染结果相关的操作,比如订阅事件、发起网络请求、操作 DOM 等

使用 useEffect 可以在函数组件中模拟类组件中的生命周期方法(如 componentDidMountcomponentDidUpdatecomponentWillUnmount),它会在每次组件渲染后执行。

useEffect 接收两个参数:副作用函数和依赖项数组

副作用函数是一个包含副作用操作的回调函数,可以是同步或异步的。它会在组件渲染完毕后执行,并且可以返回一个清理函数,在组件卸载之前执行清理操作。

副作用操作是相对于操作state而言的。每次因为state的改变,都会有一次对应副作用函数的执行时机,如果state 多次改变,那么就有多次对应副作用的执行时机。

执行流程

useState(状态改变) -> rende(渲染) -> useEffect(执行副作用)

以下几种方式都认为是副作用

  1. 引用外部变量
  2. 调用外部函数
  3. 修改DOM
  4. 修改全局变量
  5. 计时器
  6. 存储相关
  7. 网络请求

依赖项数组是一个可选的参数,用于指定副作用函数所依赖的值。当依赖项数组发生变化时,副作用函数会重新执行。如果省略依赖项数组,副作用函数会在每次组件渲染后都执行

基本使用

  1. 仅在组件挂载时执行一次副作用函数:
import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    console.log('Component mounted');

    return () => {
      console.log('Component unmounted');
    };
  }, []);

  return (
    // JSX
  );
}

在上述示例中,当组件挂载时,副作用函数会被执行,并输出 "Component mounted"。返回的清理函数会在组件卸载前执行,并输出 "Component unmounted"。

  1. 在依赖项变化时执行副作用函数:
import React, { useState, useEffect } from 'react';

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

  useEffect(() => {
    console.log(`Count: ${count}`);
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

在上述示例中,每次 count 发生变化时,副作用函数会被执行,并输出当前的计数值。

useEffect 的灵活性使它成为处理许多常见场景的有力工具,比如发送网络请求、订阅事件、操作 DOM 等。

注意事项

  • 在函数组件的顶层使用,且不能放在循环和条件语句中

  • 依赖项数组useEffect 的第二个参数是依赖项数组,用于控制副作用函数的触发时机。确保正确设置依赖项,以避免不必要的重复执行或遗漏更新。

    • 如果依赖项数组为空 [],副作用函数仅在组件挂载和卸载时执行一次。
    • 如果依赖项数组中包含某些数据或状态,副作用函数将在这些数据或状态变化时执行。
    • 如果依赖项数组不传递,副作用函数将在组件每次渲染后都执行。(这种如果改变state的值,会形成死循环)
  • 清理操作:如果副作用函数需要进行清理操作(如取消订阅、清除定时器),可以在副作用函数内部返回一个清理函数。该清理函数将在组件卸载前执行。确保在清理函数中对副作用操作进行正确的撤销或清理。
useEffect(() => {
  // 执行副作用操作...

  return () => {
    // 执行清理操作...
  };
}, [依赖项数组]);
  • 无限循环的副作用:务必小心处理可能导致无限循环的副作用函数。

    例如,在副作用函数中更新某个状态,并将该状态添加到依赖项数组中,会导致每次状态更新时触发副作用函数,从而形成循环。确保仅在必要时将状态添加到依赖项数组中。

  • 副作用的执行是异步的:useEffect异步的闭包函数,在render渲染完成之后才会执行useEffect

  • useEffect回调函数不能用async修饰,但是可以在回调函数内部重新写一个async函数,然后调用

    //不可以
    useEffect(async()=>{
        const res = await fetchNewData(id)
        setData(res.data)
    },[id])
    
    //推荐
    useEffect(()=>{
        const fetchData = async() => {
            const res = await fetchNewData(id)
            setData(res.data)
        }
        fetchData()
    },[id])
    
  • 页面初始化后不立即加载某些配置,可以在 useEffect 中添加条件判断来控制加载的时机。
import React, { useState,useEffect } from 'react';
const App = () => {
  const [count, setCount] = useState(0)
  useEffect(() => {
    if(count > 0){
      console.log('count发生了变化最新值为' + count);
    }
  }, [count])
  return (<div>
    <div>
      {count}
    </div>
    <button onClick={() => setCount(count => count + 1)}>加1</button>
  </div>)
}

export default App;
  • 每个 useEffect的任务都是独立

参考文章

理解 Effect Hooks 副作用

useEffect五个经典问题&实践总结

useLayoutEffect

useLayoutEffect 是 React 提供的一个 Hook,它在 DOM 更新之后同步执行副作用

useEffect 相比,useLayoutEffect 在排队页面更新之前同步执行,可以在更新之前立即执行一些操作。

基本使用

使用 useLayoutEffect 的方式和 useEffect 类似,你可以在组件中使用它来处理副作用。以下是一个示例:

import { useLayoutEffect } from 'react';

function MyComponent() {
  useLayoutEffect(() => {
    // 在DOM更新后立即执行的副作用操作
    console.log('执行副作用操作');
    
    // 返回一个清理函数(可选)
    return () => {
      // 在组件卸载或下一次副作用执行之前进行清理操作
      console.log('执行清理操作');
    };
  }, []);
  
  // 组件的其余部分
  return <div>My Component</div>;
}

在上面的示例中,我们定义了一个 useLayoutEffect,它在组件渲染后会立即执行副作用操作。可以在该函数中执行需要在 DOM 更新之后立即执行的操作,例如获取 DOM 元素的位置或尺寸等。

useEffect 类似,你可以选择返回一个清理函数。此清理函数将在组件卸载时或下一次副作用执行之前执行,用于清理副作用操作产生的任何资源。

需要注意的是,在大多数情况下,应该优先使用 useEffect,因为它是异步执行的,不会阻塞页面的渲染。只有在需要在更新之前同步执行副作用操作的特殊情况下,才应该使用 useLayoutEffect

与useEffect区别

useEffectuseLayoutEffect 是 React 提供的两个 Hook,它们在处理副作用时有一些区别。

  1. 执行时机:useEffect 是异步执行的,而 useLayoutEffect 是同步执行的。

    • useEffect 在组件渲染完成后延迟执行,不会阻塞页面渲染。它会在浏览器绘制完成后执行副作用操作。
    • useLayoutEffect 则会在 DOM 更新之后同步执行,但在浏览器绘制之前执行。这可能会导致页面稍微延迟渲染。
  2. 使用场景:由于 useLayoutEffect 是同步执行的,因此适合处理需要在 DOM 更新之后立即进行的操作。

    • 如果副作用操作不需要立即执行,或者涉及到网络请求、数据获取等异步操作,应该优先选择使用 useEffect
    • 当需要计算 DOM 元素的位置、尺寸、布局等信息,或者需要进行 DOM 操作(例如添加/移除节点)并且希望立即反映在页面中时,可以考虑使用 useLayoutEffect
  3. 性能影响:由于 useLayoutEffect 是同步执行的,可能会对页面渲染性能产生较大的影响。

    • 如果 useLayoutEffect 的副作用操作非常耗时,可能会导致页面渲染的卡顿或阻塞。
    • 另一方面,useEffect 是异步执行的,不会直接影响页面的渲染性能。

4.useLayoutEffect设置的callback要优先于useEffect去执行

  useEffect(() => {
    console.log('useEffect');
  }, [])
  useLayoutEffect(()=>{
    console.log('useLayoutEffect');
  }, [])

输出:

useLayoutEffect
useEffect

在大多数情况下,应该优先使用 useEffect。只有在需要在更新之前立即同步执行副作用操作的特殊情况下,才需要使用 useLayoutEffect

请根据具体的需求选择合适的 Hook,并确保在处理副作用时考虑到性能和渲染方面的影响。

参考文章

React教程 - Hooks

useRef

useRef 是 React 提供的一个 Hook,用于在函数组件中获取持久化的引用。它返回一个可变的 ref 对象,其 .current 属性可以存储和读取任意可变值。

基本使用

访问 DOM 元素

通过将 ref 对象传递给组件或 DOM 元素的ref属性,可以获取或操作对应的 DOM 元素。

import React, { useRef } from 'react';

function MyComponent() {
  const myRef = useRef(null);

  const handleClick = () => {
    // 通过 myRef.current 可以访问到 ref 绑定的DOM元素
    console.log(myRef.current);
  };

  return <div ref={myRef} onClick={handleClick}>Click me</div>;
}

利用useRef 保存普通变量

当需要在函数组件中保存普通变量,并且不希望因为变量的改变触发组件重新渲染时,可以使用 useRef。

import React, { useState, useRef, useEffect ,useMemo, useCallback} from 'react';

const App = () => {
  let count = 0;
  let countRef = useRef(0)
  let [label,setLabel] = useState(0)
  const handleClick = () => {
    count++;
    countRef.current = countRef.current + 1
  };

  useEffect(()=>{
    if(label > 0){
      handleClick()
      console.log(count,countRef.current,label);
    }
  },[label])


  return (
    <div style={{padding:'20px'}}>
      <div>count:{count}</div>
      <div>countRef:{countRef.current}</div>
      <div>label:{label}</div>
      <button onClick={() => setLabel(label => label + 1)}>Increase Count</button>
    </div>
  );
}

export default App;

多次点击按钮,控制台输出:

1 1 1
1 2 2
1 3 3
1 4 4
1 5 5
1 6 6
1 7 7

页面展示:

React之常用Hooks React之常用Hooks

通过三种定义数据的方式我们可以看出:

  • 不是用react hooks直接定义数据,会出现页面重新渲染时,数据不会被渲染,而且每次重新渲染时,数据会被还原;
  • 使用useRef定义数据,变量的改变不会触发组件重新渲染,但数据每次都能获得最新的值;
  • 使用useState定义数据,变量的改变会触发组件重新渲染,但不一定数据每次都能获得最新的值;

这里如果我们把上面的代码useEffect中的条件语句去掉,我们看下控制台的输出结果:

1 1 0 (初始渲染)
2 2 0 (更新渲染)
1 3 1 (第一次点击)

我们发现页面进入时,会出现两次输出的结果,React 组件在初始渲染时会执行一次,然后在状态或属性发生变化时进行更新渲染。这是 React 的正常行为,React 通过对比前后的虚拟 DOM 树来确定是否需要进行重新渲染。

这里就要介绍一下React Hooks 中存在 Capture Value 的特性;

Capture Value 的特性

在 JavaScript 中,闭包是一种特性,它允许函数访问其词法作用域(即定义该函数时所在的作用域)之外的变量。这导致了 "Capture Value"(值捕获)的行为。

当一个函数内部引用了外部变量,并且该函数形成了闭包时,该函数会捕获(或绑定)这些外部变量的值。这意味着即使在函数执行时,这些外部变量的值发生了改变,闭包仍然能够访问到当初被捕获时的值。

对于React组件中的输入元素,"Capture Value"特性是指将输入元素的值捕获并保存到组件状态中,以便在需要时进行处理或存储。

看一段代码:

  import React, { useState } from "react";

  const MyComponent = () => {
    const [message, setMessage] = useState("");

    const showMessage = () => {
      alert("You said: " + message);
    };

    const handleSendClick = () => {
      setTimeout(showMessage, 3000);
    };

    const handleMessageChange = (e) => {
      setMessage(e.target.value);
    };

    return (
      <>
        <input value={message} onChange={handleMessageChange} />
        <button onClick={handleSendClick}>Send</button>
      </>
    );
  };

  export default MyComponent;

对Capture Value 特性的分析:在点击 Send 按钮后,再次修改输入框的值,3 秒后的输出依然是点击前输入框的值。这就是所谓的 capture value 的特性。而在类组件中不会出现这种情况,在 3 秒后输出的就是修改后的值,因为这时候 message 是挂载在 this 变量上,它保留的是一个引用值,对 this 属性的访问都会获取到最新的值。此时我们可以用 useRef 来创建一个引用,就可以有效规避 React Hooks 中 Capture Value 特性。

  import React, { useRef } from "react";

  const MyComponent = () => {
    const latestMessage = useRef("");

    const showMessage = () => {
      alert("You said: " + latestMessage.current);
    };

    const handleSendClick = () => {
      setTimeout(showMessage, 3000);
    };

    const handleMessageChange = (e) => {
      latestMessage.current = e.target.value; // 这里就是赋值取值对象变为useRef
    };

    return (
      <>
        <input  onChange={handleMessageChange} />
        <button onClick={handleSendClick}>Send</button>
      </>
    );
  };

  export default MyComponent;

只要将赋值与取值的对象变成 useRef,而不是 useState,就可以躲过 capture value 特性,在 3 秒后得到最新的值。

注意事项

  • useRef钩子通常用于获取DOM元素的引用。通过将ref属性赋值给DOM元素,你可以使用ref.current来访问DOM节点,并进行一些DOM操作。
  • useRef返回的是一个可变的引用对象,而不是一个普通的值。因此,如果你想获取ref对象的当前值,需要使用ref.current
  • useRef钩子在组件渲染之间保持了引用的稳定性。这意味着在组件重新渲染时,useRef返回的引用对象是相同的,不会发生变化。这对于在多次渲染之间共享数据非常有用。
  • useRef钩子在每次组件重新渲染时都会返回相同的引用。这意味着如果你通过useRef在函数组件中创建了一个对象,每次重新渲染时都可以保持引用不变。这与useState钩子不同,useState在每次渲染时都会创建一个新的状态变量。
  • useRef钩子不会自动触发组件的重新渲染。因此,当ref.current的值发生变化时,组件不会自动更新。如果你希望在ref.current发生变化时触发重新渲染,你需要使用其他的钩子(例如useStateuseEffect)来监视ref.current的变化并进行相应的处理。
  • 当使用useRef来保存值时,一定要注意初始值设置里不能用函数来进行赋值,因为这样会造车每次render此函数都会执行。

参考文章

React-Hooks 初识 (三): useRef 的使用:获取DOM元素和保存变量

useContext

useContext 是 React 中的一个自定义 Hook,用于在函数组件中获取上下文(Context)的值。通过 useContext,提供读取和订阅功能。你可以避免在组件树中一层一层地传递上下文。

接收参数

  • context:是 createContext 创建出来的对象,他不保持信息,他是信息的载体。声明了可以从组件获取或者给组件提供信息。在 provider 中可以传递具体的值。

返回值

  • contextValue:返回传递的只读context载体的值。是调用堆栈中组件上方最近的 SomeContext.Provider 出来的值。如果没有这样的Provider,则返回的值将是传递给该context的 createContext 的 defaultValue。其返回的值始终是最新的。如果上下文发生变化,React 会自动重新渲染读取context的组件。

基本使用

使用 useContext 的步骤如下:

  1. 首先,你需要创建一个上下文(Context)。可以使用 React.createContext 方法来创建一个上下文对象。
const MyContext = React.createContext();
  1. 在父组件中,使用 MyContext.Provider 组件来提供上下文的值。将需要共享的值作为 value 属性传递给 Provider。
function ParentComponent() {
  const sharedValue = 'Hello, World!';

  return (
    <MyContext.Provider value={sharedValue}>
      <ChildComponent />
    </MyContext.Provider>
  );
}
  1. 在子组件中,使用 useContext 来获取上下文的值。
import React, { useContext } from 'react';

function ChildComponent() {
  const sharedValue = useContext(MyContext);

  return <div>{sharedValue}</div>;
}

在这个示例中,ChildComponent 中使用 useContext 来获取 MyContext 上下文的值。通过 useContext(MyContext),你可以直接获得提供者(MyContext.Provider)中传递的共享值。

更多示例可以参考:新版React官方文档解读(四)- Hooks 之 useContext、useRef 和 useImperativeHandle

注意事项

  • useContext 只能用于函数组件或自定义 Hook 中,不能用于类组件。
  • 需要确保上下文对象(通过 React.createContext 创建的对象)已经创建并且提供者(Context.Provider)已正确放置在父组件中。如果没有正确设置提供者,useContext 将返回上下文对象的默认值。
  • 当上下文的值发生变化时,使用 useContext 的组件将会重新渲染。这是 React 的正常行为。如果需要避免不必要的重新渲染,可以使用 React.memo 或者优化组件的渲染逻辑。
  • 如果组件需要访问多个上下文的值,可以使用多个 useContext 调用。每个 useContext 调用都应该对应一个上下文对象。
const ThemeContext = createContext(null);
const CurrentUserContext = createContext(null);

// ...

<ThemeContext.Provider value={theme}>
  <CurrentUserContext.Provider
    value={{
      currentUser,
      setCurrentUser
    }}
  >
    <WelcomePanel />
// ...

const {currentUser} = useContext(CurrentUserContex);
const {theme} = useContext(ThemeContext);
  • 子组件返回值 是只读的,所以子组件不需要任何修改。

参考文章

新版React官方文档解读(四)- Hooks 之 useContext、useRef 和 useImperativeHandle

useReducer

useReducer是React中的一个常用Hook,用于管理复杂的状态逻辑。它通过接受一个reducer函数和一个初始状态值,返回一个状态值和一个派发action的函数。

使用useReducer的基本语法如下:

const [state, dispatch] = useReducer(reducer, initialState);
  • state:表示当前的状态值。
  • dispatch:一个函数,用于派发action来更新状态。
  • reducer:一个函数,用于根据接收的action来更新状态的逻辑。
  • initialState:初始状态值。

reducer函数接收两个参数:当前的状态和派发的action。它根据action的类型来执行相应的操作,并返回新的状态值。reducer函数的基本语法如下:

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

在组件中使用useReducer时,你可以通过调用dispatch函数来派发action,从而更新状态。action是一个包含type属性的对象,用于描述要执行的操作。根据不同的type,reducer函数将会执行相应的逻辑来更新状态。

基本使用

以下是一个使用useReducer的示例代码:

import React, { useReducer } from 'react';

const initialState = 0;

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

const Counter = () => {
  const [count, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
    </div>
  );
};

export default Counter;

在上述示例中,我们定义了一个计数器组件Counter。通过useReducer,我们创建了一个名为count的状态变量,并将初始状态值为0和reducer函数传递给useReducer。然后,在组件中通过调用dispatch函数来派发INCREMENTDECREMENT的action,从而更新计数器的值。

useReducer 三个参数

useReducer中,除了接受 reducerinitialState 作为前两个参数外,还可以传递第三个参数来定义一个初始化函数。

使用useReducer的完整语法如下:

const [state, dispatch] = useReducer(reducer, initialState, init?);
  • state:表示当前的状态值。
  • dispatch:一个函数,用于派发操作(action)来更新状态。
  • reducer:一个处理状态更新逻辑的函数。
  • initialState:初始状态值。
  • init(可选):一个初始化函数,用于在组件首次渲染时根据某些逻辑计算初始状态值。如果init没有指定,则初始状态为initialState,否则,初始状态将调用init的结果。其中initialState作为init参数,如果init没有参数,则第二个参数可以直接写为null即可。

如果提供了 init 函数,则它将以 initialState 作为参数调用,并返回计算得到的初始状态。

这个 init 函数通常用于在计算初始状态时执行复杂的逻辑,比如从本地存储加载数据、从服务器请求数据等。

下面是一个示例,展示了如何使用 init 函数在组件首次渲染时计算初始状态:

import React, { useReducer } from 'react';

const initialState = 0;

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

function init(initialCount) {
  return initialCount * 2; // 计算初始状态值
}

const Counter = () => {
  const [count, dispatch] = useReducer(reducer, initialState, init);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
    </div>
  );
};

export default Counter;

在上述示例中,我们定义了一个初始化函数 init,它以 initialState 作为参数,并返回计算得到的初始状态。

通过使用第三个参数的 init 函数,我们可以在组件首次渲染时基于一些逻辑来计算初始状态,从而实现更灵活的状态初始化。

可能会有人想,可不可以直接改为:

const [count, dispatch] = useReducer(reducer, init(initialState));

这样我们看下下面一段代码:

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

const initialState = 0;

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

function init(initialCount) {
  console.log('reducer',initialCount);
  return initialCount * 2; // 计算初始状态值
}

const Counter = () => {
  //const [count, dispatch] = useReducer(reducer, initialState, init);
  const [count, dispatch] = useReducer(reducer, init(initialState));
  const [number,setNumber] = useState(0)
  useEffect(()=>{
    console.log(number)
  },[number])

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
      <button onClick={() => setNumber(number + 1)}>Number +1</button>
    </div>
  );
};

export default Counter;

我们多次点击Number +1按钮,出发页面重新渲染,我们在控制台中就会发现,使用三个参数的情况,最后的函数只在页面初始化渲染时会被调用,而使用useReducer(reducer, init(initialState));这种情况的,页面只要重新渲染,函数都会被调用,如果是创建大型数组或者执行大量计算,这可能会造成浪费。

dispatch传递额外参数

在使用useReducer时,可以通过传递额外的参数来扩展dispatch函数的功能。这可以通过使用闭包或箭头函数来实现。

下面是一个示例,演示了如何在dispatch函数中传递额外的参数:

import React, { useReducer } from 'react';

const initialState = {
  count: 0,
};

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + action.payload };
    case 'DECREMENT':
      return { count: state.count - action.payload };
    default:
      return state;
  }
}

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const handleIncrement = () => {
    dispatch({ type: 'INCREMENT', payload: 1 });
  };

  const handleDecrement = () => {
    dispatch({ type: 'DECREMENT', payload: 2 });
  };

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleDecrement}>Decrement</button>
    </div>
  );
};

export default Counter;

在上述示例中,我们在dispatch函数中传递了一个名为payload的额外参数。当调用dispatch函数时,我们将payload作为action对象的属性传递给reducer函数,以便在reducer函数中处理相应的逻辑。

通过传递额外参数给dispatch函数,可以在reducer中执行与该参数相关的操作,从而实现更灵活和定制化的状态更新逻辑。

需要注意的是,由于dispatch函数在组件渲染期间是稳定的,因此在闭包或箭头函数中引用的额外参数也会保持稳定。这意味着在每次渲染时,通过闭包或箭头函数传递的额外参数值是最新的。

将dispatch函数传递给子组件

若您想将dispatch函数传递给子组件,可以通过props将其传递下去。下面是一个示例,展示了如何在React中使用useReducer和将dispatch函数传递给子组件:

首先,在父组件中定义useReducer

import React, { useReducer } from 'react';
import ChildComponent from './ChildComponent';

function reducer(state, action) {
  // 根据action更新state的逻辑
  // ...
}

function ParentComponent() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      {/* 其他父组件内容 */}
      <ChildComponent dispatch={dispatch} />
    </div>
  );
}

export default ParentComponent;

然后,在子组件中接收并使用dispatch函数:

import React from 'react';

function ChildComponent({ dispatch }) {
  // 使用dispatch函数进行操作
  const handleButtonClick = () => {
    dispatch({ type: 'ACTION_NAME', payload: 'data' });
  };

  return (
    <div>
      <button onClick={handleButtonClick}>触发动作</button>
    </div>
  );
}

export default ChildComponent;

通过将dispatch函数作为props传递给ChildComponent,子组件可以直接调用dispatch函数来触发相应的操作。这样,父组件和子组件之间可以共享状态并进行协调操作。

注意事项

  • 状态更新不会自动合并:与useState不同,useReducer中的状态更新不会自动合并。每次调用dispatch函数时,都会完全替换先前的状态。因此,在更新状态时需要确保包含所有需要保留的属性。

我们将上段代码进行些许修改:给initialState添加额外的属性,

import React, { useReducer } from 'react';

const initialState = {
  count: 0,
  name:"Tony"
};

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + action.payload };
    case 'DECREMENT':
      return { count: state.count - action.payload };
    default:
      return state;
  }
}

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const handleIncrement = () => {
    dispatch({ type: 'INCREMENT', payload: 1 });
  };

  const handleDecrement = () => {
    dispatch({ type: 'DECREMENT', payload: 2 });
  };
  console.log(state);
    
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleDecrement}>Decrement</button>
    </div>
  );
};

export default Counter;

这样我们每次点击按钮的时候,就会发现state的属性值只剩count了,要解决这种问题可以这么做,修改reduer函数,返回对象默认添加原对象:

return {
    ...state,
    count: state.count - action.payload,
  };
  • 避免在reducer中修改原始状态reducer函数应该是一个纯函数,不能直接修改原始的状态。它应该返回一个新的状态对象,而不是对原始状态对象的修改。这样可以确保状态的不可变性,避免产生潜在的副作用。
  • 遵循常规的reducer约定:在编写reducer函数时,应遵循常规的约定。使用switch语句来处理不同的操作类型,并返回新的状态对象。确保在默认情况下返回原始状态对象,并且不会对无效的操作类型进行任何更改。
  • 将dispatch函数传递给子组件时的性能考虑:当将dispatch函数传递给子组件时,需要小心,因为每次组件重新渲染时,传递给子组件的dispatch函数实际上是一个新的函数。这可能导致子组件重新渲染,即使它们的props没有发生变化。为了避免这种情况,可以使用useCallback Hook来确保dispatch函数的稳定性。

和useState使用场景差异

useReduceruseState是React中用于管理状态的两个常用Hook,它们有一些区别和不同的使用场景。

  1. 状态逻辑复杂度useState适用于简单的状态管理,适合处理单个值或少量相关值的状态。当状态逻辑较为复杂,包含多个相关变量时,可以考虑使用useReducer来更好地组织和管理状态。
  2. 状态更新逻辑useState提供了一个简单的函数用于更新状态,该函数可以直接更新状态的值。而useReducer使用reducer函数来处理状态更新逻辑,更适合于更复杂的状态更新操作。使用useReducer可以将状态更新逻辑封装到reducer函数中,使得代码更清晰、可维护。
  3. 共享状态useReducer可以更好地支持多个组件之间共享状态。通过将dispatch函数传递给子组件,可以将状态更新逻辑和状态值共享给其他组件。而useState的状态是局部的,只能在当前组件内部使用。

综上所述,一般来说,当状态比较简单且逻辑不复杂时,可以优先选择使用useState。而当状态逻辑较为复杂,需要更好地组织和管理状态时,可以选择使用useReducer。另外,如果需要在多个组件之间共享状态,也可以使用useReducer来实现。

和useContext结合使用

useReduceruseContext可以结合使用,以便在React应用中更好地管理和共享状态。

useReducer可以用于管理和更新局部状态,而useContext可以用于创建和管理全局状态。

如果共享数据层级不深,可以只使用useReducer来共享数据,如果层级很深,就需要useReduceruseContext结合使用;

下面是一个使用useReduceruseContext结合的示例:

import React, { useReducer, useContext } from 'react';

// 创建一个全局的上下文对象
const MyContext = React.createContext();

// 初始状态
const initialState = {
  count: 0,
};

// reducer函数
function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

// 组件A
function ComponentA() {
  const { state, dispatch } = useContext(MyContext);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
    </div>
  );
}

// 组件B
function ComponentB() {
  const { state } = useContext(MyContext);

  return <p>Count in Component B: {state.count}</p>;
}

// 根组件
function App() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <MyContext.Provider value={{ state, dispatch }}>
      <div>
        <ComponentA />
        <ComponentB />
      </div>
    </MyContext.Provider>
  );
}

export default App;

在上述示例中,我们首先创建了一个名为MyContext的全局上下文对象。然后,我们定义了一个reducer函数来处理状态的更新逻辑。

在根组件App中,我们使用useReducer来创建一个局部状态和dispatch函数。然后,我们通过将状态和dispatch函数作为值传递给MyContext.Providervalue属性,将它们共享给子组件。

在组件A中,我们使用useContext来获取共享的状态和dispatch函数,并在按钮上添加点击事件来派发操作以更新状态。

在组件B中,我们也可以使用useContext来获取共享的状态,以便在组件中使用它。

通过结合使用useReduceruseContext,我们可以更好地管理和共享状态,使组件之间可以方便地访问和更新共享的状态。

参考文章

深入浅出-useReducer

useImperativeHandle

useImperativeHandle是React的一个自定义Hook,用于向父组件暴露自定义的实例值或方法。它可以与forwardRef一起使用,使函数组件能够像类组件一样通过ref访问子组件的实例或方法。

基本使用

使用useImperativeHandle可以在子组件中定义一个自定义的实例值或方法,并将其暴露给父组件。这样,父组件就可以通过ref来访问和调用子组件的实例或方法。

下面是一个使用useImperativeHandle的示例:

子组件

import React, { useState, useRef, useImperativeHandle, forwardRef } from 'react';

// 子组件
const ChildComponent = forwardRef((props, ref) => {
  const inputRef = useRef();
  const [value,setValue] = useState('')

  // 定义一个自定义的方法
  const focusInput = () => {
    setValue('子组件')
    inputRef.current.focus();
  };

  // 使用useImperativeHandle暴露自定义方法给父组件
  useImperativeHandle(ref, () => ({
    focusInput
  }));

  return <input ref={inputRef} value={value}/>;
});

export default ChildComponent

首先在子组件中创建了一个inputRef引用,用于引用子组件中的input元素。然后,我们定义了一个名为focusInput的自定义方法,用于将焦点设置到input元素上,并使得输入框的值为‘子组件’。

通过使用useImperativeHandle,我们将focusInput方法暴露给父组件,以便父组件可以通过ref来调用子组件中的这个方法。

父组件

import React, { useRef } from "react";
import ChildComponent from './children';

// 父组件
const ParentComponent = () => {
  const childRef = useRef();

  const handleButtonClick = () => {
    childRef.current.focusInput();
  };

  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={handleButtonClick}>Focus Input</button>
    </div>
  );
};

export default ParentComponent;

在父组件中,我们使用useRef来创建一个childRef引用,并将它传递给子组件的ref属性。

然后,我们在父组件中定义了一个handleButtonClick方法,用于在点击按钮时调用子组件中的focusInput方法,以将焦点设置到input元素上,并修改输入框的值。

注意事项

  • useImperativeHandle应该与forwardRef一起使用。forwardRef使得函数组件能够接收一个ref参数,并可以将其传递给内部的子组件。
  • useImperativeHandle应该在子组件中定义,并且只能在子组件中使用。它用于自定义暴露给父组件的实例值或方法。
  • useImperativeHandle在子组件内部的自定义Hook中调用。它需要传递两个参数:ref和一个回调函数。回调函数应该返回一个对象,其中包含要暴露给父组件的实例值或方法。
  • 如果不使用useImperativeHandle,父组件通过ref访问子组件时,将获得子组件的整个实例对象。使用useImperativeHandle可以选择性地暴露一部分子组件实例给父组件,以提供更好的封装和控制。
  • 当使用useImperativeHandle时,父组件必须通过ref来访问子组件的实例或方法。例如:childRef.current.someMethod()
  • 注意,使用useImperativeHandle暴露给父组件的实例或方法应该是可变的。这意味着它们可以在组件重新渲染时改变,而不会触发父组件的重新渲染。
  • 尽量避免使用useImperativeHandle,除非确实需要在函数组件中暴露实例或方法给父组件。通常情况下,应该优先考虑使用React的推荐方式,如使用props来传递数据和回调函数。

useMemo

useMemo是React的一个自定义Hook,用于在函数组件中进行性能优化。它的作用是缓存计算结果,并在依赖项发生变化时重新计算。

使用useMemo可以避免在每次函数组件渲染时重复执行昂贵的计算操作,从而提高性能。

用途和vue中的computed相似

基本使用

useMemo接收两个参数:一个回调函数和一个依赖项数组。回调函数用于执行计算操作,而依赖项数组用于指定在依赖项发生变化时需要重新计算的情况。

下面是一个使用useMemo的示例:

import React, { useMemo } from 'react';

const MyComponent = ({ a, b }) => {
  // 使用useMemo缓存计算结果
  const result = useMemo(() => {
    // 执行一些昂贵的计算操作
    console.log('Expensive calculation');
    return a + b;
  }, [a, b]);

  return <div>Result: {result}</div>;
};

export default MyComponent;

在上述示例中,我们定义了一个名为MyComponent的函数组件。在组件内部,我们使用useMemo来缓存计算结果。

ab发生变化时,useMemo会重新计算并返回新的结果。但是,在依赖项没有变化时,useMemo会直接返回上次计算的结果,避免了重复执行计算操作。

通过使用useMemo,我们可以有效地优化函数组件,并在需要时缓存计算结果,避免不必要的计算开销。这对于处理大量数据或复杂的计算操作特别有用。

注意事项

  1. 避免过度使用useMemo应该在需要时才使用,而不是在每个计算操作都使用。过度使用useMemo可能会导致代码变得复杂,降低可读性。
  2. 选择合适的依赖项useMemo的第二个参数是一个依赖项数组,用于指定在其中任何一个依赖项发生变化时需要重新计算。确保只将必要的依赖项添加到数组中,避免不必要的计算。如果没有依赖项,可以传递一个空数组。
  3. 不要过度优化:只有在有性能问题或计算开销较大时,才应该考虑使用useMemo进行优化。在大多数情况下,React的默认渲染机制足够高效,不需要额外的优化。
  4. 注意副作用useMemo回调函数内部不应该产生副作用,例如修改组件状态、发起网络请求等。如果需要执行副作用操作,请使用useEffect
  5. 不要与useState一起使用useMemouseState有着不同的目的。useMemo用于缓存计算结果,而useState用于管理组件的状态。不要将计算结果存储在useState中,而应该使用useMemo
  6. 小心引用类型的依赖项:当依赖项是引用类型(如对象或数组)时,要小心处理。因为在每次重新渲染时,依赖项的引用都会发生变化,这可能会导致useMemo在每次渲染时都重新计算。可以使用useCallback来处理这种情况。

useCallback

useCallback是React的一个自定义Hook,用于在函数组件中缓存回调函数,以避免在每次渲染时创建新的回调函数。它可以用于性能优化,特别是在将回调函数作为props传递给子组件时。

基本使用

使用useCallback可以确保在依赖项不变的情况下,返回相同的回调函数实例。这样可以避免不必要的子组件重新渲染,因为子组件会将回调函数作为依赖项进行比较。

useCallback接收两个参数:一个回调函数和一个依赖项数组。回调函数用于定义需要缓存的回调逻辑,依赖项数组用于指定在其中任何一个依赖项发生变化时需要重新创建回调函数。

下面是一个使用useCallback的示例:

import React, { useCallback } from 'react';

const MyComponent = ({ onClick }) => {
  // 使用useCallback缓存回调函数
  const handleClick = useCallback(() => {
    // 执行一些操作
    console.log('Button clicked');
    onClick();
  }, [onClick]);

  return <button onClick={handleClick}>Click me</button>;
};

export default MyComponent;

在这个示例中,我们的回调函数是handleClick,它会在按钮被点击时执行。我们使用useCallback来缓存这个回调函数。

onClick依赖项发生变化时,useCallback会返回新的回调函数实例。但是,在onClick没有变化时,useCallback会直接返回上次缓存的回调函数实例,避免了不必要的重新创建。

通过使用useCallback,我们可以确保在函数组件中缓存回调函数,并在需要时提供相同的回调函数实例,以避免不必要的子组件重新渲染。

需要注意的是,useCallback并不总是必需的,只有当回调函数作为props传递给子组件,并且子组件需要使用React.memo进行性能优化时,才需要考虑使用它。在大多数情况下,React已经在内部做了优化,不需要手动使用useCallback进行性能优化。

注意事项

  1. 性能优化: useCallback可以用于缓存回调函数,以避免在每次渲染时创建新的回调函数。这对于性能优化非常有用,特别是在将回调函数作为prop传递给子组件时。但请注意,不是每个回调函数都需要使用useCallback进行优化,只有在确实存在性能问题时才进行优化。
  2. 依赖项数组: useCallback接收第二个参数,即依赖项数组。这个数组用于指定在依赖项发生变化时,是否需要重新创建回调函数。如果依赖项数组为空,则回调函数只会在组件首次渲染时创建一次。如果依赖项数组不为空,则会在依赖项发生变化时重新创建回调函数。
  3. 避免不必要的使用: useCallback创建的缓存回调函数会占用内存。因此,应避免在不必要的情况下过度使用useCallback。只有当回调函数实际上会被传递给其他组件或作为effect的依赖项时,才值得使用useCallback进行优化。
  4. 注意闭包问题: 当在useCallback内部使用外部的变量时,请确保将这些变量包含在依赖项数组中,以便在依赖项发生变化时,重新创建具有正确引用的回调函数。否则,可能会遇到闭包问题,导致回调函数引用过时的变量。