React 逻辑与视图分离实践

在 React 开发中,将业务逻辑与视图组件分离是一种重要的架构模式,可以提高代码的可维护性、可测试性和复用性。

分离模式概览

1. 自定义 Hook 模式

// useUserData.js (逻辑层)
import { useState, useEffect } from 'react';

export function useUserData(userId) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const userData = await response.json();
        setUser(userData);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, [userId]);

  return { user, loading, error };
}

// UserProfile.js (视图层)
import { useUserData } from './useUserData';

function UserProfile({ userId }) {
  const { user, loading, error } = useUserData(userId);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

2. 容器组件模式

// UserProfileContainer.js (逻辑容器)
import { useState, useEffect } from 'react';
import UserProfile from './UserProfile';

function UserProfileContainer({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 数据获取逻辑
    fetchUserData(userId).then(userData => {
      setUser(userData);
      setLoading(false);
    });
  }, [userId]);

  return <UserProfile user={user} loading={loading} />;
}

// UserProfile.js (展示组件)
function UserProfile({ user, loading }) {
  if (loading) return <div>Loading...</div>;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

3. 高阶组件模式

// withUserData.js (高阶组件)
function withUserData(WrappedComponent) {
  return function WithUserDataComponent({ userId, ...props }) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
      fetchUserData(userId).then(userData => {
        setUser(userData);
        setLoading(false);
      });
    }, [userId]);

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

// UserProfile.js (展示组件)
function UserProfile({ user, loading }) {
  if (loading) return <div>Loading...</div>;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// 使用高阶组件
export default withUserData(UserProfile);

最佳实践

1. 逻辑分层架构

src/
  components/     # 纯展示组件
  hooks/         # 自定义 Hook
  services/      # API 调用和数据操作
  utils/         # 工具函数
  containers/    # 容器组件(可选)

2. 服务层抽象

// services/userService.js
export const userService = {
  async getUserById(id) {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
  },
  
  async updateUser(id, data) {
    const response = await fetch(`/api/users/${id}`, {
      method: 'PUT',
      body: JSON.stringify(data)
    });
    return response.json();
  }
};

3. 状态管理集成

// hooks/useUserStore.js
import { useState, useCallback } from 'react';
import { userService } from '../services/userService';

export function useUserStore() {
  const [users, setUsers] = useState({});
  const [loading, setLoading] = useState(false);

  const fetchUser = useCallback(async (userId) => {
    setLoading(true);
    try {
      const user = await userService.getUserById(userId);
      setUsers(prev => ({ ...prev, [userId]: user }));
    } finally {
      setLoading(false);
    }
  }, []);

  return { users, loading, fetchUser };
}

测试策略

1. 逻辑层测试

// useUserData.test.js
import { renderHook, act } from '@testing-library/react-hooks';
import { useUserData } from './useUserData';

jest.mock('../services/userService');

test('should fetch user data', async () => {
  const { result, waitForNextUpdate } = renderHook(() => useUserData('123'));
  
  await waitForNextUpdate();
  
  expect(result.current.user).toEqual({ id: '123', name: 'John' });
});

2. 视图层测试

// UserProfile.test.js
import { render, screen } from '@testing-library/react';
import UserProfile from './UserProfile';

test('should display user profile', () => {
  const mockUser = { name: 'John', email: 'john@example.com' };
  
  render(<UserProfile user={mockUser} loading={false} />);
  
  expect(screen.getByText('John')).toBeInTheDocument();
  expect(screen.getByText('john@example.com')).toBeInTheDocument();
});

优势总结

  1. 更好的可测试性:逻辑和视图可以独立测试
  2. 代码复用:业务逻辑可以在多个组件间共享
  3. 关注点分离:开发者可以专注于单一职责
  4. 维护性:修改逻辑不影响视图,反之亦然
  5. 团队协作:前端和设计可以并行工作

实际应用建议

  • 对于简单组件,可以直接使用自定义 Hook
  • 对于复杂业务场景,考虑使用状态管理库(Redux、Zustand)
  • 使用 TypeScript 增强类型安全和开发体验
  • 结合 React Query 或 SWR 处理服务器状态

这种分离模式让 React 应用更加结构清晰,易于维护和扩展。

更多推荐