React

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > React > React Hooks介绍

React Hooks之useState,useEffect及自定义Hook的最佳实践

作者:天天进步2015

React Hooks自16.8版本引入以来,彻底改变了我们编写React组件的方式,本文将深入探讨三个最重要的Hooks:useState、useEffect,以及如何创建和使用自定义Hooks,感兴趣的小伙伴可以了解下

React Hooks自16.8版本引入以来,彻底改变了我们编写React组件的方式。它们让函数组件拥有了状态管理和生命周期方法的能力,使代码更加简洁、可复用且易于测试。本文将深入探讨三个最重要的Hooks:useState、useEffect,以及如何创建和使用自定义Hooks。

1. useState:状态管理的基石

基础用法

useState是最基础也是最常用的Hook,它让函数组件能够拥有内部状态。

import React, { useState } from 'react';

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

  return (
    <div>
      <p>当前计数: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        增加
      </button>
    </div>
  );
}

状态更新的最佳实践

1. 使用函数式更新

当新状态依赖于旧状态时,推荐使用函数式更新:

// ❌ 不推荐:直接使用状态值
setCount(count + 1);

// ✅ 推荐:使用函数式更新
setCount(prevCount => prevCount + 1);

函数式更新的优势在于确保获取到最新的状态值,避免闭包陷阱。

2. 合并对象状态

useState不会自动合并对象,需要手动合并:

const [user, setUser] = useState({
  name: '',
  email: '',
  age: 0
});

// ❌ 错误:会覆盖整个对象
setUser({ name: 'Alice' });

// ✅ 正确:手动合并
setUser(prevUser => ({
  ...prevUser,
  name: 'Alice'
}));

3. 初始状态的惰性计算

对于复杂的初始状态计算,使用惰性初始化:

// ❌ 每次渲染都会执行计算
const [expensiveValue, setExpensiveValue] = useState(computeExpensiveValue());

// ✅ 只在初始化时执行一次
const [expensiveValue, setExpensiveValue] = useState(() => computeExpensiveValue());

2. useEffect:副作用管理专家

基础概念

useEffect用于处理组件的副作用,如数据获取、订阅、手动修改DOM等。

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

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchUser() {
      setLoading(true);
      try {
        const response = await fetch(`/api/users/${userId}`);
        const userData = await response.json();
        setUser(userData);
      } catch (error) {
        console.error('获取用户信息失败:', error);
      } finally {
        setLoading(false);
      }
    }

    fetchUser();
  }, [userId]); // 依赖数组

  if (loading) return <div>加载中...</div>;
  if (!user) return <div>用户未找到</div>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

依赖数组的深度理解

1. 空依赖数组

useEffect(() => {
  // 只在组件挂载时执行一次
  console.log('组件已挂载');
}, []); // 空数组

2. 无依赖数组

useEffect(() => {
  // 每次渲染后都执行
  console.log('每次渲染后执行');
}); // 无依赖数组

3. 有依赖的数组

useEffect(() => {
  // 当count或name发生变化时执行
  console.log('count或name发生了变化');
}, [count, name]); // 依赖count和name

清理副作用

对于需要清理的副作用(如定时器、订阅),useEffect可以返回一个清理函数:

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

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

    // 返回清理函数
    return () => {
      clearInterval(intervalId);
    };
  }, []); // 只设置一次定时器

  return <div>已运行 {seconds} 秒</div>;
}

useEffect的最佳实践

1. 合理拆分effect

将不同关注点的副作用分离到不同的useEffect中:

function UserDashboard({ userId }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);

  // 获取用户信息
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  // 获取用户帖子
  useEffect(() => {
    fetchUserPosts(userId).then(setPosts);
  }, [userId]);

  // 设置页面标题
  useEffect(() => {
    if (user) {
      document.title = `${user.name}的仪表板`;
    }
  }, [user]);

  // ...
}

2. 避免无限循环

确保依赖数组正确,避免不必要的重新执行:

function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  // ❌ 可能造成无限循环
  useEffect(() => {
    search(query).then(setResults);
  }, [results]); // results变化会再次触发

  // ✅ 正确的依赖
  useEffect(() => {
    search(query).then(setResults);
  }, [query]); // 只有query变化时才执行
}

3. 自定义Hook:代码复用的艺术

自定义Hook是以"use"开头的函数,可以在其内部调用其他Hook。它们是提取组件逻辑到可重用函数的强大方式。

创建第一个自定义Hook

useCounter:计数器逻辑

import { useState } from 'react';

function useCounter(initialValue = 0, step = 1) {
  const [count, setCount] = useState(initialValue);

  const increment = () => setCount(prev => prev + step);
  const decrement = () => setCount(prev => prev - step);
  const reset = () => setCount(initialValue);

  return {
    count,
    increment,
    decrement,
    reset,
    setCount
  };
}

// 使用自定义Hook
function Counter() {
  const { count, increment, decrement, reset } = useCounter(0, 2);

  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={increment}>+2</button>
      <button onClick={decrement}>-2</button>
      <button onClick={reset}>重置</button>
    </div>
  );
}

