React Hooks实战:告别Class组件的现代开发

React Hooks 实战教程封面图 - 告别 Class 组件的现代前端开发指南

前言

我是从React 15开始学的,那时候全是Class组件。写一个简单的计数器要这样:

jsx

class Counter extends React.Component {
    constructor(props) {
        super(props);
        this.state = { count: 0 };
        this.handleClick = this.handleClick.bind(this);
    }
    
    handleClick() {
        this.setState({ count: this.state.count + 1 });
    }
    
    componentDidMount() {
        console.log('组件挂载了');
    }
    
    componentDidUpdate() {
        console.log('组件更新了');
    }
    
    componentWillUnmount() {
        console.log('组件要卸载了');
    }
    
    render() {
        return (
            <button onClick={this.handleClick}>
                点击次数: {this.state.count}
            </button>
        );
    }
}

第一次写的时候我都懵了——怎么这么多重复代码?this是什么东西?为什么点击事件要.bind(this)?生命周期方法怎么这么乱?

后来React 16.8出了Hooks,一切都变了。同样的计数器,用Hooks写:

jsx

function Counter() {
    const [count, setCount] = useState(0);
    
    return (
        <button onClick={() => setCount(count + 1)}>
            点击次数: {count}
        </button>
    );
}

就这几行,代码少了三分之二,逻辑清晰多了。这就是Hooks的魅力——让React组件变得更简单、更容易理解和维护。

为什么需要Hooks?

Class组件的痛点

在说Hooks之前,先聊聊为什么Facebook要发明Hooks:

1. 逻辑复用困难

Class组件复用逻辑只有两种方式:HOC(高阶组件)和Render Props。这两种方式都会让代码嵌套很深,调试困难。

jsx

// HOC示例
const withUser = (Component) => {
    return class WithUser extends React.Component {
        constructor(props) {
            super(props);
            this.state = { user: null };
        }
        
        componentDidMount() {
            fetchUser().then(user => this.setState({ user }));
        }
        
        render() {
            return <Component {...this.props} user={this.state.user} />;
        }
    };
};

// 使用高阶组件
const UserPage = withUser(ProfilePage);

// 问题:层层嵌套
const EnhancedComponent = withLogging(withTheme(withUser(OriginalComponent)));

2. 生命周期逻辑混乱

相关逻辑被分散到不同生命周期里,比如数据获取,可能在componentDidMount里发请求,在componentDidUpdate里处理更新,在componentWillUnmount里清理。但相关的逻辑其实应该放在一起。

jsx

class UserProfile extends React.Component {
    componentDidMount() {
        this.fetchUser();
        this.startPolling();
        this.setupEventListeners();
        this.updateDocumentTitle();
    }
    
    componentDidUpdate(prevProps) {
        if (prevProps.userId !== this.props.userId) {
            this.fetchUser();
            this.updateDocumentTitle();
        }
    }
    
    componentWillUnmount() {
        this.stopPolling();
        this.removeEventListeners();
    }
    
    // 问题:相关的逻辑分散在不同地方,很难维护
}

3. Class的this问题

jsx

class MyComponent extends React.Component {
    handleClick() {
        // 这里的this是undefined!必须bind或者用箭头函数
        this.setState({ clicked: true });
    }
    
    render() {
        return (
            <div>
                {/* 方法1:bind */}
                <button onClick={this.handleClick.bind(this)}>点击1</button>
                
                {/* 方法2:构造函数里bind */}
                <button onClick={this.handleClick}>点击2</button>
                
                {/* 方法3:箭头函数 */}
                <button onClick={() => this.handleClick()}>点击3</button>
            </div>
        );
    }
}

Hooks带来的改变

Hooks是React 16.8引入的新特性,它让你在不写Class的情况下使用state和其他React特性:

  • 逻辑复用更简单:自定义Hook让逻辑复用变得直观
  • 代码更简洁:不用写Class,少打很多字
  • 更容易理解:相关的逻辑放在一起
  • 告别this:函数组件没有this问题
  • 更容易测试:Hook是普通函数,可以单独测试

核心Hooks详解

