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

React Hooks 入门与基础概念

2023-03-066.5k 阅读

React Hooks 简介

React Hooks 是 React 16.8 引入的新特性,它允许在不编写类的情况下使用 state 以及其他 React 特性。在此之前,在函数组件中使用 state 和生命周期方法是不可能的,开发者必须使用类组件来实现这些功能。Hooks 改变了这一现状,让函数组件变得更加强大。

为什么需要 React Hooks

  1. 逻辑复用困难:在类组件中,当需要在多个组件间复用状态逻辑时,通常会使用高阶组件(HOC)或渲染属性(render props)模式。然而,这两种模式都会导致组件层级嵌套加深,代码变得难以理解和维护。例如,在使用 HOC 时:
import React from 'react';

// 高阶组件
const withData = (WrappedComponent) => {
    return (props) => {
        // 模拟数据获取逻辑
        const data = { message: 'Hello from HOC' };
        return <WrappedComponent data={data} {...props} />;
    };
};

const MyComponent = ({ data }) => {
    return <div>{data.message}</div>;
};

export default withData(MyComponent);

这里 withData 高阶组件包裹了 MyComponent,虽然实现了数据注入,但增加了组件的嵌套层级。

  1. 类组件的问题:类组件存在一些问题,比如 this 指向问题,这使得代码容易出错且难以调试。同时,类组件的生命周期方法在大型组件中会导致代码碎片化,不同的生命周期方法中可能处理不同的逻辑,但这些逻辑可能是相关的。例如:
import React, { Component } from'react';

class MyClassComponent extends Component {
    constructor(props) {
        super(props);
        this.state = { count: 0 };
        this.handleClick = this.handleClick.bind(this);
    }

    handleClick() {
        this.setState((prevState) => ({ count: prevState.count + 1 }));
    }

    componentDidMount() {
        // 模拟数据获取
        console.log('Component mounted');
    }

    componentWillUnmount() {
        // 清理操作
        console.log('Component will unmount');
    }

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

export default MyClassComponent;

这里 this 的绑定以及生命周期方法的分散使用,使得代码在维护和理解上存在一定难度。

useState Hook

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

基本使用

useState 接收一个初始 state 值作为参数,并返回一个数组。数组的第一个元素是当前的 state 值,第二个元素是一个函数,用于更新这个 state。例如:

import React, { useState } from'react';

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

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

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

export default Counter;

在上述代码中,useState(0) 初始化了 count 为 0,setCount 函数用于更新 count 的值。当点击按钮时,increment 函数调用 setCount(count + 1),从而更新 count 并触发组件重新渲染。

多个 useState

一个组件中可以使用多个 useState 来管理不同的 state。例如:

import React, { useState } from'react';

const Form = () => {
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');

    const handleNameChange = (e) => {
        setName(e.target.value);
    };

    const handleEmailChange = (e) => {
        setEmail(e.target.value);
    };

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log(`Name: ${name}, Email: ${email}`);
    };

    return (
        <form onSubmit={handleSubmit}>
            <label>
                Name:
                <input type="text" value={name} onChange={handleNameChange} />
            </label>
            <br />
            <label>
                Email:
                <input type="email" value={email} onChange={handleEmailChange} />
            </label>
            <br />
            <button type="submit">Submit</button>
        </form>
    );
};

export default Form;

这里使用两个 useState 分别管理 nameemail 的 state,使得代码更加清晰和易于维护。

函数式更新

当更新 state 依赖于之前的 state 值时,应该使用函数式更新。例如:

import React, { useState } from'react';

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

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

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

export default Counter;

setCount((prevCount) => prevCount + 1) 中,prevCount 是之前的 count 值,通过这种方式可以确保在更新 state 时不会丢失之前的状态。

useEffect Hook

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

基本使用

useEffect 接收一个函数作为参数,这个函数会在组件渲染后执行。例如:

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

const DataComponent = () => {
    const [data, setData] = useState(null);

    useEffect(() => {
        // 模拟数据获取
        const fetchData = async () => {
            const response = await fetch('https://example.com/api/data');
            const result = await response.json();
            setData(result);
        };
        fetchData();
    }, []);

    return (
        <div>
            {data? (
                <p>{JSON.stringify(data)}</p>
            ) : (
                <p>Loading...</p>
            )}
        </div>
    );
};

export default DataComponent;

在上述代码中,useEffect 中的函数会在组件挂载后执行,通过 fetch 获取数据并更新 data state。

依赖数组

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

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

