React 组件与 Props

0x01 组件基础

组件是 React 应用的可复用构建块,将 UI 拆分为独立、可组合的部分。每个组件维护自己的状态和逻辑。

函数组件

函数组件是现代 React 开发的主流方式,配合 Hooks 使用:

// 基本函数组件
function Greeting({ name }: { name: string }) {
  return <h1>Hello, {name}!</h1>;
}

// 使用组件
function App() {
  return <Greeting name="React" />;
}

类组件

类组件是早期的组件写法,现在较少使用:

import React, { Component } from 'react';

interface GreetingProps {
  name: string;
}

class Greeting extends Component<GreetingProps> {
  render() {
    return <h1>Hello, {this.props.name}!</h1>;
  }
}

组件类型声明

// 函数组件类型
type ComponentProps<T> = {
  data: T;
  onChange: (value: T) => void;
};

function MyComponent<T>({ data, onChange }: ComponentProps<T>) {
  return <div onClick={() => onChange(data)}>{data}</div>;
}

// 泛型组件
function List<T>({ items }: { items: T[] }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{String(item)}</li>
      ))}
    </ul>
  );
}

0x02 Props 基础

Props 是传递给组件的数据,作为组件的只读参数。

基本 Props

// 定义 Props 接口
interface ButtonProps {
  text: string;
  onClick: () => void;
  disabled?: boolean;  // 可选属性
}

// 使用 Props
function Button({ text, onClick, disabled = false }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {text}
    </button>
  );
}

// 调用组件
function App() {
  return (
    <Button 
      text="Click Me" 
      onClick={() => console.log('Clicked!')} 
    />
  );
}

默认 Props

// 方式一:默认参数
function Avatar({ 
  src, 
  size = 48, 
  alt = 'avatar' 
}: { 
  src: string; 
  size?: number; 
  alt?: string;
}) {
  return (
    <img 
      src={src} 
      width={size} 
      height={size} 
      alt={alt} 
      style={{ borderRadius: '50%' }}
    />
  );
}

// 方式二:defaultProps(类组件)
interface AlertProps {
  message: string;
  type?: 'info' | 'warning' | 'error';
}

function Alert({ message, type = 'info' }: AlertProps) {
  return <div className={`alert alert-${type}`}>{message}</div>;
}

Alert.defaultProps = {
  type: 'info'
};

children Props

// 接收 children
function Card({ 
  title, 
  children 
}: { 
  title: string; 
  children: React.ReactNode;
}) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-content">
        {children}
      </div>
    </div>
  );
}

// 使用
function App() {
  return (
    <Card title="Welcome">
      <p>This is the card content.</p>
      <button>Learn More</button>
    </Card>
  );
}

函数作为 Props

interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
}

function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item, index)}</li>
      ))}
    </ul>
  );
}

// 使用
function App() {
  const users = ['Alice', 'Bob', 'Charlie'];

  return (
    <List 
      items={users}
      renderItem={(name, index) => (
        <span>{index + 1}. {name}</span>
      )}
    />
  );
}

Props 展开

interface ButtonProps {
  variant: 'primary' | 'secondary';
  size: 'sm' | 'md' | 'lg';
  children: React.ReactNode;
  onClick?: () => void;
}

function Button({ variant, size, children, ...rest }: ButtonProps) {
  return (
    <button 
      className={`btn btn-${variant} btn-${size}`}
      {...rest}
    >
      {children}
    </button>
  );
}

0x03 组件组合

组合模式

// Layout 组件
function Layout({ 
  header, 
  main, 
  footer 
}: { 
  header: React.ReactNode; 
  main: React.ReactNode; 
  footer: React.ReactNode;
}) {
  return (
    <div className="layout">
      <header>{header}</header>
      <main>{main}</main>
      <footer>{footer}</footer>
    </div>
  );
}

// 使用
function App() {
  return (
    <Layout
      header={<h1>My App</h1>}
      main={<p>Main content goes here</p>}
      footer={<p>&copy; 2024</p>}
    />
  );
}

渲染属性模式

// 渲染属性组件
function MouseTracker({ 
  render 
}: { 
  render: (position: { x: number; y: number }) => React.ReactNode;
}) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };

    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, []);

  return render(position);
}

// 使用
function App() {
  return (
    <MouseTracker
      render={({ x, y }) => (
        <p>Mouse position: {x}, {y}</p>
      )}
    />
  );
}

高阶组件(HOC)

高阶组件是一个函数,接受组件并返回新组件:

// 高阶组件:添加日志功能
function withLogger<T extends { name: string }>(
  WrappedComponent: React.ComponentType<T>
) {
  return function (props: T) {
    useEffect(() => {
      console.log(`Component ${props.name} mounted`);
      return () => console.log(`Component ${props.name} unmounted`);
    }, [props.name]);

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

// 使用
interface UserCardProps {
  name: string;
  email: string;
}

function UserCard({ name, email }: UserCardProps) {
  return (
    <div className="user-card">
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  );
}

const UserCardWithLogger = withLogger(UserCard);

function App() {
  return (
    <UserCardWithLogger 
      name="User Card" 
      email="user@example.com" 
    />
  );
}

受控组件与非受控组件

// 受控组件:状态由 React 控制
function ControlledInput() {
  const [value, setValue] = useState('');

  return (
    <input 
      value={value}
      onChange={e => setValue(e.target.value)}
    />
  );
}

// 非受控组件:状态由 DOM 控制
function UncontrolledInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleSubmit = () => {
    console.log('Value:', inputRef.current?.value);
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleSubmit}>Submit</button>
    </div>
  );
}