useState:状态管理

useState是最基础的Hook,用来在函数组件中添加状态。

基础用法

jsx

import { useState } from 'react';

function Counter() {
    // count是状态值,setCount是用来更新状态的函数
    const [count, setCount] = useState(0);
    
    return (
        <div>
            <p>计数: {count}</p>
            <button onClick={() => setCount(count + 1)}>增加</button>
            <button onClick={() => setCount(count - 1)}>减少</button>
            <button onClick={() => setCount(0)}>重置</button>
        </div>
    );
}

函数式更新

当新的状态依赖于旧状态时,用函数式更新更安全:

jsx

// 普通方式:快速点击时可能出问题
setCount(count + 1);

// 函数式更新(推荐,尤其在快速点击时)
setCount(prevCount => prevCount + 1);

// 更复杂的情况
setCount(prev => {
    if (prev >= 10) return 0;
    return prev + 1;
});

对象类型状态

jsx

// 对象状态需要整体更新
const [user, setUser] = useState({ name: '', age: 0, email: '' });

// 正确:用展开运算符保留其他属性
setUser(prev => ({ ...prev, name: '小明' }));
setUser(prev => ({ ...prev, age: prev.age + 1 }));

// 错误:会丢失其他属性
setUser({ name: '小明' }); // age和email都没了!

数组类型状态

jsx

const [items, setItems] = useState([]);

// 添加元素
setItems(prev => [...prev, newItem]);

// 删除元素
setItems(prev => prev.filter(item => item.id !== id));

// 更新元素
setItems(prev => prev.map(item => 
    item.id === id 
        ? { ...item, ...updates }  // 合并更新
        : item
));

// 清空数组
setItems([]);

多状态管理

jsx

function UserForm() {
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [isSubmitting, setIsSubmitting] = useState(false);
    const [errors, setErrors] = useState({});
    
    // ...
}

useEffect:副作用处理

useEffect用来处理副作用,比如数据获取、订阅、手动修改DOM、定时器等。

基础用法

jsx

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    
    useEffect(() => {
        // 这个函数会在组件挂载后执行
        console.log('副作用执行了');
        
        // 可选:返回一个清理函数
        return () => {
            console.log('清理副作用');
        };
    }, []); // 空依赖数组表示只在挂载时执行
    
    return <div>...</div>;
}

数据获取实战

jsx

function UserProfile({ userId }) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    
    useEffect(() => {
        // 创建AbortController用于取消请求
        const controller = new AbortController();
        
        async function fetchUser() {
            setLoading(true);
            setError(null);
            
            try {
                const response = await fetch(`/api/users/${userId}`, {
                    signal: controller.signal
                });
                
                if (!response.ok) {
                    throw new Error('获取用户信息失败');
                }
                
                const data = await response.json();
                setUser(data);
            } catch (err) {
                // 忽略取消请求的错误
                if (err.name !== 'AbortError') {
                    setError(err.message);
                }
            } finally {
                // 只有请求没被取消时才更新loading
                if (!controller.signal.aborted) {
                    setLoading(false);
                }
            }
        }
        
        fetchUser();
        
        // 清理:组件卸载或userId变化时取消请求
        return () => controller.abort();
    }, [userId]); // userId变化时重新获取
    
    if (loading) return <div>加载中...</div>;
    if (error) return <div>出错了: {error}</div>;
    if (!user) return <div>用户不存在</div>;
    
    return (
        <div>
            <h1>{user.name}</h1>
            <p>{user.email}</p>
        </div>
    );
}

依赖数组的三种用法

jsx

useEffect(() => {
    // 1. 不传依赖数组:每次渲染都执行(容易造成无限循环)
    // 慎用!通常需要配合useRef
});

useEffect(() => {
    // 2. 空数组:只在挂载时执行一次(常用于初始化)
    // 类似componentDidMount
}, []);

useEffect(() => {
    // 3. 指定依赖:依赖变化时执行(最常用)
    // 类似componentDidUpdate
}, [dependency1, dependency2]);

清理副作用

jsx

