返回首页

说说对 React Hooks 的理解?解决了什么问题?

问题解析

面试官考察点:

  • 是否了解 Hooks 的设计动机和解决的问题
  • 是否掌握常用 Hooks 的使用方法
  • 是否理解 Hooks 的工作原理
  • 对函数式编程思想的理解

核心概念

什么是 Hooks

Hooks 是 React 16.8 引入的新特性,它允许你在不编写 class 的情况下使用 state 以及其他的 React 特性。

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

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

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

Hooks 解决的问题

在 Hooks 出现之前,React 存在以下问题:

  1. 难以复用状态逻辑:高阶组件嵌套地狱,render props 代码冗余
  2. 复杂组件难以理解:生命周期函数中混杂不相关逻辑,相关逻辑分散在不同生命周期
  3. this 指向问题:需要绑定 this,增加学习成本
  4. 函数组件无状态:简单组件也需要写成 class

详细解答

1. useState - 状态管理

import React, { useState } from 'react';

function Counter() {
  // 声明一个叫 "count" 的 state 变量
  const [count, setCount] = useState(0);

  // 使用对象作为 state
  const [user, setUser] = useState({ name: '', age: 0 });

  const updateName = (newName) => {
    // 对象需要展开合并
    setUser({ ...user, name: newName });
  };

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

      <p>User: {user.name}</p>
      <input
        value={user.name}
        onChange={(e) => updateName(e.target.value)}
      />
    </div>
  );
}

2. useEffect - 副作用处理

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

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

  // 相当于 componentDidMount + componentDidUpdate
  useEffect(() => {
    document.title = `Count: ${count}`;
  });

  // 只在 count 变化时执行
  useEffect(() => {
    console.log('Count changed:', count);
  }, [count]);

  // 只在挂载时执行(空依赖数组)
  useEffect(() => {
    console.log('Component mounted');

    // 返回清理函数,相当于 componentWillUnmount
    return () => {
      console.log('Component will unmount');
    };
  }, []);

  // 多个 effect,分离关注点
  useEffect(() => {
    // 订阅操作
    const subscription = subscribeToFriendStatus(id, handleStatusChange);

    return () => {
      // 取消订阅
      subscription.unsubscribe();
    };
  }, [id]);

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

3. useContext - 上下文消费

import React, { useContext } from 'react';

const ThemeContext = React.createContext('light');

function ThemedButton() {
  // 使用 useContext 替代 Consumer
  const theme = useContext(ThemeContext);

  return (
    <button style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
      I am styled by theme context!
    </button>
  );
}

// 对比 class 组件的写法
class ThemedButtonClass extends React.Component {
  static contextType = ThemeContext;

  render() {
    return (
      <button style={{ background: this.context === 'dark' ? '#333' : '#fff' }}>
        I am styled!
      </button>
    );
  }
}

4. useRef - 引用

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

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const timerRef = useRef(null);

  const onButtonClick = () => {
    // 访问 DOM 节点
    inputEl.current.focus();
  };

  useEffect(() => {
    // 保存任意可变值
    timerRef.current = setInterval(() => {
      console.log('Tick');
    }, 1000);

    return () => {
      clearInterval(timerRef.current);
    };
  }, []);

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

5. useReducer - 复杂状态逻辑

import React, { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </div>
  );
}

6. useMemo 和 useCallback - 性能优化

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

function ParentComponent({ data, onUpdate }) {
  // 缓存计算结果
  const processedData = useMemo(() => {
    console.log('Computing...');
    return data.map(item => item * 2);
  }, [data]);

  // 缓存回调函数
  const handleClick = useCallback(() => {
    onUpdate(processedData);
  }, [onUpdate, processedData]);

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

7. 自定义 Hooks

// useLocalStorage.js
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) {
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.log(error);
    }
  };

  return [storedValue, setValue];
}

// 使用
function Example() {
  const [name, setName] = useLocalStorage('name', 'Bob');

  return (
    <input
      type="text"
      value={name}
      onChange={(e) => setName(e.target.value)}
    />
  );
}

深入理解

Hooks 的工作机制

// React 内部使用数组来存储 state
const React = (function() {
  let hooks = [];
  let idx = 0;

  function useState(initialValue) {
    const state = hooks[idx] !== undefined ? hooks[idx] : initialValue;
    hooks[idx] = state;

    const setState = (newValue) => {
      hooks[idx] = newValue;
      render();
    };

    idx++; // 下一个 hook 使用下一个索引
    return [state, setState];
  }

  function render(Component) {
    idx = 0; // 每次渲染重置索引
    const comp = Component();
    comp.render();
    return comp;
  }

  return { useState, render };
})();

Hooks 规则

  1. 只在最顶层使用 Hooks:不要在循环、条件或嵌套函数中调用
  2. 只在 React 函数中调用 Hooks:函数组件或自定义 Hooks
// 错误示例
function BadExample() {
  if (condition) {
    // 在条件中调用 Hook 是错误的
    const [state, setState] = useState(0);
  }

  for (let i = 0; i < 10; i++) {
    // 在循环中调用 Hook 是错误的
    const [state, setState] = useState(0);
  }
}

// 正确示例
function GoodExample() {
  const [state, setState] = useState(0);

  useEffect(() => {
    if (condition) {
      setState(1);
    }
  }, [condition]);
}

最佳实践

  1. 使用 ESLint 插件eslint-plugin-react-hooks 自动检查规则
  2. 合理拆分 Hooks:将相关逻辑封装到自定义 Hooks
  3. 注意依赖数组:确保 effect 的依赖项完整
  4. 避免过度优化:不是所有情况都需要 useMemo/useCallback
// 依赖项完整示例
useEffect(() => {
  fetchData(id);
}, [id]); // 确保包含所有依赖

// 使用函数式更新避免依赖
useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1); // 不依赖外部 count
  }, 1000);
  return () => clearInterval(timer);
}, []); // 空依赖数组是安全的

面试要点

  1. Hooks 解决的问题:复用逻辑、分离关注点、消除 this
  2. 常用 Hooks:useState、useEffect、useContext、useRef、useReducer、useMemo、useCallback
  3. Hooks 规则:只在顶层调用、只在 React 函数中调用
  4. 自定义 Hooks:以 use 开头,封装可复用逻辑
  5. 与 Class 组件对比:更简洁、更符合函数式编程思想

常见追问:

  • 为什么 Hooks 不能在循环或条件中使用?
  • useEffect 的清理函数什么时候执行?
  • useMemo 和 useCallback 的区别是什么?
  • 如何模拟 componentDidMount?
  • useState 和 useReducer 如何选择?