const CounterWithEffect = () => {
    const [count, setCount] = useState(0);
    const [message, setMessage] = useState('');

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

    useEffect(() => {
        console.log(`Message has changed to ${message}`);
    }, [message]);

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

    const updateMessage = () => {
        setMessage('New message');
    };

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

export default CounterWithEffect;

这里第一个 useEffect 依赖 count,只有 count 变化时才会执行;第二个 useEffect 依赖 message,只有 message 变化时才会执行。

清理函数

useEffect 中返回的函数会在组件卸载时执行,用于清理副作用,比如取消订阅。例如:

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

const SubscriptionComponent = () => {
    const [subscription, setSubscription] = useState(null);

    useEffect(() => {
        // 模拟订阅操作
        const newSubscription = { id: 1, data: 'Initial data' };
        setSubscription(newSubscription);

        return () => {
            // 模拟取消订阅操作
            console.log('Unsubscribing...');
            setSubscription(null);
        };
    }, []);

    return (
        <div>
            {subscription? (
                <p>Subscribed: {JSON.stringify(subscription)}</p>
            ) : (
                <p>Not subscribed</p>
            )}
        </div>
    );
};

export default SubscriptionComponent;

在上述代码中,useEffect 返回的函数会在组件卸载时执行,输出 Unsubscribing... 并清理 subscription state。

useContext Hook

useContext 用于在组件间共享数据,而无需通过 props 层层传递。

创建 Context

首先需要创建一个 Context 对象。例如:

import React from'react';

const ThemeContext = React.createContext();

export default ThemeContext;

使用 Context

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

const ThemeComponent = () => {
    const theme = useContext(ThemeContext);

    return (
        <div style={{ color: theme === 'dark'? 'white' : 'black' }}>
            This text has a color based on the theme.
        </div>
    );
};

export default ThemeComponent;

提供 Context

在父组件中提供 Context 值。例如:

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

const App = () => {
    const theme = 'dark';

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

export default App;

在上述代码中,ThemeContext.Provider 提供了 theme 值,ThemeComponent 可以通过 useContext 获取这个值,从而避免了通过 props 层层传递。

useReducer Hook

useReduceruseState 的替代方案,适用于 state 逻辑较为复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等情况。

基本使用

useReducer 接收一个 reducer 函数和初始 state。reducer 函数接收当前 state 和一个 action,返回新的 state。例如:

import React, { useReducer } from'react';

const initialState = { count: 0 };

const reducer = (state, action) => {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 };
        case 'decrement':
            return { count: state.count - 1 };
        default:
            return state;
    }
};

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

    const increment = () => {
        dispatch({ type: 'increment' });
    };

    const decrement = () => {
        dispatch({ type: 'decrement' });
    };

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

export default CounterWithReducer;

在上述代码中,reducer 函数根据 action.type 来更新 statedispatch 函数用于触发 action

与 Redux 的比较

虽然 useReducer 和 Redux 都使用 reducer 概念,但 Redux 更适用于大型应用中全局状态管理,它有更复杂的架构,如 store、action creators 等。而 useReducer 主要用于组件内的状态管理,相对轻量级。例如,在 Redux 中:

// actions.js
const increment = () => ({ type: 'increment' });

// reducer.js
const initialState = { count: 0 };

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 };
        default:
            return state;
    }
};

// store.js
import { createStore } from'redux';
import reducer from './reducer';

const store = createStore(reducer);

// component.js
import React from'react';
import { useSelector, useDispatch } from'react-redux';
import { increment } from './actions';

