React 组件

一、组件化思想与核心概念

1.1 什么是组件化?

组件化是将用户界面拆分为独立、可复用、可组合的代码单元的开发模式。React 组件化基于以下几个核心理念:

  • 单一职责原则:每个组件只关注一个特定功能
  • 封装性:组件内部状态和逻辑对外隐藏,通过 props 接口通信
  • 可组合性:小组件可以组合成更复杂的组件
  • 可复用性:一次编写,多处使用

1.2 React 组件化优势

javascript 复制代码
// 传统方式 vs 组件化方式

// 传统:代码重复,难以维护
function renderUserProfile(user) {
  return `
    <div class="profile">
      <img src="${user.avatar}" class="avatar"/>
      <h2>${user.name}</h2>
      <p>${user.bio}</p>
    </div>
  `;
}

function renderUserCard(user) {
  return `
    <div class="card">
      <img src="${user.avatar}" class="avatar"/>
      <h2>${user.name}</h2>
    </div>
  `;
}

// React组件化:复用Avatar组件
const Avatar = ({ src, size }) => (
  <img src={src} className={`avatar avatar-${size}`} />
);

const UserProfile = ({ user }) => (
  <div className="profile">
    <Avatar src={user.avatar} size="large" />
    <h2>{user.name}</h2>
    <p>{user.bio}</p>
  </div>
);

const UserCard = ({ user }) => (
  <div className="card">
    <Avatar src={user.avatar} size="medium" />
    <h2>{user.name}</h2>
  </div>
);

二、组件类型与创建方式

2.1 函数组件 (Function Components)

jsx 复制代码
// 基础函数组件
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

// 箭头函数组件
const Welcome = (props) => {
  return <h1>Hello, {props.name}</h1>;
};

// 隐式返回
const Welcome = props => <h1>Hello, {props.name}</h1>;

2.2 类组件 (Class Components)

jsx 复制代码
class Welcome extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  componentDidMount() {
    console.log('Component mounted');
  }

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

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

2.3 组件组合模式

jsx 复制代码
// 容器组件
const UserDashboard = () => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

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

  if (loading) return <LoadingSpinner />;
  if (!user) return <ErrorPage />;

  return (
    <div className="dashboard">
      <UserHeader user={user} />
      <UserStats stats={user.stats} />
      <RecentActivity activities={user.activities} />
    </div>
  );
};

// 展示组件
const UserHeader = ({ user }) => (
  <header>
    <Avatar src={user.avatar} size="large" />
    <h1>{user.name}</h1>
    <p>{user.title}</p>
  </header>
);

三、Props:组件间通信

3.1 Props 基础使用

jsx 复制代码
// 传递 props
const App = () => (
  <UserProfile
    name="John Doe"
    age={30}
    isOnline={true}
    onLogin={() => console.log('Logged in')}
  />
);

// 接收 props
const UserProfile = ({ name, age, isOnline, onLogin }) => (
  <div>
    <h2>{name}</h2>
    <p>Age: {age}</p>
    <p>Status: {isOnline ? 'Online' : 'Offline'}</p>
    <button onClick={onLogin}>Login</button>
  </div>
);

3.2 Props 验证 (PropTypes)

jsx 复制代码
import PropTypes from 'prop-types';

const UserCard = ({ user, onEdit, showActions }) => (
  <div className="user-card">
    <h3>{user.name}</h3>
    <p>{user.email}</p>
    {showActions && <button onClick={() => onEdit(user.id)}>Edit</button>}
  </div>
);

UserCard.propTypes = {
  user: PropTypes.shape({
    id: PropTypes.number.isRequired,
    name: PropTypes.string.isRequired,
    email: PropTypes.string.isRequired
  }).isRequired,
  onEdit: PropTypes.func,
  showActions: PropTypes.bool
};

UserCard.defaultProps = {
  showActions: true,
  onEdit: () => {}
};

3.3 Children Props

jsx 复制代码
// 容器组件
const Card = ({ title, children, footer }) => (
  <div className="card">
    {title && <div className="card-header">{title}</div>}
    <div className="card-body">{children}</div>
    {footer && <div className="card-footer">{footer}</div>}
  </div>
);

// 使用
const UserProfile = () => (
  <Card 
    title="User Information"
    footer={<button>Save Changes</button>}
  >
    <p>Name: John Doe</p>
    <p>Email: john@example.com</p>
    <p>Location: New York</p>
  </Card>
);

