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

React 自定义 Hooks 在组件开发中的应用

2021-10-102.7k 阅读

React 自定义 Hooks 基础概念

在 React 开发中,Hooks 是一项强大的特性,它允许我们在不编写类的情况下使用 state 以及其他 React 特性。React 自带了一些像 useStateuseEffect 这样的内置 Hooks。而自定义 Hooks 则是基于这些内置 Hooks 构建的,可以在多个组件之间复用有状态逻辑的函数。

自定义 Hooks 本质上就是一个 JavaScript 函数,其命名约定以 use 开头,并且可以调用其他的 Hooks。例如,假设我们有一个需求,在多个组件中都要获取当前窗口的宽度,我们可以创建一个自定义 Hooks 来实现这个逻辑的复用。

import { useState, useEffect } from 'react';

const useWindowWidth = () => {
    const [width, setWidth] = useState(window.innerWidth);

    useEffect(() => {
        const handleResize = () => {
            setWidth(window.innerWidth);
        };
        window.addEventListener('resize', handleResize);
        return () => {
            window.removeEventListener('resize', handleResize);
        };
    }, []);

    return width;
};

const MyComponent = () => {
    const windowWidth = useWindowWidth();
    return (
        <div>
            <p>当前窗口宽度: {windowWidth}</p>
        </div>
    );
};

在上述代码中,useWindowWidth 就是一个自定义 Hooks。它内部使用了 useState 来管理窗口宽度的状态,使用 useEffect 来监听窗口的 resize 事件,并在组件卸载时移除事件监听器。任何组件只要调用 useWindowWidth,就可以轻松获取当前窗口的宽度,实现了逻辑的复用。

自定义 Hooks 的优势

  1. 逻辑复用:在传统的 React 开发中,如果要在多个组件之间复用有状态的逻辑,可能需要使用高阶组件(Higher - Order Components,HOC)或者 render props 模式。这些模式虽然也能实现逻辑复用,但会导致组件层级嵌套过深,代码难以理解和维护。而自定义 Hooks 则可以在不增加组件嵌套的情况下,实现逻辑的复用。例如,假设有多个组件都需要进行数据的异步加载和缓存,使用自定义 Hooks 可以将这部分逻辑封装起来,每个组件只需要调用这个自定义 Hooks 即可。
import { useState, useEffect } from'react';

const useAsyncData = (url) => {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);

    useEffect(() => {
        const fetchData = async () => {
            setLoading(true);
            try {
                const response = await fetch(url);
                if (!response.ok) {
                    throw new Error('Network response was not ok');
                }
                const result = await response.json();
                setData(result);
            } catch (error) {
                setError(error);
            } finally {
                setLoading(false);
            }
        };
        fetchData();
    }, [url]);

    return { data, loading, error };
};

const ComponentA = () => {
    const { data, loading, error } = useAsyncData('https://example.com/api/data1');
    if (loading) return <p>加载中...</p>;
    if (error) return <p>错误: {error.message}</p>;
    return (
        <div>
            <p>Component A的数据: {JSON.stringify(data)}</p>
        </div>
    );
};

const ComponentB = () => {
    const { data, loading, error } = useAsyncData('https://example.com/api/data2');
    if (loading) return <p>加载中...</p>;
    if (error) return <p>错误: {error.message}</p>;
    return (
        <div>
            <p>Component B的数据: {JSON.stringify(data)}</p>
        </div>
    );
};

在这个例子中,useAsyncData 自定义 Hooks 封装了异步数据获取的逻辑,包括加载状态、数据和错误处理。ComponentAComponentB 都可以复用这个逻辑,而不需要重复编写异步请求的代码。

  1. 代码简洁性:自定义 Hooks 使得组件的代码更加简洁明了。因为将一些复杂的逻辑提取到了自定义 Hooks 中,组件本身只需要关注如何使用这些逻辑,而不需要关心具体的实现细节。这使得组件的代码量减少,可读性提高。例如,在一个表单组件中,如果要实现输入防抖的功能,使用自定义 Hooks 可以将防抖逻辑封装起来,表单组件只需要调用这个自定义 Hooks 并传入相应的参数即可。