function Timer() {
    const [seconds, setSeconds] = useState(0);
    
    useEffect(() => {
        const interval = setInterval(() => {
            setSeconds(s => s + 1);
        }, 1000);
        
        // 清理:组件卸载时清除定时器
        return () => {
            clearInterval(interval);
        };
    }, []);
    
    return <div>已过去 {seconds} 秒</div>;
}

function WindowSize() {
    const [width, setWidth] = useState(window.innerWidth);
    
    useEffect(() => {
        function handleResize() {
            setWidth(window.innerWidth);
        }
        
        window.addEventListener('resize', handleResize);
        
        // 清理:移除事件监听
        return () => {
            window.removeEventListener('resize', handleResize);
        };
    }, []);
    
    return <div>窗口宽度: {width}</div>;
}

useRef:操作DOM和保存可变值

useRef有两个主要用途:操作DOM和保存不会触发渲染的可变值。

操作DOM

jsx

import { useRef, useEffect } from 'react';

function AutoFocusInput() {
    const inputRef = useRef(null);
    
    useEffect(() => {
        // 自动聚焦到输入框
        inputRef.current.focus();
    }, []);
    
    return (
        <input 
            ref={inputRef} 
            type="text" 
            placeholder="自动聚焦在这里"
        />
    );
}

function ScrollToTop() {
    const topRef = useRef(null);
    
    const scrollToTop = () => {
        topRef.current?.scrollIntoView({ behavior: 'smooth' });
    };
    
    return (
        <div>
            <div ref={topRef}>页面顶部</div>
            <button onClick={scrollToTop}>回到顶部</button>
        </div>
    );
}

保存上一次的props或state

jsx

function usePrevious(value) {
    const ref = useRef();
    
    useEffect(() => {
        // 每次value变化时,更新ref
        ref.current = value;
    });
    
    // 返回的是上一次的值
    return ref.current;
}

function Counter() {
    const [count, setCount] = useState(0);
    const previousCount = usePrevious(count);
    
    return (
        <div>
            <p>现在: {count}, 上次: {previousCount}</p>
            <button onClick={() => setCount(c => c + 1)}>增加</button>
        </div>
    );
}

保存定时器ID和其他可变值

jsx

function Timer() {
    const [seconds, setSeconds] = useState(0);
    const intervalRef = useRef(null);
    
    const startTimer = () => {
        if (intervalRef.current) return;
        
        intervalRef.current = setInterval(() => {
            setSeconds(s => s + 1);
        }, 1000);
    };
    
    const stopTimer = () => {
        if (intervalRef.current) {
            clearInterval(intervalRef.current);
            intervalRef.current = null;
        }
    };
    
    return (
        <div>
            <p>已过去 {seconds} 秒</p>
            <button onClick={startTimer}>开始</button>
            <button onClick={stopTimer}>停止</button>
        </div>
    );
}

useMemo和useCallback:性能优化

这两个Hook用来避免不必要的计算和渲染。

useMemo:缓存计算结果

jsx

import { useMemo } from 'react';

function ExpensiveList({ items, filter, sortBy }) {
    // 只有items、filter或sortBy变化时才重新计算
    const filteredAndSorted = useMemo(() => {
        console.log('开始过滤和排序...');
        
        return items
            .filter(item => item.name.includes(filter))
            .sort((a, b) => {
                if (sortBy === 'name') {
                    return a.name.localeCompare(b.name);
                }
                return a[sortBy] - b[sortBy];
            });
    }, [items, filter, sortBy]);
    
    return (
        <ul>
            {filteredAndSorted.map(item => (
                <li key={item.id}>{item.name}</li>
            ))}
        </ul>
    );
}

// 另一个例子:缓存计算结果
function App() {
    const [count, setCount] = useState(0);
    const [multiplier, setMultiplier] = useState(1);
    
    // expensiveCalculation是耗时计算
    const result = useMemo(() => {
        return expensiveCalculation(count);
    }, [count]);
    
    return (
        <div>
            <p>{count} * {multiplier} = {result}</p>
            <button onClick={() => setCount(c => c + 1)}>增加计数</button>
            <button onClick={() => setMultiplier(m => m + 1)}>增加倍数</button>
        </div>
    );
}

