MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React Hooks基础入门与实战

2024-06-264.4k 阅读

React Hooks是什么

在React中,Hooks是一种在不编写class的情况下使用state以及其他React特性的方式。React从16.8版本开始引入Hooks,这是一次重大的变革,它使得函数式组件能够拥有与class组件相似的功能,例如状态管理和副作用操作。

为什么需要React Hooks

在Hooks出现之前,React应用主要通过class组件来管理状态和处理副作用。然而,class组件存在一些问题:

  1. 代码复用困难:在class组件中复用状态逻辑比较麻烦,通常需要使用高阶组件(Higher - Order Components, HOC)或渲染属性(Render Props),但这两种方式都会导致“嵌套地狱”问题,使代码变得难以阅读和维护。
  2. this指向问题:在class组件中,this的指向常常令人困惑,特别是在处理事件绑定和回调函数时,需要手动绑定this
  3. 逻辑分散:随着组件功能的增加,class组件中的render方法可能变得非常庞大,不同的逻辑(如状态更新、副作用操作等)混合在一起,难以理解和维护。

Hooks的出现很好地解决了这些问题。它允许将状态逻辑拆分成更小的函数,并且可以在不同的组件之间复用这些逻辑,同时避免了this指向的问题,使代码更加简洁和易于维护。

useState基础

useState是React Hooks中最基本的Hook,用于在函数组件中添加状态。

基本使用

useState接收一个初始状态值,并返回一个数组,数组的第一个元素是当前状态值,第二个元素是用于更新状态的函数。以下是一个简单的计数器示例:

import React, { useState } from'react';

function Counter() {
  // 声明一个名为count的状态变量,初始值为0
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

export default Counter;

在上述代码中,通过useState(0)声明了一个初始值为0的状态变量count,以及用于更新count的函数setCount。当按钮被点击时,setCount(count + 1)会将count的值加1。

状态更新机制

  1. 批量更新:React会批量处理状态更新,以提高性能。例如,在一个事件处理函数中多次调用setState,React会将这些更新合并,只进行一次重新渲染。
import React, { useState } from'react';

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

  const handleClick = () => {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  };

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

export default BatchUpdateExample;

在这个例子中,尽管多次调用了setCount,但最终count只会增加1,因为React会将这些更新合并。

  1. 函数式更新:当新状态依赖于旧状态时,应该使用函数式更新。例如:
import React, { useState } from'react';

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

  const handleClick = () => {
    setCount(prevCount => prevCount + 1);
  };

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

export default FunctionalUpdateExample;

setCount(prevCount => prevCount + 1)中,prevCount是旧的状态值,通过返回prevCount + 1来更新状态。这样可以确保在状态更新时使用的是最新的旧状态值,特别是在批量更新的情况下。

useEffect基础

useEffect用于在函数组件中执行副作用操作,例如数据获取、订阅事件、手动修改DOM等。

基本使用

useEffect接收一个函数作为参数,这个函数会在组件渲染后和每次更新后执行。以下是一个简单的示例,在组件挂载和更新时打印一条消息:

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

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

  useEffect(() => {
    console.log('Component has mounted or updated');
  });

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

export default EffectExample;

在上述代码中,useEffect中的回调函数会在组件首次渲染后以及每次count更新后执行,打印出消息。

清除副作用

有些副作用操作需要在组件卸载时进行清理,例如取消订阅事件、清除定时器等。useEffect的回调函数可以返回一个清理函数,这个清理函数会在组件卸载时执行。以下是一个定时器的示例:

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

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setSeconds(seconds => seconds + 1);
    }, 1000);

    return () => {
      clearInterval(intervalId);
    };
  }, []);

  return (
    <div>
      <p>Seconds elapsed: {seconds}</p>
    </div>
  );
}

export default Timer;

在这个例子中,useEffect使用setInterval创建了一个每秒更新一次seconds状态的定时器。返回的清理函数clearInterval(intervalId)会在组件卸载时清除定时器,防止内存泄漏。

依赖数组

useEffect的第二个参数是一个依赖数组。只有当依赖数组中的值发生变化时,useEffect的回调函数才会执行。例如:

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

function DependenceExample() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  useEffect(() => {
    console.log('Count has changed');
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
    </div>
  );
}

export default DependenceExample;

在上述代码中,useEffect的依赖数组为[count],只有当count的值发生变化时,useEffect的回调函数才会执行。当name的值发生变化时,由于不在依赖数组中,useEffect不会执行。

如果依赖数组为空[],则useEffect的回调函数只会在组件挂载时执行一次,类似于componentDidMount生命周期方法。

自定义Hooks

自定义Hooks允许将可复用的状态逻辑提取到独立的函数中,从而提高代码的复用性和可维护性。

创建自定义Hooks

创建自定义Hooks非常简单,只需要创建一个以use开头的函数,并在函数内部调用其他Hooks。以下是一个自定义的useFetch Hook示例,用于获取数据:

