高阶组件 #

一、高阶组件概述 #

1.1 什么是高阶组件 #

高阶组件(Higher-Order Component,HOC)是一个函数,接收一个组件并返回一个新组件。

text
Component → HOC → Enhanced Component

1.2 基本形式 #

jsx
function higherOrderComponent(WrappedComponent) {
  return function EnhancedComponent(props) {
    return <WrappedComponent {...props} />;
  };
}

// 箭头函数形式
const withEnhancement = (WrappedComponent) => (props) => (
  <WrappedComponent {...props} />
);

1.3 使用场景 #

场景 说明
逻辑复用 复用组件逻辑
属性代理 操作 props
渲染劫持 控制渲染
状态抽象 提取状态管理

二、属性代理 #

2.1 基本示例 #

jsx
function withLoading(WrappedComponent) {
  return function WithLoadingComponent({ isLoading, ...props }) {
    if (isLoading) {
      return <div>Loading...</div>;
    }
    return <WrappedComponent {...props} />;
  };
}

// 使用
const UserListWithLoading = withLoading(UserList);

<UserListWithLoading isLoading={loading} users={users} />

2.2 添加额外 Props #

jsx
function withUser(WrappedComponent) {
  return function WithUserComponent(props) {
    const [user, setUser] = useState(null);

    useEffect(() => {
      fetchCurrentUser().then(setUser);
    }, []);

    return <WrappedComponent {...props} user={user} />;
  };
}

// 使用
const ProfileWithUser = withUser(Profile);

<ProfileWithUser />

2.3 操作 Props #

jsx
function withDefaultValue(WrappedComponent, defaultProps) {
  return function WithDefaultPropsComponent(props) {
    const mergedProps = { ...defaultProps, ...props };
    return <WrappedComponent {...mergedProps} />;
  };
}

// 使用
const InputWithDefault = withDefaultValue(Input, {
  type: 'text',
  placeholder: 'Enter value'
});

<InputWithDefault />

2.4 提取状态 #

jsx
function withFormState(WrappedComponent) {
  return function WithFormStateComponent(props) {
    const [values, setValues] = useState({});
    const [errors, setErrors] = useState({});

    const handleChange = (name, value) => {
      setValues(prev => ({ ...prev, [name]: value }));
    };

    const setFieldError = (name, error) => {
      setErrors(prev => ({ ...prev, [name]: error }));
    };

    return (
      <WrappedComponent
        {...props}
        values={values}
        errors={errors}
        handleChange={handleChange}
        setFieldError={setFieldError}
      />
    );
  };
}

三、渲染劫持 #

3.1 条件渲染 #

jsx
function withAuthentication(WrappedComponent) {
  return function WithAuthenticationComponent(props) {
    const { isAuthenticated } = useContext(AuthContext);

    if (!isAuthenticated) {
      return <LoginPage />;
    }

    return <WrappedComponent {...props} />;
  };
}

// 使用
const ProtectedDashboard = withAuthentication(Dashboard);

3.2 权限控制 #

jsx
function withPermission(WrappedComponent, requiredPermission) {
  return function WithPermissionComponent(props) {
    const { user } = useContext(UserContext);

    if (!user?.permissions?.includes(requiredPermission)) {
      return <AccessDenied />;
    }

    return <WrappedComponent {...props} />;
  };
}

// 使用
const AdminPanel = withPermission(AdminPanelComponent, 'admin');

3.3 修改渲染输出 #

jsx
function withLayout(WrappedComponent, layoutProps) {
  return function WithLayoutComponent(props) {
    return (
      <div class="layout">
        <Header {...layoutProps?.header} />
        <main>
          <WrappedComponent {...props} />
        </main>
        <Footer {...layoutProps?.footer} />
      </div>
    );
  };
}

四、组合 HOC #

4.1 嵌套使用 #

jsx
const EnhancedComponent = withAuthentication(
  withPermission(
    withLoading(
      UserProfile
    ),
    'view_profile'
  )
);

// 使用
<EnhancedComponent isLoading={loading} />

4.2 compose 工具函数 #

jsx
function compose(...funcs) {
  return funcs.reduce((a, b) => (...args) => a(b(...args)), arg => arg);
}

// 使用
const enhance = compose(
  withAuthentication,
  withPermission('view_profile'),
  withLoading
);

const EnhancedUserProfile = enhance(UserProfile);

4.3 使用示例 #

jsx
// 定义多个 HOC
function withLogger(WrappedComponent) {
  return function WithLoggerComponent(props) {
    useEffect(() => {
      console.log('Component mounted/updated');
    });

    return <WrappedComponent {...props} />;
  };
}

function withTheme(WrappedComponent) {
  return function WithThemeComponent(props) {
    const theme = useContext(ThemeContext);
    return <WrappedComponent {...props} theme={theme} />;
  };
}

function withErrorBoundary(WrappedComponent) {
  return class WithErrorBoundaryComponent extends Component {
    state = { hasError: false };

    static getDerivedStateFromError() {
      return { hasError: true };
    }

    render() {
      if (this.state.hasError) {
        return <h1>Something went wrong.</h1>;
      }
      return <WrappedComponent {...this.props} />;
    }
  };
}

// 组合使用
const enhance = compose(
  withErrorBoundary,
  withTheme,
  withLogger
);

const EnhancedComponent = enhance(MyComponent);

五、Ref 转发 #

5.1 问题 #

jsx
function withHOC(WrappedComponent) {
  return function EnhancedComponent(props) {
    return <WrappedComponent {...props} />;
  };
}