// 受控select
function ControlledSelect() {
  const [value, setValue] = useState('apple');

  return (
    <select value={value} onChange={e => setValue(e.target.value)}>
      <option value="apple">Apple</option>
      <option value="banana">Banana</option>
      <option value="orange">Orange</option>
    </select>
  );
}

0x04 组件通信

父子组件通信

// 父组件
function Parent() {
  const [message, setMessage] = useState('');

  return (
    <div>
      <Child onSend={setMessage} />
      <p>Received: {message}</p>
    </div>
  );
}

// 子组件
function Child({ onSend }: { onSend: (msg: string) => void }) {
  const [input, setInput] = useState('');

  return (
    <div>
      <input 
        value={input}
        onChange={e => setInput(e.target.value)}
      />
      <button onClick={() => onSend(input)}>
        Send to Parent
      </button>
    </div>
  );
}

兄弟组件通信

// 使用父组件作为中介
function Siblings() {
  const [sharedState, setSharedState] = useState(0);

  return (
    <div>
      <SiblingA count={sharedState} />
      <SiblingB onIncrement={() => setSharedState(s => s + 1)} />
    </div>
  );
}

function SiblingA({ count }: { count: number }) {
  return <p>Count: {count}</p>;
}

function SiblingB({ onIncrement }: { onIncrement: () => void }) {
  return <button onClick={onIncrement}>Increment</button>;
}

Context 跨层级通信

// 创建 Context
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | null>(null);

// Provider
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

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

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 使用 Context 的组件
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <button 
      onClick={toggleTheme}
      style={{
        background: theme === 'dark' ? '#333' : '#fff',
        color: theme === 'dark' ? '#fff' : '#333'
      }}
    >
      Current: {theme}
    </button>
  );
}

状态提升

// 将状态提升到公共父组件
function Calculator() {
  const [num1, setNum1] = useState(0);
  const [num2, setNum2] = useState(0);

  const result = num1 + num2;

  return (
    <div>
      <InputValue value={num1} onChange={setNum1} label="Number 1" />
      <InputValue value={num2} onChange={setNum2} label="Number 2" />
      <p>Result: {result}</p>
    </div>
  );
}

function InputValue({ 
  value, 
  onChange, 
  label 
}: { 
  value: number; 
  onChange: (v: number) => void;
  label: string;
}) {
  return (
    <div>
      <label>{label}: </label>
      <input
        type="number"
        value={value}
        onChange={e => onChange(Number(e.target.value))}
      />
    </div>
  );
}

0x05 组件性能优化

React.memo

使用 React.memo 缓存组件,避免不必要的渲染:

import { memo } from 'react';

// memoized 组件
const ListItem = memo(function ListItem({ 
  item, 
  onClick 
}: { 
  item: { id: number; name: string };
  onClick: (id: number) => void;
}) {
  console.log('ListItem rendered:', item.id);

  return (
    <li onClick={() => onClick(item.id)}>
      {item.name}
    </li>
  );
});

// 使用
function List({ items, onItemClick }: { 
  items: Array<{ id: number; name: string }>;
  onItemClick: (id: number) => void;
}) {
  return (
    <ul>
      {items.map(item => (
        <ListItem 
          key={item.id} 
          item={item} 
          onClick={onItemClick} 
        />
      ))}
    </ul>
  );
}

useMemo 优化 props

interface User {
  id: number;
  name: string;
  email: string;
}

function UserList({ 
  users, 
  filter 
}: { 
  users: User[]; 
  filter: string;
}) {
  // 缓存过滤后的用户列表
  const filteredUsers = useMemo(() => {
    return users.filter(user => 
      user.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [users, filter]);

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

useCallback 优化回调

function Parent() {
  const [items] = useState(['a', 'b', 'c']);
  const [count, setCount] = useState(0);

  // 缓存回调函数,避免子组件重渲染
  const handleClick = useCallback((item: string) => {
    console.log('Clicked:', item);
  }, []);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>
        Re-render: {count}
      </button>
      <ChildList items={items} onItemClick={handleClick} />
    </div>
  );
}

0x06 组件最佳实践

单一职责

// ❌ 错误:一个组件做太多事情
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);

  // 获取用户、帖子...
  // 渲染用户信息、帖子列表...

  return <div>...</div>;
}

// ✅ 正确:拆分组件
function UserProfile({ userId }: { userId: string }) {
  return (
    <div>
      <UserInfo userId={userId} />
      <UserPosts userId={userId} />
    </div>
  );
}

function UserInfo({ userId }: { userId: string }) {
  // 只负责用户信息
  const [user, setUser] = useState(null);
  // ...
  return <div>...</div>;
}

function UserPosts({ userId }: { userId: string }) {
  // 只负责帖子列表
  const [posts, setPosts] = useState([]);
  // ...
  return <div>...</div>;
}

Props 校验

import PropTypes from 'prop-types';

function Button({ text, variant, disabled }: ButtonProps) {
  return (
    <button className={`btn-${variant}`} disabled={disabled}>
      {text}
    </button>
  );
}

Button.propTypes = {
  text: PropTypes.string.isRequired,
  variant: PropTypes.oneOf(['primary', 'secondary', 'danger']),
  disabled: PropTypes.bool
};

Button.defaultProps = {
  variant: 'primary',
  disabled: false
};

类型化 Hooks

// 自定义 Hook 类型
function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  return [storedValue, setStoredValue] as const;
}

// 使用
function App() {
  const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');

  return <div>{theme}</div>;
}

参考