import { useState, useEffect } from'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
        setLoading(false);
      } catch (error) {
        setError(error);
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

export default useFetch;

在上述代码中,useFetch Hook接收一个url参数,使用useState来管理数据、加载状态和错误状态。useEffect在组件挂载和url变化时发起数据请求,并根据请求结果更新状态。最后返回一个包含数据、加载状态和错误的对象。

使用自定义Hooks

使用自定义Hooks也很简单,只需要在函数组件中调用即可。以下是使用useFetch Hook的示例:

import React from'react';
import useFetch from './useFetch';

function DataComponent() {
  const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/todos/1');

  if (loading) {
    return <p>Loading...</p>;
  }

  if (error) {
    return <p>Error: {error.message}</p>;
  }

  return (
    <div>
      <p>{data.title}</p>
    </div>
  );
}

export default DataComponent;

在这个组件中,通过useFetch获取数据,并根据loadingerror状态进行相应的渲染。

useContext基础

useContext用于在组件之间共享数据,避免了通过props层层传递数据的繁琐过程。

创建Context

首先,需要使用React.createContext创建一个Context对象。以下是一个简单的示例:

import React from'react';

const ThemeContext = React.createContext();

export default ThemeContext;

在上述代码中,创建了一个名为ThemeContext的Context对象。

使用Context.Provider提供数据

使用Context.Provider组件将数据传递给后代组件。例如:

import React from'react';
import ThemeContext from './ThemeContext';

function App() {
  const theme = { color: 'blue' };

  return (
    <ThemeContext.Provider value={theme}>
      <div>
        <ChildComponent />
      </div>
    </ThemeContext.Provider>
  );
}

function ChildComponent() {
  return (
    <div>
      <GrandChildComponent />
    </div>
  );
}

function GrandChildComponent() {
  return (
    <div>
      {/* 这里将使用ThemeContext中的数据 */}
    </div>
  );
}

export default App;

App组件中,通过ThemeContext.Providertheme对象作为value传递下去,所有的后代组件都可以访问这个数据。

使用useContext消费数据

在需要使用Context数据的组件中,可以使用useContext Hook。例如:

import React, { useContext } from'react';
import ThemeContext from './ThemeContext';

function GrandChildComponent() {
  const theme = useContext(ThemeContext);

  return (
    <div style={{ color: theme.color }}>
      This text has a blue color
    </div>
  );
}

export default GrandChildComponent;

GrandChildComponent中,通过useContext(ThemeContext)获取到ThemeContext中的theme对象,并使用其中的color属性设置文本颜色。

useReducer基础

useReduceruseState的一种替代方案,它更适合用于管理复杂的状态逻辑,特别是当状态更新需要依赖于之前的状态,并且有多个不同的更新动作时。

基本使用

useReducer接收两个参数:一个reducer函数和初始状态。reducer函数接收当前状态和一个action对象,并返回新的状态。以下是一个简单的计数器示例,使用useReducer

import React, { useReducer } from'react';

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

function CounterWithReducer() {
  const [count, dispatch] = useReducer(counterReducer, 0);

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

export default CounterWithReducer;

在上述代码中,counterReducer是reducer函数,根据action.type来决定如何更新状态。useReducer(counterReducer, 0)初始化状态为0,并返回当前状态count和用于派发action的dispatch函数。当按钮被点击时,通过dispatch派发相应的action来更新状态。

与Redux的关系

useReducer与Redux的原理类似,都是通过reducer函数来处理状态更新。不同的是,useReducer通常用于组件内部的状态管理,而Redux更适合用于整个应用的状态管理,并且提供了诸如中间件、时间旅行调试等更多功能。

实战项目:简单的待办事项列表

通过一个简单的待办事项列表项目,来综合运用上述的React Hooks。

项目功能

  1. 添加待办事项:用户可以在输入框中输入待办事项内容,并点击“添加”按钮将其添加到列表中。
  2. 标记待办事项为完成:用户可以点击待办事项前面的复选框,将其标记为完成。
  3. 删除待办事项:用户可以点击待办事项后面的“删除”按钮,将其从列表中删除。

项目实现

  1. 创建项目结构
todo - list/
├── src/
│   ├── components/
│   │   ├── TodoItem.js
│   │   └── TodoList.js
│   ├── App.js
│   └── index.js
├── package.json
└── README.md
  1. 编写TodoItem.js组件
import React, { useState } from'react';

function TodoItem({ todo, onToggle, onDelete }) {
  const [isEditing, setIsEditing] = useState(false);
  const [editedText, setEditedText] = useState(todo.text);

  const handleToggle = () => {
    onToggle(todo.id);
  };

  const handleDelete = () => {
    onDelete(todo.id);
  };

  const handleEdit = () => {
    setIsEditing(true);
  };

  const handleSave = () => {
    setIsEditing(false);
    // 这里可以添加保存到服务器的逻辑
  };

  return (
    <li>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={handleToggle}
      />
      {isEditing? (
        <input
          type="text"
          value={editedText}
          onChange={(e) => setEditedText(e.target.value)}
        />
      ) : (
        <span>{todo.text}</span>
      )}
      {isEditing? (
        <button onClick={handleSave}>Save</button>
      ) : (
        <button onClick={handleEdit}>Edit</button>
      )}
      <button onClick={handleDelete}>Delete</button>
    </li>
  );
}

export default TodoItem;

TodoItem组件中,使用useState来管理是否处于编辑状态以及编辑后的文本。通过props接收onToggleonDelete函数,用于处理复选框点击和删除按钮点击事件。

  1. 编写TodoList.js组件
import React, { useState, useEffect } from'react';
import TodoItem from './TodoItem';

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [newTodoText, setNewTodoText] = useState('');

  const addTodo = () => {
    if (newTodoText.trim()!== '') {
      const newTodo = {
        id: Date.now(),
        text: newTodoText,
        completed: false
      };
      setTodos([...todos, newTodo]);
      setNewTodoText('');
    }
  };

  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id? { ...todo, completed:!todo.completed } : todo
    ));
  };

  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id!== id));
  };

  useEffect(() => {
    // 模拟从本地存储加载数据
    const storedTodos = JSON.parse(localStorage.getItem('todos')) || [];
    setTodos(storedTodos);
  }, []);

  useEffect(() => {
    // 模拟保存数据到本地存储
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  return (
    <div>
      <input
        type="text"
        placeholder="Add a new todo"
        value={newTodoText}
        onChange={(e) => setNewTodoText(e.target.value)}
      />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.map(todo => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={toggleTodo}
            onDelete={deleteTodo}
          />
        ))}
      </ul>
    </div>
  );
}