useCallback:缓存函数

jsx

import { useCallback } from 'react';

function Parent() {
    const [count, setCount] = useState(0);
    const [name, setName] = useState('');
    
    // 用useCallback缓存函数
    const handleClick = useCallback((id) => {
        console.log('点击了', id);
        setCount(c => c + 1);
    }, []); // 空依赖,函数永远不变
    
    const handleSubmit = useCallback((data) => {
        console.log('提交数据:', data);
        setName(data.name);
    }, []); // 空依赖
    
    return (
        <div>
            <Child onClick={handleClick} />
            <Form onSubmit={handleSubmit} />
            <p>点击次数: {count}</p>
        </div>
    );
}

// 子组件使用React.memo避免不必要的重渲染
const Child = React.memo(function Child({ onClick }) {
    console.log('Child渲染了');
    return <button onClick={() => onClick(1)}>点击</button>;
});

注意:不要过度使用useMemo和useCallback,只有在确实有性能问题时才用。

自定义Hooks:逻辑复用

自定义Hook是React Hooks最强大的特性——它让你可以把组件逻辑提取成可复用的函数。

基础自定义Hook

jsx

// 自定义localStorage Hook
function useLocalStorage(key, initialValue) {
    // 从localStorage读取初始值
    const [storedValue, setStoredValue] = useState(() => {
        try {
            const item = window.localStorage.getItem(key);
            return item ? JSON.parse(item) : initialValue;
        } catch (error) {
            console.error(error);
            return initialValue;
        }
    });
    
    // 更新localStorage
    const setValue = (value) => {
        try {
            // 支持函数更新
            const valueToStore = value instanceof Function 
                ? value(storedValue) 
                : value;
            
            setStoredValue(valueToStore);
            window.localStorage.setItem(key, JSON.stringify(valueToStore));
        } catch (error) {
            console.error(error);
        }
    };
    
    return [storedValue, setValue];
}

// 使用
function App() {
    const [name, setName] = useLocalStorage('name', '');
    const [theme, setTheme] = useLocalStorage('theme', 'light');
    
    return (
        <div>
            <input 
                value={name} 
                onChange={e => setName(e.target.value)} 
            />
            <select value={theme} onChange={e => setTheme(e.target.value)}>
                <option value="light">浅色</option>
                <option value="dark">深色</option>
            </select>
        </div>
    );
}

监听窗口大小

jsx

function useWindowSize() {
    const [size, setSize] = useState({
        width: window.innerWidth,
        height: window.innerHeight
    });
    
    useEffect(() => {
        function handleResize() {
            setSize({
                width: window.innerWidth,
                height: window.innerHeight
            });
        }
        
        window.addEventListener('resize', handleResize);
        handleResize(); // 初始化
        
        return () => window.removeEventListener('resize', handleResize);
    }, []);
    
    return size;
}

function ResponsiveComponent() {
    const { width, height } = useWindowSize();
    
    return (
        <div>
            <p>窗口大小: {width} x {height}</p>
            {width < 768 && <MobileMenu />}
            {width >= 768 && <DesktopMenu />}
        </div>
    );
}

异步数据请求

jsx

function useAsync(asyncFunction, immediate = true) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(immediate);
    const [error, setError] = useState(null);
    
    const execute = useCallback(async (...args) => {
        setLoading(true);
        setError(null);
        
        try {
            const response = await asyncFunction(...args);
            setData(response);
        } catch (err) {
            setError(err);
        } finally {
            setLoading(false);
        }
    }, [asyncFunction]);
    
    useEffect(() => {
        if (immediate) {
            execute();
        }
    }, [execute, immediate]);
    
    return { data, loading, error, execute };
}

// 使用
function UserList() {
    const { data: users, loading, error, execute: refetch } = useAsync(
        async () => {
            const response = await fetch('/api/users');
            if (!response.ok) throw new Error('获取失败');
            return response.json();
        }
    );
    
    if (loading) return <div>加载中...</div>;
    if (error) return <div>错误: {error.message}</div>;
    
    return (
        <div>
            <button onClick={refetch}>刷新</button>
            <ul>
                {users?.map(user => (
                    <li key={user.id}>{user.name}</li>
                ))}
            </ul>
        </div>
    );
}

