React一学就会(6): 细说Hooks及其应用

这几天小孩生病发烧,天天跑医院,搞得精力实在跟不上。写作的时间大大减少,写出的质量也不高。关于昨天写的函数组件的内容有点乱,很多都是直接参考了官方文档,今天再对相关的知识作个补充。请大家谅解。

Context

由于状态是封闭的,所以组件与组件之间不能共享状态,我们只能把数据通过Props的方式传递给子组件,子组件再通过props传递给它的子组件,这样一级一级的传递下去。如果层级很多,数据很多,就很繁琐。Context 时候就能大展伸手了。

  • React 中的 Context 提供了一种在组件之间共享数据的方式,而不必通过组件树的逐层传递 props。它适用于在应用程序中全局管理状态、配置信息、主题等情况下。
Context 的基本概念

Context 主要由三个部分组成:

  • Context 对象: 创建一个 Context 对象,该对象包含共享的数据。通常,你会在一个单独的文件中创建 Context 对象。
  • Provider: Provider 是 Context 对象的提供者,它用于提供数据。Provider 通过 value 属性传递数据给其后代组件。
  • Consumer: Consumer 是 Context 对象的消费者,用于订阅 Context 的变化。它允许在组件中访问 Context 中的值。
使用 Context

以下是如何在 React 中使用 Context 的基本步骤:

// context.js
import React from 'react';
const defaultValue = { /* 默认的共享数据 */ }
const MyContext = React.createContext(defaultValue);
export default MyContext;

上面我们使用 createContext 创建了一个Context, 它的初始化值为 defaultValue。并将这个Context赋于变量 MyContext并导出,此时 MyContext 就唯一代表了这个Context的类型。我们把它可以当作一个数据组件。对,此时的它也是一种组件。
在需要共享数据的组件层级中使用 Provider

// App.js
import React from 'react';
import MyContext from './context';
import ChildComponent from './ChildComponent';

function App() {
  const sharedData = { /* 共享的数据 */ };

  return (
    <MyContext.Provider value={sharedData}>
      <ChildComponent />
    </MyContext.Provider>
  );
}

export default App;

我们在App.jsx中导入了 MyContext 组件,所有它的子组件中都可以通过一定的方式获取到这个组件的值。 我们也可以通过对其value重新赋值,就像上面的示例所操作的一样。一定要用 Provider 才能向子组件提供数据共享。所有要使用这个数据的子组件我们称之为消费者 Consumer

我们来看看如何在消费者组件中使用 Consumer

// ChildComponent.js
import React from 'react';
import MyContext from './context';

function ChildComponent() {
  return (
    <MyContext.Consumer>
      {value => (
        <div>
          {/* 在这里可以使用从 Context 中获取的值 */}
          <p>Shared data: {value}</p>
        </div>
      )}
    </MyContext.Consumer>
  );
}

export default ChildComponent;
注意事项
  • Provider 可以嵌套:可以在组件树的不同层级中使用多个 Provider,每个 Provider 可以传递不同的值给其后代。
  • 默认值:在没有匹配到 Provider 时,Consumer 组件可以使用默认值。
  • 动态 Context:Provider 的 value 可以是动态的,可以是状态、上下文或任何从 props 中获取的值。
  • 性能考虑:当 Context 中的值发生变化时,所有使用该 Context 的组件都会重新渲染。因此,在设计时需要考虑性能影响。
  • 推荐使用场景:Context 适用于在组件树中传递数据,但是并不适用于所有数据共享的场景。在某些情况下,使用 Context 可能会使代码变得更加复杂,因此需要谨慎使用。
    总之,React Context 提供了一种方便的方式来共享数据,但是在实际使用中需要根据具体情况权衡利弊。

你可能对上面这个嵌套的用法有点不知如何下手,看下面的示例你就明白了:

function NestContextDemo{
    ...
    
return (
        <SideMenuState.Provider value={ menuState }>
            <SideMenuBadge.Provider value={badge}>
                <DispatchMenuState.Provider value={updateMenuState}>
                    <DispatchMenuBadge.Provider value={updateBadgeHandler}>
                        <SideMenuData.Provider value={menuData}>
                            {
                                /* 你的消费组件 */
                                children
                            }
                        </SideMenuData.Provider>
                    </DispatchMenuBadge.Provider>
                </DispatchMenuState.Provider>
            </SideMenuBadge.Provider>
        </SideMenuState.Provider>
    )
}