四、State:组件内部状态管理

4.1 useState Hook

jsx 复制代码
import { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ name: '', age: 0 });

  const increment = () => setCount(prev => prev + 1);
  const updateName = name => setUser(prev => ({ ...prev, name }));

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      
      <input
        value={user.name}
        onChange={e => updateName(e.target.value)}
        placeholder="Enter name"
      />
    </div>
  );
};

4.2 状态提升 (Lifting State Up)

jsx 复制代码
// 父组件管理状态
const ParentComponent = () => {
  const [sharedData, setSharedData] = useState('');

  return (
    <div>
      <ChildA data={sharedData} onDataChange={setSharedData} />
      <ChildB data={sharedData} />
      <ChildC data={sharedData} />
    </div>
  );
};

// 子组件A - 修改数据
const ChildA = ({ data, onDataChange }) => (
  <input
    value={data}
    onChange={e => onDataChange(e.target.value)}
  />
);

// 子组件B - 显示数据
const ChildB = ({ data }) => <p>Data: {data}</p>;

// 子组件C - 使用数据
const ChildC = ({ data }) => <div>Length: {data.length}</div>;

4.3 使用Reducer管理复杂状态

jsx 复制代码
import { useReducer } from 'react';

const initialState = {
  loading: false,
  data: null,
  error: null
};

function apiReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS':
      return { loading: false, data: action.payload, error: null };
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload };
    default:
      return state;
  }
}

const ApiComponent = () => {
  const [state, dispatch] = useReducer(apiReducer, initialState);

  const fetchData = async () => {
    dispatch({ type: 'FETCH_START' });
    try {
      const response = await fetch('/api/data');
      const data = await response.json();
      dispatch({ type: 'FETCH_SUCCESS', payload: data });
    } catch (error) {
      dispatch({ type: 'FETCH_ERROR', payload: error.message });
    }
  };

  return (
    <div>
      <button onClick={fetchData} disabled={state.loading}>
        {state.loading ? 'Loading...' : 'Fetch Data'}
      </button>
      {state.error && <p>Error: {state.error}</p>}
      {state.data && <pre>{JSON.stringify(state.data, null, 2)}</pre>}
    </div>
  );
};

五、生命周期与副作用管理

5.1 useEffect Hook

jsx 复制代码
import { useState, useEffect } from 'react';

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

  // 组件挂载和userId变化时执行
  useEffect(() => {
    let isMounted = true;

    const fetchUser = async () => {
      setLoading(true);
      try {
        const userData = await fetchUserById(userId);
        if (isMounted) {
          setUser(userData);
        }
      } catch (error) {
        console.error('Failed to fetch user:', error);
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };

    fetchUser();

    // 清理函数
    return () => {
      isMounted = false;
    };
  }, [userId]); // 依赖数组

  // 监听用户数据变化
  useEffect(() => {
    if (user) {
      document.title = `${user.name}'s Profile`;
      // 跟踪用户查看事件
      trackAnalytics('profile_view', { userId: user.id });
    }
  }, [user]);

  if (loading) return <div>Loading...</div>;
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  );
};

5.2 自定义Hook封装逻辑

jsx 复制代码
// 自定义Hook
const useApi = (url) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
};

// 使用自定义Hook
const UserList = () => {
  const { data: users, loading, error } = useApi('/api/users');

  if (loading) return <div>Loading users...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};

六、组件通信模式

6.1 父子组件通信

jsx 复制代码
// 父组件
const Parent = () => {
  const [items, setItems] = useState([]);

  const addItem = (newItem) => {
    setItems(prev => [...prev, newItem]);
  };

  const removeItem = (itemId) => {
    setItems(prev => prev.filter(item => item.id !== itemId));
  };

  return (
    <div>
      <ItemForm onAddItem={addItem} />
      <ItemList items={items} onRemoveItem={removeItem} />
    </div>
  );
};

// 子组件 - 表单
const ItemForm = ({ onAddItem }) => {
  const [input, setInput] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (input.trim()) {
      onAddItem({
        id: Date.now(),
        text: input.trim()
      });
      setInput('');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={input}
        onChange={e => setInput(e.target.value)}
        placeholder="Add new item"
      />
      <button type="submit">Add</button>
    </form>
  );
};

