阿里云主机折上折
  • 微信号
Current Site:Index > Best practices for design patterns in React

Best practices for design patterns in React

Author:Chuan Chen 阅读数:26531人阅读 分类: JavaScript

React, as a mainstream framework in modern front-end development, naturally aligns with component-based thinking and design patterns. Properly applying design patterns can enhance code maintainability, reusability, and extensibility, especially in complex application scenarios. Below is a practical analysis of common design patterns in React.

Compound Components Pattern

The compound pattern achieves functional decoupling through component nesting, with a classic example being the relationship between <select> and <option>. In React, this can be implemented using React.Context and React.cloneElement:

const Tabs = ({ children }) => {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <TabContext.Provider value={{ activeIndex, setActiveIndex }}>
      <div className="tabs">{children}</div>
    </TabContext.Provider>
  );
};

const TabList = ({ children }) => {
  const { activeIndex, setActiveIndex } = useContext(TabContext);
  return React.Children.map(children, (child, index) =>
    React.cloneElement(child, {
      isActive: index === activeIndex,
      onClick: () => setActiveIndex(index)
    })
  );
};

// Usage example
<Tabs>
  <TabList>
    <Tab>Home</Tab>
    <Tab>Products</Tab>
  </TabList>
  <TabPanels>
    <TabPanel>Home content</TabPanel>
    <TabPanel>Product list</TabPanel>
  </TabPanels>
</Tabs>

This pattern allows the parent component to manage state while child components access the state via context, keeping UI and logic separate.

Render Props Pattern

Passing dynamic content through a function as a child component (children prop) to achieve logic reuse:

const MouseTracker = ({ children }) => {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const handleMouseMove = (e) => setPosition({ x: e.clientX, y: e.clientY });

  return (
    <div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
      {children(position)}
    </div>
  );
};

// Usage example
<MouseTracker>
  ({ x, y }) => (
    <h1>
      Mouse position: {x}, {y}
    </h1>
  )
</MouseTracker>

A more flexible variant uses any prop name:

<DataProvider render={(data) => <Child data={data} />} />

Higher-Order Components (HOC)

Wrapping components with functions to implement cross-cutting concerns:

const withLoading = (WrappedComponent) => {
  return (props) => {
    const [loading, setLoading] = useState(true);
    useEffect(() => {
      const timer = setTimeout(() => setLoading(false), 1000);
      return () => clearTimeout(timer);
    }, []);

    return loading ? <Spinner /> : <WrappedComponent {...props} />;
  };
};

// Usage example
const UserProfile = ({ user }) => <div>{user.name}</div>;
const UserProfileWithLoading = withLoading(UserProfile);

Note: HOCs may cause ref loss, which can be resolved using React.forwardRef.

State Lifting and Single Source of Truth

A common pattern in form components for lifting state:

const FormContainer = () => {
  const [formData, setFormData] = useState({
    username: '',
    password: ''
  });

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

  return (
    <form>
      <Input 
        name="username"
        value={formData.username}
        onChange={handleChange}
      />
      <Input
        name="password"
        type="password"
        value={formData.password}
        onChange={handleChange}
      />
    </form>
  );
};

Provider Pattern

Implementing global state management via the Context API:

const ThemeContext = React.createContext('light');

const App = () => {
  const [theme, setTheme] = useState('dark');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Toolbar />
    </ThemeContext.Provider>
  );
};

const ThemedButton = () => {
  const { theme, setTheme } = useContext(ThemeContext);
  return (
    <button 
      style={{ background: theme === 'dark' ? '#333' : '#EEE' }}
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
    >
      Toggle Theme
    </button>
  );
};

Container/Presentational Component Pattern

A classic pattern for separating logic and UI:

// Container component
const UserListContainer = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    fetchUsers().then(data => {
      setUsers(data);
      setLoading(false);
    });
  }, []);

  return <UserList users={users} loading={loading} />;
};

// Presentational component
const UserList = ({ users, loading }) => (
  loading ? <Spinner /> : (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
);

Custom Hook Pattern

Extracting component logic into reusable hooks:

const useLocalStorage = (key, initialValue) => {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

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

  return [value, setValue];
};

// Usage example
const Counter = () => {
  const [count, setCount] = useLocalStorage('counter', 0);
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Click count: {count}
    </button>
  );
};

Strategy Pattern in Form Validation

Dynamically switching validation rules:

const validationStrategies = {
  email: (value) => /^\S+@\S+$/.test(value),
  password: (value) => value.length >= 8
};

const useValidation = (strategyName) => {
  const validate = validationStrategies[strategyName];
  const [error, setError] = useState('');

  const check = (value) => {
    const isValid = validate(value);
    setError(isValid ? '' : `Invalid ${strategyName}`);
    return isValid;
  };

  return [error, check];
};

// Usage example
const EmailInput = () => {
  const [email, setEmail] = useState('');
  const [error, validateEmail] = useValidation('email');

  return (
    <>
      <input
        type="text"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        onBlur={() => validateEmail(email)}
      />
      {error && <div className="error">{error}</div>}
    </>
  );
};

Observer Pattern and Event Bus

Implementing loose coupling between components:

const EventBus = {
  events: {},
  subscribe(event, callback) {
    if (!this.events[event]) this.events[event] = [];
    this.events[event].push(callback);
  },
  publish(event, data) {
    (this.events[event] || []).forEach(cb => cb(data));
  }
};

// Publisher component
const Publisher = () => (
  <button onClick={() => EventBus.publish('alert', 'Button clicked')}>
    Trigger Event
  </button>
);

// Subscriber component
const Subscriber = () => {
  const [message, setMessage] = useState('');
  
  useEffect(() => {
    EventBus.subscribe('alert', setMessage);
    return () => EventBus.unsubscribe('alert', setMessage);
  }, []);

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

Factory Pattern for Dynamic Components

Generating components based on configuration:

const componentMap = {
  text: ({ value }) => <span>{value}</span>,
  image: ({ src }) => <img src={src} alt="" />,
  button: ({ label, onClick }) => (
    <button onClick={onClick}>{label}</button>
  )
};

const DynamicComponent = ({ type, ...props }) => {
  const Component = componentMap[type] || componentMap.text;
  return <Component {...props} />;
};

// Usage example
const configs = [
  { type: 'text', value: 'Static text' },
  { type: 'button', label: 'Submit', onClick: () => alert('Clicked') }
];

const ComponentFactory = () => (
  <div>
    {configs.map((config, i) => (
      <DynamicComponent key={i} {...config} />
    ))}
  </div>
);

Memento Pattern and State Snapshots

Implementing undo/redo functionality:

const useHistory = (initialState) => {
  const [history, setHistory] = useState([initialState]);
  const [index, setIndex] = useState(0);

  const current = history[index];
  
  const push = (newState) => {
    const newHistory = history.slice(0, index + 1);
    setHistory([...newHistory, newState]);
    setIndex(newHistory.length);
  };

  const undo = () => index > 0 && setIndex(i => i - 1);
  const redo = () => index < history.length - 1 && setIndex(i => i + 1);

  return [current, push, { undo, redo, canUndo: index > 0, canRedo: index < history.length - 1 }];
};

// Usage example
const SketchPad = () => {
  const [paths, setPaths, { undo, redo }] = useHistory([]);
  
  const handleDraw = (newPath) => {
    setPaths([...paths, newPath]);
  };

  return (
    <div>
      <Canvas onDraw={handleDraw} paths={paths} />
      <button onClick={undo} disabled={!canUndo}>Undo</button>
      <button onClick={redo} disabled={!canRedo}>Redo</button>
    </div>
  );
};

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.