你看,上面嵌套了多少层。是不是很魔幻。
当有多个Context层次嵌套的时候,你会发现往往 Consumer 也有很多嵌套。这就有点无法接受了。本来很清爽的组件,外面弄了多个嵌套层,我是无论如何都不能接受的。 还好,React 已经想到了。

useContext

React 中,除了使用 Consumer 外,还有另一种更简单的方式来使用 Context,那就是使用 useContext 钩子。useContext 钩子可以让你更方便地从 Context 中读取值。
useContext 钩子接收一个 context 对象(由 React.createContext 创建)并返回该 context 的当前值。当组件上下文发生变化时,useContext 将使组件重新渲染。

// ChildComponent.js
import React, { useContext } from 'react';
import MyContext from './context';

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

  return (
    <div>
      {/* 在这里可以直接使用从 Context 中获取的值 */}
      <p>Shared data: {value}</p>
    </div>
  );
}

export default ChildComponent;

通过 useContext,我们可以直接从上下文中获取值,并在组件中使用它,而无需嵌套多余的组件。这使得代码更加简洁和易读。

使用场景比较
Consumer vs useContext:

  • 使用 Consumer:适用于类组件和函数组件,适用于渲染逻辑较为复杂的情况。
  • 使用 useContext:适用于函数组件,适用于简单的场景和快速访问上下文值的情况。

性能方面:

在性能方面,两者并没有显著差异,但是 useContext 的语法更为简洁,可读性更高。
总之,useContext 提供了一种更简洁的方式来使用 Context,特别是在函数组件中。但是在一些情况下,如需要进行条件渲染或者动态修改上下文值时,Consumer 仍然是一种更灵活的选择。

use

除了上面的 useContext, 在React18.2的版本中,提供了另一种使用Context的方法 use
与其他 React Hook 不同的是,可以在循环和条件语句(如 if)中调用 use。但需要注意的是,调用 use 的函数仍然必须是一个组件或 Hook。如下所示:

function HorizontalRule({ show }) {
  if (show) {
    const theme = use(ThemeContext);
    return <hr className={theme} />;
  }
  return false;
}

if 语句内部调用了 use,允许有条件地从 context 中读取值。

关于useEffect的补充

指定依赖项

依赖项是指当 useEffect 中使用了组件内 useEffect 之外的变量。 一般我们都要指定这个依赖,这能极大地优化性能。依赖项指定后,当只有依赖项的数据发生变化,组合才会更新渲染。它会对依赖项的上次值进行比较,如果不相同就刷新渲染。

function ChatRoom({ roomId }) { // 这是一个响应式值
  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // 这也是一个响应式值

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // 这个 Effect 读取这些响应式值
    connection.connect();
    return () => connection.disconnect();
  }, [serverUrl, roomId]); // ✅ 因此你必须指定它们作为 Effect 的依赖项
  // ...
}

useEffect 中最后一个参数就是依赖项列表,可以看出上列中 useEffect内部使用了 变量 serverUrlroomId 即 依赖项。注意,依赖项必须是组件内的变量,包括 props 和 state 。组件外的变量不用指定为依赖项。如下面把 serverUrl 的定义移到组件外进行定义, 这就可以把它从依赖项中移除。

const serverUrl = 'https://localhost:1234'; // 不再是一个响应式值

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ 所有声明的依赖项
  // ...
}

如果 Effect 的代码不使用任何响应式值,则其依赖项列表应为空([]):

const serverUrl = 'https://localhost:1234'; // 不再是响应式值
const roomId = 'music'; // 不再是响应式值

function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // ✅ 所有声明的依赖项
  // ...
}

空依赖项 [] 和 没有传入依赖项是不同的。空依赖项表示 useEffect 只执行一次。而 null 则表示只要变量发生变化就会重新渲染。一定要慎用。

useCallback

这个 Hook 是 用于优化性能。它的作用是返回一个记忆化的回调函数,这个回调函数仅在依赖项发生变化时才会更新。通常情况下,它用于将回调函数传递给子组件,以避免不必要的函数重新创建,从而减少不必要的组件重新渲染。