防抖Hook

jsx

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

function SearchBox() {
    const [searchTerm, setSearchTerm] = useState('');
    const [results, setResults] = useState([]);
    const [isSearching, setIsSearching] = useState(false);
    
    // 防抖500毫秒
    const debouncedSearch = useDebounce(searchTerm, 500);
    
    useEffect(() => {
        if (!debouncedSearch) {
            setResults([]);
            return;
        }
        
        setIsSearching(true);
        
        // 执行搜索
        fetch(`/api/search?q=${debouncedSearch}`)
            .then(res => res.json())
            .then(data => setResults(data))
            .finally(() => setIsSearching(false));
            
    }, [debouncedSearch]);
    
    return (
        <div>
            <input
                value={searchTerm}
                onChange={e => setSearchTerm(e.target.value)}
                placeholder="输入搜索内容..."
            />
            {isSearching && <p>搜索中...</p>}
            {results.map(r => (
                <div key={r.id}>{r.title}</div>
            ))}
        </div>
    );
}

表单处理Hook

jsx

function useForm(initialValues) {
    const [values, setValues] = useState(initialValues);
    const [errors, setErrors] = useState({});
    const [touched, setTouched] = useState({});
    const [isSubmitting, setIsSubmitting] = useState(false);
    
    const handleChange = (e) => {
        const { name, value } = e.target;
        setValues(prev => ({ ...prev, [name]: value }));
    };
    
    const handleBlur = (e) => {
        const { name } = e.target;
        setTouched(prev => ({ ...prev, [name]: true }));
    };
    
    const reset = () => {
        setValues(initialValues);
        setErrors({});
        setTouched({});
        setIsSubmitting(false);
    };
    
    const validate = (validator) => {
        const newErrors = validator(values);
        setErrors(newErrors);
        return Object.keys(newErrors).length === 0;
    };
    
    return {
        values,
        errors,
        touched,
        isSubmitting,
        setIsSubmitting,
        handleChange,
        handleBlur,
        reset,
        validate
    };
}

// 使用
function ContactForm({ onSubmit }) {
    const {
        values,
        errors,
        touched,
        isSubmitting,
        handleChange,
        handleBlur,
        reset,
        validate
    } = useForm({ 
        name: '', 
        email: '', 
        message: '' 
    });
    
    const handleSubmit = async (e) => {
        e.preventDefault();
        
        // 标记所有字段已被访问
        setTouched({ name: true, email: true, message: true });
        
        // 验证
        if (!validate(v => {
            const errors = {};
            if (!v.name) errors.name = '姓名不能为空';
            if (!v.email.includes('@')) errors.email = '邮箱格式不正确';
            if (v.message.length < 10) errors.message = '留言至少10个字符';
            return errors;
        })) return;
        
        setIsSubmitting(true);
        await onSubmit(values);
        setIsSubmitting(false);
        reset();
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <div>
                <input
                    name="name"
                    value={values.name}
                    onChange={handleChange}
                    onBlur={handleBlur}
                    placeholder="姓名"
                />
                {touched.name && errors.name && <span>{errors.name}</span>}
            </div>
            
            <div>
                <input
                    name="email"
                    value={values.email}
                    onChange={handleChange}
                    onBlur={handleBlur}
                    placeholder="邮箱"
                />
                {touched.email && errors.email && <span>{errors.email}</span>}
            </div>
            
            <div>
                <textarea
                    name="message"
                    value={values.message}
                    onChange={handleChange}
                    onBlur={handleBlur}
                    placeholder="留言"
                />
                {touched.message && errors.message && <span>{errors.message}</span>}
            </div>
            
            <button type="submit" disabled={isSubmitting}>
                {isSubmitting ? '提交中...' : '提交'}
            </button>
        </form>
    );
}

Hooks使用规则