export default TodoList;

TodoList组件中,使用useState来管理待办事项列表和新待办事项的文本。addTodo函数用于添加新的待办事项,toggleTodo函数用于切换待办事项的完成状态,deleteTodo函数用于删除待办事项。通过useEffect在组件挂载时从本地存储加载数据,并在todos状态变化时将数据保存到本地存储。

  1. 编写App.js组件
import React from'react';
import TodoList from './components/TodoList';

function App() {
  return (
    <div>
      <h1>Simple Todo List</h1>
      <TodoList />
    </div>
  );
}

export default App;

App组件简单地渲染了TodoList组件。

通过这个实战项目,我们可以看到如何在实际应用中综合运用React Hooks来实现一个完整的功能。

React Hooks的性能优化

在使用React Hooks时,也需要关注性能问题,以下是一些性能优化的方法:

  1. 合理使用依赖数组:在useEffectuseCallbackuseMemo中,正确设置依赖数组可以避免不必要的重新渲染。确保依赖数组中的值确实是回调函数或计算值所依赖的,不要遗漏依赖,但也不要添加不必要的依赖。
  2. 使用useMemouseCallback
    • useMemo用于缓存计算结果,只有当依赖数组中的值发生变化时才重新计算。例如:
import React, { useState, useMemo } from'react';

function ExpensiveCalculation() {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState(0);

  const result = useMemo(() => {
    // 模拟一个复杂的计算
    let sum = 0;
    for (let i = 0; i < 1000000; i++) {
      sum += i;
    }
    return sum + value;
  }, [value]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <input
        type="number"
        value={value}
        onChange={(e) => setValue(parseInt(e.target.value))}
      />
      <p>Result: {result}</p>
    </div>
  );
}

export default ExpensiveCalculation;

在上述代码中,result的计算依赖于value,只有当value变化时,result才会重新计算,而count的变化不会触发result的重新计算。 - useCallback用于缓存函数,只有当依赖数组中的值发生变化时才重新创建函数。这在将函数作为props传递给子组件时非常有用,可以避免子组件不必要的重新渲染。例如:

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

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

  const handleClick = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return (
    <div>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

function ChildComponent({ onClick }) {
  return (
    <button onClick={onClick}>Click me</button>
  );
}

export default ParentComponent;

在这个例子中,handleClick函数只有在count变化时才会重新创建,从而避免了ChildComponent不必要的重新渲染。

  1. 避免在循环或条件语句中使用Hooks:React依赖于Hooks的调用顺序来正确管理状态和副作用,在循环或条件语句中使用Hooks会导致调用顺序不稳定,从而引发难以调试的问题。确保Hooks在函数组件的顶层被调用。

总结React Hooks的优势与注意事项

  1. 优势
    • 代码复用:自定义Hooks使得状态逻辑的复用变得非常简单,避免了高阶组件和渲染属性带来的嵌套问题。
    • 简洁代码:函数式组件加上Hooks使代码更加简洁,避免了class组件中this指向的问题和复杂的生命周期方法。
    • 逻辑拆分:可以将不同的状态逻辑拆分成多个更小的Hook函数,使代码更易于理解和维护。
  2. 注意事项
    • 调用规则:必须在函数组件的顶层调用Hooks,不能在循环、条件语句或嵌套函数中调用,以保证Hooks的调用顺序稳定。
    • 依赖数组:在useEffectuseCallbackuseMemo中,要正确设置依赖数组,避免因依赖设置不当导致的性能问题或逻辑错误。
    • 状态更新:在使用useState进行状态更新时,要注意批量更新和函数式更新的机制,特别是在新状态依赖于旧状态的情况下。

通过深入理解和掌握React Hooks的基础知识与实战应用,开发者可以更加高效地构建React应用,提升代码的质量和可维护性。