useCallback 的基本语法

const memoizedCallback = useCallback(
  () => {
    // 回调函数体
  },
  [依赖项数组]
);
  • 第一个参数是回调函数本身。
  • 第二个参数是一个依赖项数组,当数组中的任何一个值发生变化时,回调函数都会被重新创建。如果没有传递依赖项数组,那么每次组件重新渲染时,都会返回一个新的回调函数。
使用场景
  • 避免不必要的函数重新创建:当你需要将回调函数传递给子组件时,使用 useCallback 可以确保只有在依赖项变化时才会重新创建函数,避免不必要的重新渲染。这句话一定要牢记。

  • 优化性能:特别是在性能敏感的场景下,避免不必要的函数创建可以提高性能。

注意事项
  • 依赖项数组的作用:确保你传递的依赖项数组包含所有回调函数中使用的外部变量,否则可能会导致闭包问题。
  • 不滥用:虽然 useCallback 可以提高性能,但是不应该滥用。只有在确定会带来性能提升的情况下才使用。
  • 适用性:useCallback 主要用于优化函数传递,如果你的函数没有被传递给子组件或作为依赖项传递给其他钩子函数,那么可能不需要使用 useCallback。

示例

import React, { useCallback, useState } from 'react';

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

  // 使用 useCallback 优化回调函数
  const handleClick = useCallback(() => {
    setCount(count + 1);
  }, [count]); // 依赖项为 count

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

在这个示例中,handleClick 回调函数使用了 count 状态,因此我们将 count 添加到依赖项数组中。这样,每次 count 发生变化时,handleClick 才会重新创建。

useMemo

useMemo 是 用于在渲染过程中执行昂贵的计算,并且仅在其中的某些值发生更改时才执行。它的作用类似于 useCallback,但是它用于返回记忆化的计算值,而不是记忆化的回调函数。

基本语法
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 第一个参数是一个函数,用于执行昂贵的计算或逻辑。
  • 第二个参数是一个依赖项数组,当数组中的任何一个值发生变化时,函数都会被重新执行。如果没有传递依赖项数组,则在每次组件重新渲染时都会重新执行函数。
使用场景
  • 昂贵的计算:当你需要执行一些计算量较大的操作,比如对数组进行过滤、排序等操作时,可以使用 useMemo 来避免在每次渲染时都重新执行这些操作。
  • 优化性能:与 useCallback 类似,useMemo 可以帮助优化性能,避免不必要的计算和重新渲染。
注意事项

依赖项数组的作用:确保你传递的依赖项数组包含所有在计算函数中使用的外部变量,否则可能会导致不一致的结果。

不滥用:只有在确定会带来性能提升的情况下才使用 useMemo。滥用 useMemo 可能会导致代码变得更加复杂,同时也会带来额外的开销。

示例
import React, { useMemo, useState } from 'react';

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

  // 使用 useMemo 优化昂贵的计算
  const expensiveValue = useMemo(() => {
    return computeExpensiveValue(count);
  }, [count]); // 依赖项为 count

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

// 假设这是一个昂贵的计算函数
function computeExpensiveValue(count) {
  console.log("Calculating expensive value...");
  // 这里可以是任何复杂的计算逻辑
  return count * 2;
}

在这个示例中,computeExpensiveValue 函数执行了一些昂贵的计算,我们使用 useMemo 将其结果缓存起来,当 count 发生变化时,才会重新执行这个计算函数。这样可以避免在每次渲染时都重新执行昂贵的计算逻辑。

useRef

用于在函数组件中创建可变的 ref 对象。它提供了一种在函数组件中访问 DOM 元素或者在组件渲染周期之间共享持久化数据的方式。

基本用法
const refContainer = useRef(initialValue);

initialValueref 对象的初始值,可以是任何 JavaScript 值。

使用场景
  • 访问 DOM 元素:useRef 最常见的用法是用来获取或操作 DOM 元素。
  • 存储任意可变值:除了 DOM 元素,useRef 也可以用来存储任意可变值,并且这个值在重新渲染时保持不变。这使得 useRef 在某些情况下可以替代 useState。
  • 保存引用:useRef 也可以用来保存对上一次渲染时创建的值的引用,从而在多次渲染之间共享数据。
与 createRef 的区别

