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. 最佳实践:单一职责、错误边界、测试策略
相关推荐
吃奥特曼的饼干2 小时前
React useEffect 清理函数:别让依赖数组坑了你!
前端·react.js
随笔记2 小时前
react中函数式组件和类组件有什么区别?新建的react项目用函数式组件还是类组件?
前端·react.js·typescript
emojiwoo3 小时前
React 状态管理:useState 与 useDatePersistentState 深度对比
前端·javascript·react.js
D11_3 小时前
【React】JSX基础
前端·react.js·前端框架
晴空雨3 小时前
Zustand vs Redux Toolkit:现代 React 状态管理深度对比
前端·react.js
gaog2zh4 小时前
100202Title和Input组件_编辑器-react-仿低代码平台项目
react.js·低代码·编辑器
XiaoMu_0014 小时前
【Vue vs React:前端框架深度对比分析】
vue.js·react.js·前端框架
GISer_Jing8 小时前
前端面试通关:Cesium+Three+React优化+TypeScript实战+ECharts性能方案
前端·react.js·面试
刺客-Andy17 小时前
React 第七十节 Router中matchRoutes的使用详解及注意事项
前端·javascript·react.js