import { useState, useEffect } from'react';

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

    useEffect(() => {
        const handler = setTimeout(() => {
            setDebouncedValue(value);
        }, delay);
        return () => {
            clearTimeout(handler);
        };
    }, [value, delay]);

    return debouncedValue;
};

const MyForm = () => {
    const [inputValue, setInputValue] = useState('');
    const debouncedValue = useDebounce(inputValue, 500);

    return (
        <div>
            <input
                type="text"
                value={inputValue}
                onChange={(e) => setInputValue(e.target.value)}
                placeholder="输入内容"
            />
            <p>防抖后的值: {debouncedValue}</p>
        </div>
    );
};

在上述代码中,useDebounce 自定义 Hooks 封装了防抖逻辑,MyForm 组件只需要调用它并传入输入值和延迟时间,就可以轻松实现输入防抖功能,组件代码简洁清晰。

  1. 易于测试:由于自定义 Hooks 是普通的 JavaScript 函数,测试它们相对比较容易。可以使用 Jest 等测试框架,对自定义 Hooks 进行单元测试,确保其逻辑的正确性。例如,对于前面提到的 useDebounce 自定义 Hooks,可以编写如下测试用例:
import { renderHook, act } from '@testing-library/react-hooks';
import useDebounce from './useDebounce';

describe('useDebounce', () => {
    it('should return the initial value immediately', () => {
        const { result } = renderHook(() => useDebounce('initial', 500));
        expect(result.current).toBe('initial');
    });

    it('should update the debounced value after the delay', () => {
        const { result } = renderHook(() => useDebounce('initial', 500));
        act(() => {
            // 模拟值的变化
            result.current = 'new value';
        });
        setTimeout(() => {
            expect(result.current).toBe('new value');
        }, 500);
    });
});

在这个测试用例中,使用 @testing-library/react - hooks 库提供的 renderHook 来测试 useDebounce 自定义 Hooks。通过 act 来模拟值的变化,并使用 setTimeout 来模拟延迟,测试了 useDebounce 的基本功能。

在复杂组件中使用自定义 Hooks

  1. 状态管理与复用:在一些复杂的组件中,可能会有多个状态需要管理,并且这些状态管理逻辑在其他组件中也可能会用到。例如,在一个电商应用的商品列表组件中,可能需要管理商品的筛选条件(如价格范围、品牌等),同时在搜索结果页面也需要类似的筛选条件管理逻辑。这时可以创建一个自定义 Hooks 来管理这些筛选条件的状态。
import { useState, useEffect } from'react';

const useFilterState = () => {
    const [priceRange, setPriceRange] = useState({ min: 0, max: Infinity });
    const [brands, setBrands] = useState([]);

    const updatePriceRange = (newRange) => {
        setPriceRange(newRange);
    };

    const toggleBrand = (brand) => {
        if (brands.includes(brand)) {
            setBrands(brands.filter(b => b!== brand));
        } else {
            setBrands([...brands, brand]);
        }
    };

    return {
        priceRange,
        brands,
        updatePriceRange,
        toggleBrand
    };
};

const ProductList = () => {
    const { priceRange, brands, updatePriceRange, toggleBrand } = useFilterState();

    // 根据筛选条件获取商品列表数据的逻辑

    return (
        <div>
            <input
                type="number"
                placeholder="最小价格"
                value={priceRange.min}
                onChange={(e) => {
                    const newMin = parseInt(e.target.value, 10);
                    updatePriceRange({...priceRange, min: newMin });
                }}
            />
            <input
                type="number"
                placeholder="最大价格"
                value={priceRange.max}
                onChange={(e) => {
                    const newMax = parseInt(e.target.value, 10);
                    updatePriceRange({...priceRange, max: newMax });
                }}
            />
            <div>
                <label>
                    <input
                        type="checkbox"
                        value="brand1"
                        onChange={() => toggleBrand('brand1')}
                    />
                    brand1
                </label>
                <label>
                    <input
                        type="checkbox"
                        value="brand2"
                        onChange={() => toggleBrand('brand2')}
                    />
                    brand2
                </label>
            </div>
            {/* 显示商品列表 */}
        </div>
    );
};