React对Hook的调用有两条强制的规则:

规则1:只在顶层调用Hooks

不要在循环、条件语句或嵌套函数中调用Hook:

jsx

// 错误 ❌ - 在条件语句中调用Hook
function Component({ isLoggedIn }) {
    if (isLoggedIn) {
        const [name, setName] = useState(''); // 错误!
    }
    
    const [count, setCount] = useState(0);
}

// 正确 ✅ - 所有Hook在顶层调用
function Component({ isLoggedIn }) {
    const [name, setName] = useState('');
    const [count, setCount] = useState(0);
    
    // 在条件块内部使用状态
    if (isLoggedIn) {
        console.log(name);
    }
}

规则2:只在React函数中调用

只能在React函数组件或自定义Hook中调用:

jsx

// 错误 ❌
function ordinaryFunction() {
    const [count, setCount] = useState(0); // 错误!
}

// 正确 ✅
function MyComponent() {
    const [count, setCount] = useState(0);
}

// 正确 ✅ - 自定义Hook
function useCustomHook() {
    const [data, setData] = useState(null);
    // ...
}

从Class组件迁移到Hooks

很多老项目还在用Class组件,以下是常见迁移方案:

生命周期方法迁移

Class组件Hooks版本
constructoruseState初始化
componentDidMountuseEffect(() => {}, [])
componentDidUpdateuseEffect(() => {}, [deps])
componentWillUnmountuseEffect return cleanup
shouldComponentUpdateReact.memo 或 useMemo

完整迁移示例

Class组件

jsx

class UserProfile extends React.Component {
    state = { user: null, loading: true, error: null };
    
    componentDidMount() {
        this.fetchUser();
        document.title = '加载中...';
    }
    
    componentDidUpdate(prevProps) {
        if (prevProps.userId !== this.props.userId) {
            this.fetchUser();
        }
        document.title = this.state.user?.name || '加载中';
    }
    
    componentWillUnmount() {
        this.cancelFetch();
    }
    
    async fetchUser() {
        try {
            this.setState({ loading: true, error: null });
            const user = await api.getUser(this.props.userId);
            this.setState({ user, loading: false });
        } catch (error) {
            this.setState({ error: error.message, loading: false });
        }
    }
    
    render() {
        const { user, loading, error } = this.state;
        
        if (loading) return <div>加载中...</div>;
        if (error) return <div>出错了: {error}</div>;
        
        return <div>{user.name}</div>;
    }
}

Hooks版本

jsx

function UserProfile({ userId }) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    
    useEffect(() => {
        let cancelled = false;
        
        async function fetchUser() {
            try {
                setLoading(true);
                setError(null);
                const user = await api.getUser(userId);
                if (!cancelled) {
                    setUser(user);
                }
            } catch (error) {
                if (!cancelled) {
                    setError(error.message);
                }
            } finally {
                if (!cancelled) {
                    setLoading(false);
                }
            }
        }
        
        fetchUser();
        
        return () => {
            cancelled = true;
        };
    }, [userId]);
    
    useEffect(() => {
        document.title = user?.name || '加载中';
    }, [user]);
    
    if (loading) return <div>加载中...</div>;
    if (error) return <div>出错了: {error}</div>;
    
    return <div>{user.name}</div>;
}

常见问题和解决方案

1. 闭包陷阱

jsx

// 问题:定时器会一直输出0
function Counter() {
    const [count, setCount] = useState(0);
    
    useEffect(() => {
        const timer = setInterval(() => {
            console.log(count); // 永远是0(闭包问题)
            setCount(count + 1); // 永远执行 setCount(0 + 1)
        }, 1000);
        
        return () => clearInterval(timer);
    }, []); // 空依赖,只执行一次
    
    return <div>{count}</div>;
}

// 解决方案1:使用函数式更新
useEffect(() => {
    const timer = setInterval(() => {
        setCount(c => c + 1); // 用函数获取最新值
    }, 1000);
    
    return () => clearInterval(timer);
}, []);