// 子组件 - 列表
const ItemList = ({ items, onRemoveItem }) => (
  <ul>
    {items.map(item => (
      <li key={item.id}>
        {item.text}
        <button onClick={() => onRemoveItem(item.id)}>Remove</button>
      </li>
    ))}
  </ul>
);

6.2 兄弟组件通信(通过父组件)

jsx 复制代码
const Dashboard = () => {
  const [selectedTab, setSelectedTab] = useState('overview');

  return (
    <div className="dashboard">
      <TabNavigation selectedTab={selectedTab} onTabChange={setSelectedTab} />
      <TabContent selectedTab={selectedTab} />
    </div>
  );
};

const TabNavigation = ({ selectedTab, onTabChange }) => (
  <nav>
    {['overview', 'analytics', 'settings'].map(tab => (
      <button
        key={tab}
        className={selectedTab === tab ? 'active' : ''}
        onClick={() => onTabChange(tab)}
      >
        {tab.charAt(0).toUpperCase() + tab.slice(1)}
      </button>
    ))}
  </nav>
);

const TabContent = ({ selectedTab }) => {
  switch (selectedTab) {
    case 'overview':
      return <OverviewTab />;
    case 'analytics':
      return <AnalyticsTab />;
    case 'settings':
      return <SettingsTab />;
    default:
      return null;
  }
};

6.3 Context API 跨组件通信

jsx 复制代码
import { createContext, useContext, useState } from 'react';

// 创建Context
const ThemeContext = createContext();

// Provider组件
const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  const value = {
    theme,
    toggleTheme,
    isDark: theme === 'dark'
  };

  return (
    <ThemeContext.Provider value={value}>
      <div className={`theme-${theme}`}>
        {children}
      </div>
    </ThemeContext.Provider>
  );
};

// 自定义Hook使用Context
const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
};

// 使用Context的组件
const ThemeToggle = () => {
  const { toggleTheme, isDark } = useTheme();
  
  return (
    <button onClick={toggleTheme}>
      Switch to {isDark ? 'Light' : 'Dark'} Mode
    </button>
  );
};

const App = () => (
  <ThemeProvider>
    <Header />
    <MainContent />
    <Footer />
  </ThemeProvider>
);

const Header = () => {
  const { theme } = useTheme();
  return <header className={`header-${theme}`}>Header</header>;
};

七、高级组件模式

7.1 高阶组件 (HOC)

jsx 复制代码
// 高阶组件:添加加载状态
const withLoading = (WrappedComponent) => {
  return function WithLoadingComponent(props) {
    const [loading, setLoading] = useState(true);
    const [data, setData] = useState(null);

    useEffect(() => {
      const fetchData = async () => {
        setLoading(true);
        try {
          // 假设props有fetchData方法
          const result = await props.fetchData();
          setData(result);
        } catch (error) {
          console.error('Error fetching data:', error);
        } finally {
          setLoading(false);
        }
      };

      fetchData();
    }, [props.fetchData]);

    if (loading) return <div>Loading...</div>;
    return <WrappedComponent {...props} data={data} />;
  };
};

// 使用高阶组件
const UserList = ({ data }) => (
  <ul>
    {data.map(user => (
      <li key={user.id}>{user.name}</li>
    ))}
  </ul>
);

const UserListWithLoading = withLoading(UserList);

// 使用
const App = () => (
  <UserListWithLoading 
    fetchData={() => fetch('/api/users').then(res => res.json())}
  />
);

7.2 Render Props 模式

jsx 复制代码
// Render Props 组件
class DataFetcher extends React.Component {
  state = {
    data: null,
    loading: true,
    error: null
  };

  async componentDidMount() {
    try {
      const data = await this.props.fetchData();
      this.setState({ data, loading: false });
    } catch (error) {
      this.setState({ error: error.message, loading: false });
    }
  }

  render() {
    return this.props.children(this.state);
  }
}

// 使用
const UserProfile = () => (
  <DataFetcher 
    fetchData={() => fetch('/api/user/1').then(res => res.json())}
  >
    {({ data, loading, error }) => {
      if (loading) return <div>Loading...</div>;
      if (error) return <div>Error: {error}</div>;
      return (
        <div>
          <h1>{data.name}</h1>
          <p>{data.email}</p>
        </div>
      );
    }}
  </DataFetcher>
);

7.3 复合组件模式