const Counter = () => {
    const count = useSelector((state) => state.count);
    const dispatch = useDispatch();

    const handleIncrement = () => {
        dispatch(increment());
    };

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

export default Counter;

这里 Redux 的代码结构更复杂,涉及多个文件和概念,而 useReducer 则简洁地在组件内实现类似功能。

useMemo Hook

useMemo 用于缓存函数的计算结果,只有当依赖项发生变化时才重新计算。

基本使用

useMemo 接收一个函数和依赖数组作为参数,返回函数的计算结果。例如:

import React, { useMemo } from'react';

const expensiveCalculation = (a, b) => {
    console.log('Performing expensive calculation');
    return a + b;
};

const MemoComponent = () => {
    const a = 5;
    const b = 3;
    const result = useMemo(() => expensiveCalculation(a, b), [a, b]);

    return (
        <div>
            <p>Result: {result}</p>
        </div>
    );
};

export default MemoComponent;

在上述代码中,useMemo 缓存了 expensiveCalculation(a, b) 的结果,只有当 ab 变化时才会重新计算并打印 Performing expensive calculation

性能优化

在一些复杂计算的场景下,useMemo 可以显著提升性能。比如在一个表格组件中,计算表格的汇总数据可能是一个昂贵的操作:

import React, { useMemo } from'react';

const calculateTotal = (items) => {
    return items.reduce((total, item) => total + item.price, 0);
};

const Table = () => {
    const items = [
        { id: 1, price: 10 },
        { id: 2, price: 20 },
        { id: 3, price: 30 }
    ];
    const total = useMemo(() => calculateTotal(items), [items]);

    return (
        <div>
            <table>
                <thead>
                    <tr>
                        <th>ID</th>
                        <th>Price</th>
                    </tr>
                </thead>
                <tbody>
                    {items.map((item) => (
                        <tr key={item.id}>
                            <td>{item.id}</td>
                            <td>{item.price}</td>
                        </tr>
                    ))}
                </tbody>
            </table>
            <p>Total: {total}</p>
        </div>
    );
};

export default Table;

这里 calculateTotal 函数的计算结果被 useMemo 缓存,只有 items 数组变化时才会重新计算,避免了不必要的计算。

useCallback Hook

useCallback 用于缓存函数,返回一个 memoized 回调函数。只有当依赖项发生变化时,才会返回新的函数。

基本使用

useCallback 接收一个回调函数和依赖数组作为参数。例如:

import React, { useCallback } from'react';

const MyComponent = () => {
    const handleClick = useCallback(() => {
        console.log('Button clicked');
    }, []);

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

export default MyComponent;

在上述代码中,handleClick 函数被 useCallback 缓存,由于依赖数组为空,这个函数在组件的整个生命周期内都不会改变。

防止不必要的重新渲染

在父组件向子组件传递函数作为 props 时,如果不使用 useCallback,父组件每次渲染都会生成一个新的函数,这可能导致子组件不必要的重新渲染。例如:

import React, { useState } from'react';

const ChildComponent = ({ onClick }) => {
    console.log('ChildComponent rendered');
    return <button onClick={onClick}>Click me</button>;
};

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

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

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

export default ParentComponent;

在上述代码中,每次 ParentComponent 渲染(比如 count 变化时),handleClick 都是一个新函数,导致 ChildComponent 不必要的重新渲染。使用 useCallback 可以解决这个问题:

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

const ChildComponent = ({ onClick }) => {
    console.log('ChildComponent rendered');
    return <button onClick={onClick}>Click me</button>;
};

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

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

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

export default ParentComponent;

此时,只有 count 变化时,handleClick 才会更新,避免了 ChildComponent 不必要的重新渲染。

自定义 Hooks

React 允许开发者创建自定义 Hooks,以复用状态逻辑。

创建自定义 Hook

例如,创建一个用于获取窗口尺寸的自定义 Hook:

import { useState, useEffect } from'react';

const useWindowSize = () => {
    const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });

    useEffect(() => {
        const handleResize = () => {
            setSize({ width: window.innerWidth, height: window.innerHeight });
        };
        window.addEventListener('resize', handleResize);
        return () => {
            window.removeEventListener('resize', handleResize);
        };
    }, []);

    return size;
};

export default useWindowSize;

使用自定义 Hook

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

const WindowSizeComponent = () => {
    const size = useWindowSize();

    return (
        <div>
            <p>Window width: {size.width}</p>
            <p>Window height: {size.height}</p>
        </div>
    );
};

export default WindowSizeComponent;

在上述代码中,useWindowSize 自定义 Hook 封装了获取窗口尺寸的逻辑,WindowSizeComponent 可以直接使用这个 Hook 来获取窗口尺寸,实现了逻辑复用。

React Hooks 的规则

  1. 只能在函数最外层调用 Hook:不能在循环、条件语句或嵌套函数中调用 Hook。例如:
import React, { useState } from'react';

const MyComponent = () => {
    let shouldUseState = true;
    // 错误使用
    if (shouldUseState) {
        const [count, setCount] = useState(0);
    }
    return <div>Component</div>;
};

export default MyComponent;

这里在 if 语句中调用 useState 是错误的,应该将 useState 放在函数最外层。

  1. 只能在 React 函数组件或自定义 Hook 中调用 Hook:不能在普通 JavaScript 函数中调用 Hook。例如:
import React, { useState } from'react';

const regularFunction = () => {
    // 错误使用
    const [count, setCount] = useState(0);
    return count;
};

const MyComponent = () => {
    return <div>Component</div>;
};

export default MyComponent;

这里在普通函数 regularFunction 中调用 useState 是错误的。

遵循这些规则可以确保 React 能够正确地追踪 Hook 的调用顺序,从而保证组件状态和副作用的一致性。

通过以上对 React Hooks 的详细介绍,相信你对 React Hooks 的基础概念和使用方法有了较为深入的理解。在实际开发中,合理运用 Hooks 可以让代码更加简洁、可维护,提升开发效率和应用性能。