// 解决方案2:使用useRef保存最新值
function Counter() {
    const [count, setCount] = useState(0);
    const countRef = useRef(count);
    
    useEffect(() => {
        countRef.current = count;
    }, [count]);
    
    useEffect(() => {
        const timer = setInterval(() => {
            setCount(countRef.current + 1);
        }, 1000);
        
        return () => clearInterval(timer);
    }, []);
    
    return <div>{count}</div>;
}

2. 无限循环

jsx

// 问题:effect里更新状态,状态变化触发effect
useEffect(() => {
    setData(newData); // 触发重新渲染
}, [data]); // data变化又触发effect → 无限循环!

// 解决方案1:检查值是否真的变化了
useEffect(() => {
    if (data !== newData) {
        setData(newData);
    }
}, [newData]); // 只依赖newData

// 解决方案2:使用ref存储中间值
const dataRef = useRef(data);
useEffect(() => {
    if (dataRef.current !== newData) {
        dataRef.current = newData;
        setData(newData);
    }
}, [newData]);

3. 清理函数很重要

jsx

// 问题:订阅不清理会造成内存泄漏
useEffect(() => {
    const subscription = api.subscribe(data => setData(data));
    // 没写清理函数!组件卸载后订阅还在
}, []);

// 正确做法:返回清理函数
useEffect(() => {
    const subscription = api.subscribe(data => setData(data));
    
    return () => {
        subscription.unsubscribe(); // 清理
    };
}, []);

// WebSocket示例
useEffect(() => {
    const ws = new WebSocket('wss://example.com');
    
    ws.onmessage = (event) => {
        setMessages(prev => [...prev, event.data]);
    };
    
    return () => {
        ws.close(); // 关闭连接
    };
}, []);

4. useEffect里的async函数

jsx

// 错误:useEffect不能直接接收async函数
useEffect(async () => {
    const data = await fetchData();
    setData(data);
}, []);

// 正确:在effect内部定义async函数
useEffect(() => {
    async function fetchData() {
        const data = await fetch('/api/data');
        setData(data);
    }
    
    fetchData();
}, []);

// 或者用立即执行函数
useEffect(() => {
    (async () => {
        const data = await fetch('/api/data');
        setData(data);
    })();
}, []);

常用Hooks库推荐

react-use

最流行的React Hooks库,提供了大量实用的Hook:

bash

npm install react-use

jsx

import { useMouse, useDebounce, useLocalStorage } from 'react-use';

function Component() {
    const [mouseX, mouseY] = useMouse();
    const [value, setValue] = useLocalStorage('key', 'default');
    const debouncedValue = useDebounce(value, 300);
    
    return <div>鼠标位置: {mouseX}, {mouseY}</div>;
}

ahooks(阿里出品)

专为React开发的Hooks库,有中文文档:

bash

npm install ahooks

react-hook-form

高性能表单处理Hook:

bash

npm install react-hook-form

jsx

import { useForm } from 'react-hook-form';

function Form() {
    const { register, handleSubmit, formState: { errors } } = useForm();
    
    const onSubmit = (data) => console.log(data);
    
    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            <input {...register("name", { required: true })} />
            {errors.name && <span>必填</span>}
            
            <input {...register("email", { pattern: /@/ })} />
            {errors.email && <span>邮箱格式不对</span>}
            
            <button type="submit">提交</button>
        </form>
    );
}

总结

React Hooks彻底改变了React开发的方式:

特性Class组件Hooks
代码量
逻辑复用HOC/Render Props自定义Hook
this问题需要处理没有
状态逻辑分散集中
测试需要渲染组件可单独测试Hook
学习曲线陡(生命周期复杂)缓(概念简单)

建议的学习路径:

  1. 熟练使用useState和useEffect
  2. 理解useRef的两种用法
  3. 学习useMemo和useCallback做性能优化
  4. 尝试编写自定义Hook
  5. 在项目中实践,逐渐迁移旧代码

Hooks不是要完全替代Class,而是给了我们更多选择。对于简单的组件用Hooks更方便,对于复杂的、有生命周期特殊需求的组件,Class仍然有用武之地。

相关推荐

Hooks,让React开发更简单!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注