jsx 复制代码
// 复合组件:Tabs
const Tabs = ({ children }) => {
  const [activeTab, setActiveTab] = useState(0);

  return (
    <div className="tabs">
      <div className="tabs-header">
        {React.Children.map(children, (child, index) =>
          React.cloneElement(child, {
            isActive: index === activeTab,
            onSelect: () => setActiveTab(index)
          })
        )}
      </div>
      <div className="tabs-content">
        {React.Children.toArray(children)[activeTab].props.children}
      </div>
    </div>
  );
};

const Tab = ({ label, isActive, onSelect, children }) => (
  <button
    className={`tab ${isActive ? 'active' : ''}`}
    onClick={onSelect}
  >
    {label}
  </button>
);

// 使用复合组件
const App = () => (
  <Tabs>
    <Tab label="Overview">
      <h2>Overview Content</h2>
      <p>This is the overview tab content.</p>
    </Tab>
    <Tab label="Details">
      <h2>Details Content</h2>
      <p>This is the details tab content.</p>
    </Tab>
    <Tab label="Settings">
      <h2>Settings Content</h2>
      <p>This is the settings tab content.</p>
    </Tab>
  </Tabs>
);

八、性能优化

8.1 React.memo

jsx 复制代码
// 使用React.memo避免不必要的重新渲染
const ExpensiveComponent = React.memo(({ data, onUpdate }) => {
  console.log('ExpensiveComponent rendered');
  
  return (
    <div>
      <h2>Expensive Component</h2>
      <p>Data: {JSON.stringify(data)}</p>
      <button onClick={() => onUpdate(Date.now())}>Update</button>
    </div>
  );
});

// 自定义比较函数
const areEqual = (prevProps, nextProps) => {
  return prevProps.data.id === nextProps.data.id;
};

const UserCard = React.memo(({ user }) => (
  <div className="user-card">
    <h3>{user.name}</h3>
    <p>{user.email}</p>
  </div>
), areEqual);

8.2 useCallback 和 useMemo

jsx 复制代码
const UserDashboard = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [preferences, setPreferences] = useState({});

  // 使用useCallback记忆函数
  const fetchUser = useCallback(async () => {
    const response = await fetch(`/api/users/${userId}`);
    return await response.json();
  }, [userId]);

  // 使用useMemo记忆计算结果
  const userStats = useMemo(() => {
    if (!user) return null;
    
    return {
      totalPosts: user.posts.length,
      averageLikes: user.posts.reduce((sum, post) => sum + post.likes, 0) / user.posts.length,
      engagementRate: calculateEngagementRate(user)
    };
  }, [user]);

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

  return (
    <div>
      {user && (
        <>
          <UserProfile user={user} />
          <UserStats stats={userStats} />
          <UserActions onUpdate={setPreferences} />
        </>
      )}
    </div>
  );
};

8.3 代码分割与懒加载

jsx 复制代码
import { lazy, Suspense } from 'react';

// 懒加载组件
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const UserSettings = lazy(() => import('./UserSettings'));

const App = () => {
  const [showHeavy, setShowHeavy] = useState(false);

  return (
    <div>
      <button onClick={() => setShowHeavy(true)}>
        Load Heavy Component
      </button>
      
      <Suspense fallback={<div>Loading...</div>}>
        {showHeavy && <HeavyComponent />}
      </Suspense>

      <Suspense fallback={<div>Loading Settings...</div>}>
        <UserSettings />
      </Suspense>
    </div>
  );
};

九、最佳实践与设计原则

9.1 组件设计原则

jsx 复制代码
// 1. 单一职责原则
// 不好:组件做太多事情
const UserDashboard = () => {
  // 用户数据获取、UI渲染、业务逻辑全部在一起
};

// 好:拆分为多个单一职责组件
const UserDashboard = () => (
  <div>
    <UserHeader />
    <UserStats />
    <RecentActivity />
  </div>
);

// 2. 受控组件 vs 非受控组件
// 受控组件:数据由React state管理
const ControlledInput = ({ value, onChange }) => (
  <input value={value} onChange={onChange} />
);

// 非受控组件:数据由DOM管理
const UncontrolledInput = () => {
  const inputRef = useRef();
  
  const handleSubmit = () => {
    console.log(inputRef.current.value);
  };
  
  return (
    <div>
      <input ref={inputRef} />
      <button onClick={handleSubmit}>Submit</button>
    </div>
  );
};