createRef 是类组件中创建 ref 对象的方式,而 useRef 是函数组件中创建 ref 对象的方式。最主要的区别在于 createRef 每次渲染都会返回一个新的 ref 对象,而 useRef 则会在多次渲染之间保持相同的 ref 对象。

示例
import React, { useRef, useEffect } from 'react';

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

  useEffect(() => {
    // 在组件加载后聚焦输入框
    inputRef.current.focus();
  }, []);

  return (
    <div>
      <input type="text" ref={inputRef} />
    </div>
  );
}

在这个示例中,我们创建了一个 inputRef,然后将它赋值给了 元素的 ref 属性。在组件加载后,useEffect 钩子会执行,它会将焦点聚焦到输入框上,这是通过 inputRef.current.focus() 实现的。

类示例

当使用类组件时,可以使用 createRef 创建 ref。下面是一个简单的示例,演示如何在类组件中使用 createRef

import React, { Component } from 'react';

class MyComponent extends Component {
  constructor(props) {
    super(props);
    // 创建一个ref对象来存储 DOM 元素的引用
    this.textInput = React.createRef();
  }

  // 当组件加载完成后,将焦点聚焦到输入框上
  componentDidMount() {
    this.textInput.current.focus();
  }

  render() {
    return (
      <div>
        {/* 将创建的 ref 关联到 input 元素上 */}
        <input type="text" ref={this.textInput} />
        <button onClick={() => this.textInput.current.focus()}>Focus Input</button>
      </div>
    );
  }
}

export default MyComponent;

在这个示例中,我们创建了一个 textInput 的 ref 对象,并将其赋值给了 元素的 ref 属性。当组件加载完成后,componentDidMount 生命周期方法会被调用,这时我们通过 this.textInput.current.focus() 将焦点聚焦到输入框上。

总之,createRef 可以帮助我们在类组件中创建和管理 ref,使得在操作 DOM 元素或者其他组件时更加方便。

useReduce

useReducerReact 提供的一个钩子函数,用于在函数组件中管理具有复杂状态逻辑的状态。它类似于 Redux 中的 reducer,接收一个当前状态和一个 action``,并返回新的状态。通常情况下,useReducer 适用于管理多个相关的状态值,或者当下一个状态依赖于之前的状态时。 这个useReduce这里如果看不明白没有关系,因为后面我会专门讲解功能更为强大的Redux Reducer 插件,学习完那个章节后再回头来看这个就一目了然了。

基本用法
const [state, dispatch] = useReducer(reducer, initialState);
  • reducer 是一个函数,接收当前状态 (state) 和一个描述发生了什么事件的 action,然后返回新的状态。它的签名是 (state, action) => newState。
  • initialState 是初始状态的值。
使用场景
  • 管理复杂状态逻辑:当状态逻辑变得复杂,或者有多个相关的状态需要管理时,可以考虑使用 useReducer。

  • 组件间共享状态逻辑:useReducer 可以帮助将状态逻辑抽象成可重用的函数,并且可以在多个组件中共享。

  • 状态依赖于之前的状态:当下一个状态的计算依赖于之前的状态时,使用 useReducer 可以更清晰地表达状态之间的关系。

示例

让我们通过一个简单的计数器示例来演示 useReducer 的用法:

import React, { useReducer } from 'react';

// reducer函数,根据action的type来更新state
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() {
  // 初始化状态和dispatch函数
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      {/* 当点击按钮时,dispatch一个action来触发状态的更新 */}
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
    </div>
  );
}

export default Counter;

在这个示例中,我们首先定义了一个 reducer 函数来处理状态的更新逻辑。然后,我们使用 useReducer 创建了一个状态 state 和一个 dispatch 函数,dispatch 函数用于向 reducer 发送一个 action,从而更新状态。

当点击按钮时,我们调用 dispatch 函数并传递一个描述性的 action 对象,reducer 函数会根据 actiontype 来更新状态。最后,我们根据新的状态重新渲染了组件,从而实现了一个简单的计数器功能。

总之,useReducer 可以帮助我们更好地管理状态逻辑,并且在某些情况下可以比 useState 更清晰地表达状态之间的关系。

你可能感兴趣的:(前端react技术积累,react.js,javascript,前端,前端框架)