const SearchResult = () => {
    const { priceRange, brands, updatePriceRange, toggleBrand } = useFilterState();

    // 根据筛选条件获取搜索结果数据的逻辑

    return (
        <div>
            <input
                type="number"
                placeholder="最小价格"
                value={priceRange.min}
                onChange={(e) => {
                    const newMin = parseInt(e.target.value, 10);
                    updatePriceRange({...priceRange, min: newMin });
                }}
            />
            <input
                type="number"
                placeholder="最大价格"
                value={priceRange.max}
                onChange={(e) => {
                    const newMax = parseInt(e.target.value, 10);
                    updatePriceRange({...priceRange, max: newMax });
                }}
            />
            <div>
                <label>
                    <input
                        type="checkbox"
                        value="brand1"
                        onChange={() => toggleBrand('brand1')}
                    />
                    brand1
                </label>
                <label>
                    <input
                        type="checkbox"
                        value="brand2"
                        onChange={() => toggleBrand('brand2')}
                    />
                    brand2
                </label>
            </div>
            {/* 显示搜索结果 */}
        </div>
    );
};

在上述代码中,useFilterState 自定义 Hooks 封装了商品筛选条件的状态管理逻辑,包括价格范围和品牌筛选。ProductListSearchResult 组件都可以复用这个逻辑,使得代码更加简洁和可维护。

  1. 副作用处理:复杂组件中往往会有多个副作用操作,如数据的异步加载、订阅事件等。通过自定义 Hooks 可以将这些副作用操作进行封装和复用。例如,在一个实时聊天组件中,需要连接到 WebSocket 服务器并监听新消息。同时,在其他一些需要实时数据更新的组件中也可能需要类似的 WebSocket 连接逻辑。
import { useState, useEffect } from'react';

const useWebSocket = (url) => {
    const [message, setMessage] = useState(null);
    const [isConnected, setIsConnected] = useState(false);

    useEffect(() => {
        const socket = new WebSocket(url);
        socket.onopen = () => {
            setIsConnected(true);
        };
        socket.onmessage = (event) => {
            setMessage(JSON.parse(event.data));
        };
        socket.onclose = () => {
            setIsConnected(false);
        };
        return () => {
            socket.close();
        };
    }, [url]);

    const sendMessage = (data) => {
        if (isConnected) {
            socket.send(JSON.stringify(data));
        }
    };

    return { message, isConnected, sendMessage };
};

const ChatComponent = () => {
    const { message, isConnected, sendMessage } = useWebSocket('ws://example.com/socket');

    const handleSend = (text) => {
        sendMessage({ type: 'chat', text });
    };

    return (
        <div>
            {isConnected? (
                <div>
                    <p>收到的消息: {JSON.stringify(message)}</p>
                    <input
                        type="text"
                        placeholder="输入消息"
                        onChange={(e) => handleSend(e.target.value)}
                    />
                </div>
            ) : (
                <p>未连接到服务器</p>
            )}
        </div>
    );
};

const RealTimeDataComponent = () => {
    const { message, isConnected, sendMessage } = useWebSocket('ws://example.com/data - socket');

    return (
        <div>
            {isConnected? (
                <p>实时数据: {JSON.stringify(message)}</p>
            ) : (
                <p>未连接到数据服务器</p>
            )}
        </div>
    );
};

在上述代码中,useWebSocket 自定义 Hooks 封装了 WebSocket 的连接、消息接收和发送逻辑。ChatComponentRealTimeDataComponent 组件都可以复用这个逻辑,分别用于聊天功能和实时数据获取功能。

自定义 Hooks 与 React 上下文(Context)结合使用

  1. 共享状态管理:React 上下文(Context)提供了一种在组件树中共享数据的方式,而自定义 Hooks 可以与上下文结合,更好地管理共享状态。例如,在一个多语言应用中,需要在整个应用中共享当前语言设置。可以创建一个上下文和一个自定义 Hooks 来管理语言状态。
import { createContext, useState, useEffect, useContext } from'react';

const LanguageContext = createContext();

const useLanguage = () => {
    const [language, setLanguage] = useState('en');

    const changeLanguage = (newLanguage) => {
        setLanguage(newLanguage);
    };

    return { language, changeLanguage };
};