// 3. 合理的组件结构
src/
  components/
    common/          # 通用组件
      Button/
        Button.jsx
        Button.css
        index.js
      Modal/
        Modal.jsx
        Modal.css
        index.js
    features/        # 功能组件
      auth/
        LoginForm/
          LoginForm.jsx
          LoginForm.css
          index.js
      dashboard/
        UserCard/
          UserCard.jsx
          UserCard.css
          index.js

9.2 错误边界

jsx 复制代码
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

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

  componentDidCatch(error, errorInfo) {
    console.error('Error caught by boundary:', error, errorInfo);
    // 可以在这里上报错误到监控系统
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          <details>
            {this.state.error.toString()}
          </details>
        </div>
      );
    }

    return this.props.children;
  }
}

// 使用错误边界
const App = () => (
  <ErrorBoundary fallback={<div>Custom error message</div>}>
    <UserDashboard />
    <SettingsPanel />
  </ErrorBoundary>
);

十、测试策略

10.1 组件测试

jsx 复制代码
// UserCard.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import UserCard from './UserCard';

describe('UserCard', () => {
  const mockUser = {
    id: 1,
    name: 'John Doe',
    email: 'john@example.com'
  };

  test('renders user information', () => {
    render(<UserCard user={mockUser} />);
    
    expect(screen.getByText('John Doe')).toBeInTheDocument();
    expect(screen.getByText('john@example.com')).toBeInTheDocument();
  });

  test('calls onEdit when edit button is clicked', () => {
    const handleEdit = jest.fn();
    render(<UserCard user={mockUser} onEdit={handleEdit} />);
    
    fireEvent.click(screen.getByText('Edit'));
    expect(handleEdit).toHaveBeenCalledWith(1);
  });

  test('does not show edit button when showActions is false', () => {
    render(<UserCard user={mockUser} showActions={false} />);
    
    expect(screen.queryByText('Edit')).not.toBeInTheDocument();
  });
});

10.2 Hook 测试

jsx 复制代码
// useApi.test.js
import { renderHook, act } from '@testing-library/react-hooks';
import { useApi } from './useApi';

describe('useApi', () => {
  test('fetches data successfully', async () => {
    const mockData = { id: 1, name: 'Test' };
    global.fetch = jest.fn(() =>
      Promise.resolve({
        json: () => Promise.resolve(mockData)
      })
    );

    const { result, waitForNextUpdate } = renderHook(() => 
      useApi('/api/test')
    );

    expect(result.current.loading).toBe(true);

    await waitForNextUpdate();

    expect(result.current.loading).toBe(false);
    expect(result.current.data).toEqual(mockData);
    expect(result.current.error).toBeNull();
  });
});

总结

React 组件化是现代前端开发的基石,它通过以下核心概念提供了强大的开发体验:

  1. 组件类型:函数组件(推荐)和类组件
  2. 数据流:Props 向下传递,事件向上传递
  3. 状态管理:useState、useReducer、Context API
  4. 生命周期:useEffect 处理副作用
  5. 性能优化:React.memo、useCallback、useMemo
  6. 高级模式:HOC、Render Props、复合组件
  7. 最佳实践:单一职责、错误边界、测试策略
相关推荐
光影少年11 小时前
react的Context 跨层传值、优缺点、适用场景
前端·react.js·掘金·金石计划
JiaWen技术圈13 小时前
React Server Functions 深度解析
前端·react.js·前端框架
JiaWen技术圈14 小时前
React 19 并发渲染器:全面解析与实战指南
前端·react.js·前端框架
Ruihong16 小时前
VuReact v1.8.4 发布:Vue 迁移 React 编译器迎来稳定性大修,这些坑终于被填平了
前端·vue.js·react.js
从文处安16 小时前
「React Router v7 教程」从零到全栈,一篇搞定
前端·react.js
卸任17 小时前
打造基于 Milkdown 的所见即所得 Markdown 编辑器
前端·react.js·markdown
JiaWen技术圈17 小时前
React 19 Fiber 架构 深度解析
前端·react.js·架构
暗冰ཏོ17 小时前
《Vue + React + Java + PHP 项目部署到服务器完整指南》
java·服务器·vue.js·react.js·项目部署
JeariCk17 小时前
React Compiler 1.0 发布半年后的现状
react.js
. . . . .17 小时前
React Native
react native·react.js