高级自定义Hook示例

useAPI:数据获取Hook

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    let cancelled = false;

    async function fetchData() {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(url, options);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const result = await response.json();
        
        if (!cancelled) {
          setData(result);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err.message);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    fetchData();

    // 清理函数:取消请求
    return () => {
      cancelled = true;
    };
  }, [url, JSON.stringify(options)]);

  const refetch = () => {
    setLoading(true);
    setError(null);
    // 触发重新获取
  };

  return { data, loading, error, refetch };
}

// 使用示例
function UserList() {
  const { data: users, loading, error } = useAPI('/api/users');

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;

  return (
    <ul>
      {users?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

useLocalStorage:本地存储Hook

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  // 获取初始值
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`获取localStorage中的${key}失败:`, error);
      return initialValue;
    }
  });

  // 更新localStorage的函数
  const setValue = value => {
    try {
      // 允许value是函数,用于函数式更新
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(`设置localStorage中的${key}失败:`, error);
    }
  };

  return [storedValue, setValue];
}

// 使用示例
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [language, setLanguage] = useLocalStorage('language', 'zh');

  return (
    <div>
      <select value={theme} onChange={e => setTheme(e.target.value)}>
        <option value="light">浅色</option>
        <option value="dark">深色</option>
      </select>
      <select value={language} onChange={e => setLanguage(e.target.value)}>
        <option value="zh">中文</option>
        <option value="en">English</option>
      </select>
    </div>
  );
}

自定义Hook的设计原则

1. 单一职责原则

每个自定义Hook应该只做一件事,并且做好:

// ✅ 好:专注于表单验证
function useFormValidation(initialValues, validationRules) {
  // 验证逻辑
}

// ✅ 好:专注于API调用
function useAPI(url) {
  // API调用逻辑
}

// ❌ 不好:职责混乱
function useFormAPIValidation(url, initialValues, rules) {
  // 既处理API又处理验证
}

2. 清晰的接口设计

返回值应该直观易懂:

// ✅ 好:清晰的返回值
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = () => setValue(prev => !prev);
  const setTrue = () => setValue(true);
  const setFalse = () => setValue(false);

  return { value, toggle, setTrue, setFalse };
}

// ✅ 也可以返回数组(类似useState)
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = () => setValue(prev => !prev);

  return [value, toggle];
}

3. 处理边界情况

考虑各种边界情况和错误处理:

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    // 处理delay为0或负数的情况
    if (delay <= 0) {
      setDebouncedValue(value);
      return;
    }

    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
}

4. Hook使用的常见陷阱与解决方案

陷阱1:闭包陷阱

// ❌ 问题代码
function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1); // 总是使用初始值0
    }, 1000);

    return () => clearInterval(timer);
  }, []); // 空依赖数组

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

// ✅ 解决方案1:使用函数式更新
function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);

    return () => clearInterval(timer);
  }, []);

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

// ✅ 解决方案2:包含依赖
function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1);
    }, 1000);

    return () => clearInterval(timer);
  }, [count]); // 包含count依赖

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

陷阱2:依赖数组遗漏

// ❌ 问题代码
function UserProfile({ userId, theme }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId, theme).then(setUser);
  }, [userId]); // 遗漏了theme依赖

  return <div>{user?.name}</div>;
}

// ✅ 解决方案
function UserProfile({ userId, theme }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId, theme).then(setUser);
  }, [userId, theme]); // 包含所有依赖

  return <div>{user?.name}</div>;
}

5. 性能优化技巧

使用React.memo减少不必要的渲染

import React, { memo } from 'react';

const ExpensiveComponent = memo(function ExpensiveComponent({ data, onUpdate }) {
  // 只有当data或onUpdate发生变化时才重新渲染
  return (
    <div>
      {/* 复杂的渲染逻辑 */}
    </div>
  );
});

使用useCallback和useMemo

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

function OptimizedComponent({ items }) {
  const [query, setQuery] = useState('');

  // 缓存过滤后的结果
  const filteredItems = useMemo(() => {
    return items.filter(item => 
      item.name.toLowerCase().includes(query.toLowerCase())
    );
  }, [items, query]);

  // 缓存事件处理函数
  const handleSearch = useCallback((e) => {
    setQuery(e.target.value);
  }, []);

  return (
    <div>
      <input value={query} onChange={handleSearch} />
      <ItemList items={filteredItems} />
    </div>
  );
}

React Hooks为我们提供了强大而灵活的方式来管理组件状态和副作用。通过合理使用useState、useEffect和自定义Hook,我们可以编写出更加简洁、可维护和可复用的React代码。

记住这些最佳实践:

随着对Hooks理解的深入,你会发现它们不仅改变了我们编写React的方式,更重要的是改变了我们思考组件逻辑的方式。

到此这篇关于React Hooks之useState,useEffect及自定义Hook的最佳实践的文章就介绍到这了,更多相关React Hooks介绍内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文