const LanguageProvider = ({ children }) => {
    const { language, changeLanguage } = useLanguage();
    return (
        <LanguageContext.Provider value={{ language, changeLanguage }}>
            {children}
        </LanguageContext.Provider>
    );
};

const HeaderComponent = () => {
    const { language, changeLanguage } = useContext(LanguageContext);
    return (
        <div>
            <p>当前语言: {language}</p>
            <button onClick={() => changeLanguage('zh')}>切换到中文</button>
            <button onClick={() => changeLanguage('en')}>切换到英文</button>
        </div>
    );
};

const BodyComponent = () => {
    const { language } = useContext(LanguageContext);
    return (
        <div>
            {language === 'en'? <p>Content in English</p> : <p>中文内容</p>}
        </div>
    );
};

const App = () => {
    return (
        <LanguageProvider>
            <HeaderComponent />
            <BodyComponent />
        </LanguageProvider>
    );
};

在上述代码中,LanguageContext 是一个上下文对象,useLanguage 自定义 Hooks 管理语言状态。LanguageProvider 组件通过 LanguageContext.Provider 向子组件提供语言状态和切换语言的方法。HeaderComponentBodyComponent 组件通过 useContext 来获取语言状态并进行相应的展示和操作。

  1. 跨组件通信优化:通过自定义 Hooks 和上下文结合,可以优化跨组件通信。例如,在一个大型的单页应用中,不同层级的组件之间可能需要进行通信,如顶部导航栏的操作可能需要通知底部的一些组件进行更新。使用自定义 Hooks 和上下文可以简化这种通信过程。
import { createContext, useState, useEffect, useContext } from'react';

const GlobalEventContext = createContext();

const useGlobalEvent = () => {
    const [events, setEvents] = useState([]);

    const emitEvent = (event) => {
        setEvents([...events, event]);
    };

    const onEvent = (callback) => {
        useEffect(() => {
            events.forEach(event => callback(event));
            const unsubscribe = () => {
                // 可以在这里实现更复杂的取消订阅逻辑
            };
            return unsubscribe;
        }, [events]);
    };

    return { emitEvent, onEvent };
};

const GlobalEventProvider = ({ children }) => {
    const { emitEvent, onEvent } = useGlobalEvent();
    return (
        <GlobalEventContext.Provider value={{ emitEvent, onEvent }}>
            {children}
        </GlobalEventContext.Provider>
    );
};

const TopNavigation = () => {
    const { emitEvent } = useContext(GlobalEventContext);
    const handleClick = () => {
        emitEvent({ type: 'top - nav - click', data: 'Some data' });
    };
    return (
        <div>
            <button onClick={handleClick}>顶部导航按钮</button>
        </div>
    );
};

const BottomComponent = () => {
    const { onEvent } = useContext(GlobalEventContext);
    useEffect(() => {
        onEvent((event) => {
            if (event.type === 'top - nav - click') {
                console.log('收到顶部导航点击事件,数据:', event.data);
            }
        });
    }, []);
    return (
        <div>
            底部组件
        </div>
    );
};

const App = () => {
    return (
        <GlobalEventProvider>
            <TopNavigation />
            <BottomComponent />
        </GlobalEventProvider>
    );
};

在上述代码中,GlobalEventContext 上下文和 useGlobalEvent 自定义 Hooks 实现了全局事件的发布和订阅功能。TopNavigation 组件可以通过 emitEvent 发布事件,BottomComponent 组件通过 onEvent 订阅事件并进行相应的处理,优化了跨组件通信。