// ref 会指向 EnhancedComponent,而非 WrappedComponent
const EnhancedInput = withHOC(Input);
<input ref={inputRef} /> // ref 不是指向 Input

5.2 解决方案 #

jsx
import { forwardRef } from 'preact';

function withHOC(WrappedComponent) {
  const EnhancedComponent = forwardRef((props, ref) => {
    return <WrappedComponent {...props} ref={ref} />;
  });

  EnhancedComponent.displayName = `withHOC(${WrappedComponent.displayName || WrappedComponent.name})`;

  return EnhancedComponent;
}

六、命名规范 #

6.1 displayName #

jsx
function withHOC(WrappedComponent) {
  function WithHOC(props) {
    return <WrappedComponent {...props} />;
  }

  WithHOC.displayName = `withHOC(${getDisplayName(WrappedComponent)})`;

  return WithHOC;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

6.2 命名约定 #

jsx
// HOC 函数名以 with 开头
function withLoading() {}
function withAuthentication() {}
function withTheme() {}

// 返回的组件名以 With 开头或添加后缀
function withLoading(WrappedComponent) {
  return function WithLoading(props) {
    // ...
  };
}

七、实际示例 #

7.1 withFetch #

jsx
function withFetch(url) {
  return function (WrappedComponent) {
    return function WithFetchComponent(props) {
      const [data, setData] = useState(null);
      const [loading, setLoading] = useState(true);
      const [error, setError] = useState(null);

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

        async function fetchData() {
          try {
            setLoading(true);
            const response = await fetch(url);
            const json = await response.json();
            if (!cancelled) {
              setData(json);
            }
          } catch (e) {
            if (!cancelled) {
              setError(e);
            }
          } finally {
            if (!cancelled) {
              setLoading(false);
            }
          }
        }

        fetchData();

        return () => { cancelled = true; };
      }, [url]);

      return (
        <WrappedComponent
          {...props}
          data={data}
          loading={loading}
          error={error}
        />
      );
    };
  };
}

// 使用
const UserListWithFetch = withFetch('/api/users')(UserList);

<UserListWithFetch />

7.2 withToggle #

jsx
function withToggle(WrappedComponent) {
  return function WithToggleComponent(props) {
    const [isOn, setIsOn] = useState(false);

    const toggle = () => setIsOn(prev => !prev);
    const setOn = () => setIsOn(true);
    const setOff = () => setIsOn(false);

    return (
      <WrappedComponent
        {...props}
        isOn={isOn}
        toggle={toggle}
        setOn={setOn}
        setOff={setOff}
      />
    );
  };
}

// 使用
function Dropdown({ isOn, toggle, children }) {
  return (
    <div>
      <button onClick={toggle}>Toggle</button>
      {isOn && <div class="dropdown">{children}</div>}
    </div>
  );
}

const DropdownWithToggle = withToggle(Dropdown);

7.3 withLocalStorage #

jsx
function withLocalStorage(key, initialValue) {
  return function (WrappedComponent) {
    return function WithLocalStorageComponent(props) {
      const [value, setValue] = useState(() => {
        const stored = localStorage.getItem(key);
        return stored ? JSON.parse(stored) : initialValue;
      });

      useEffect(() => {
        localStorage.setItem(key, JSON.stringify(value));
      }, [value]);

      return (
        <WrappedComponent
          {...props}
          storedValue={value}
          setStoredValue={setValue}
        />
      );
    };
  };
}

// 使用
const TodoListWithStorage = withLocalStorage('todos', [])(TodoList);

八、HOC vs Hooks #

8.1 对比 #

方面 HOC Hooks
复用方式 包装组件 函数调用
嵌套问题 可能很深 扁平
调试 较难 较易
类型推断 复杂 简单

8.2 推荐使用 Hooks #

jsx
// HOC 方式
function withWindowSize(WrappedComponent) {
  return function WithWindowSizeComponent(props) {
    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 <WrappedComponent {...props} windowSize={size} />;
  };
}

// Hooks 方式(推荐)
function 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;
}

// 使用
function MyComponent() {
  const windowSize = useWindowSize();
  return <div>{windowSize.width} x {windowSize.height}</div>;
}

九、最佳实践 #

9.1 不要修改原组件 #

jsx
// ❌ 错误:修改原组件
function withHOC(WrappedComponent) {
  WrappedComponent.someProperty = 'value'; // 不要这样做
  return WrappedComponent;
}

// ✅ 正确:返回新组件
function withHOC(WrappedComponent) {
  return function EnhancedComponent(props) {
    return <WrappedComponent {...props} />;
  };
}

9.2 传递所有 Props #

jsx
function withHOC(WrappedComponent) {
  return function EnhancedComponent(props) {
    // ✅ 传递所有 props
    return <WrappedComponent {...props} />;
  };
}

9.3 复制静态方法 #

jsx
function withHOC(WrappedComponent) {
  const EnhancedComponent = (props) => (
    <WrappedComponent {...props} />
  );

  // 复制静态方法
  Object.keys(WrappedComponent).forEach(key => {
    EnhancedComponent[key] = WrappedComponent[key];
  });

  return EnhancedComponent;
}

十、总结 #

要点 说明
定义 接收组件返回组件的函数
属性代理 操作 props
渲染劫持 控制渲染
组合 使用 compose 工具
命名 with 前缀,设置 displayName

核心原则:

  • 不要修改原组件
  • 传递所有 props
  • 复制静态方法
  • 优先使用 Hooks
最后更新:2026-03-28