自定义 Hooks 的最佳实践

  1. 保持单一职责:每个自定义 Hooks 应该只负责一个特定的功能。例如,useWindowWidth 只负责获取窗口宽度,useDebounce 只负责实现防抖功能。这样可以使得自定义 Hooks 更容易理解、维护和测试。如果一个自定义 Hooks 试图实现多个不相关的功能,会导致代码逻辑混乱,难以复用和调试。
  2. 遵循命名约定:自定义 Hooks 的命名应该以 use 开头,这样可以让其他开发者一眼就能识别出这是一个自定义 Hooks。同时,命名应该能够准确反映该 Hooks 的功能,例如 useAsyncData 就清晰地表明了这个 Hooks 是用于异步数据获取的。
  3. 避免在循环、条件或嵌套函数中调用 Hooks:React 依赖于 Hooks 的调用顺序来正确地管理状态和副作用。如果在循环、条件或嵌套函数中调用 Hooks,可能会导致 Hooks 的调用顺序不一致,从而引发难以调试的错误。Hooks 应该始终在组件的顶层或者其他自定义 Hooks 的顶层调用。
  4. 提供清晰的 API:自定义 Hooks 的 API 应该简单明了,易于使用。例如,useFilterState 提供了 priceRangebrands 等状态以及 updatePriceRangetoggleBrand 等操作方法,使用者可以很容易地理解如何使用这个 Hooks 来管理筛选条件。
  5. 测试自定义 Hooks:如前文所述,应该对自定义 Hooks 进行单元测试,确保其功能的正确性。使用像 Jest 和 @testing-library/react - hooks 这样的工具可以方便地对自定义 Hooks 进行测试。

自定义 Hooks 的性能考虑

  1. 不必要的重新渲染:在自定义 Hooks 中,如果使用了 useStateuseEffect,要注意依赖数组的设置,避免不必要的重新渲染。例如,在 useEffect 中,如果依赖数组设置不当,可能会导致每次组件渲染时都触发副作用操作。
import { useState, useEffect } from'react';

const useMyHook = () => {
    const [count, setCount] = useState(0);
    // 错误的依赖数组设置,每次count变化都会触发这个副作用
    useEffect(() => {
        console.log('副作用操作');
    }, []);
    return { count, setCount };
};

const MyComponent = () => {
    const { count, setCount } = useMyHook();
    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>增加</button>
        </div>
    );
};

在上述代码中,useEffect 的依赖数组为空,这意味着这个副作用操作只会在组件挂载和卸载时执行。如果将依赖数组改为 [count],则每次 count 变化时都会执行副作用操作。要根据实际需求正确设置依赖数组,以避免不必要的性能开销。

  1. 内存泄漏:在自定义 Hooks 中,如果使用了一些需要清理的资源,如事件监听器、定时器等,要确保在组件卸载时进行清理,以避免内存泄漏。例如,在 useWindowWidth 自定义 Hooks 中,通过 useEffect 的返回函数来移除窗口 resize 事件的监听器,就是为了防止内存泄漏。
import { useState, useEffect } from'react';

const useBadWindowWidth = () => {
    const [width, setWidth] = useState(window.innerWidth);
    useEffect(() => {
        const handleResize = () => {
            setWidth(window.innerWidth);
        };
        window.addEventListener('resize', handleResize);
        // 没有返回清理函数,可能导致内存泄漏
    }, []);
    return width;
};

在上述 useBadWindowWidth 代码中,没有在 useEffect 中返回清理函数来移除 resize 事件监听器。如果组件多次挂载和卸载,可能会导致多个事件监听器同时存在,占用内存,引发内存泄漏问题。

自定义 Hooks 的未来发展与趋势

随着 React 的不断发展,自定义 Hooks 的应用场景可能会更加广泛。未来,可能会出现更多针对特定领域的自定义 Hooks 库,例如用于机器学习模型集成的 Hooks、用于增强现实(AR)/虚拟现实(VR)开发的 Hooks 等。这将进一步提升 React 在不同领域的开发效率。

同时,随着 React 对并发模式的支持不断完善,自定义 Hooks 也需要适应这种新的模式。在并发模式下,组件的渲染可能会被暂停、恢复或丢弃,自定义 Hooks 中的副作用操作需要能够正确处理这些情况,以确保应用的稳定性和性能。

此外,随着前端开发对可访问性(Accessibility)的重视程度不断提高,未来可能会出现更多用于提升可访问性的自定义 Hooks,例如帮助开发者更好地管理焦点状态、处理键盘导航等功能的 Hooks。

总的来说,自定义 Hooks 作为 React 开发中的重要工具,将在未来的前端开发中发挥更加关键的作用,推动 React 应用的不断创新和发展。开发者需要不断学习和掌握自定义 Hooks 的最新知识和技巧,以适应前端技术的快速变化。