PaoloJulian.dev - Article

go to article list

Real-Life Applications of Design Patterns in Your Daily React Code

Paolo Vincent Julian
 Real-Life Applications of Design Patterns in Your Daily React Code banner

Banner - Real-Life Applications of Design Patterns in Your Daily React Code

LAST UPDATED 

Picture this! You're crafting user interfaces, managing data flows, and ensuring smooth interactions. All these tasks involve tackling familiar scenarios – scenarios that might have you thinking, "I've got a neat way to solve this!" But have you ever wondered if your 'neat way' has an official name and a broader application? Design patterns are here to illuminate those moments of coding brilliance, giving your intuitive solutions a proper identity.

In this bite-sized exploration, we'll reveal how design patterns seamlessly blend into your daily coding routine, magnifying your skills and transforming you into a front-end virtuoso. So, fasten your seatbelt as we embark on a journey to recognize, appreciate, and apply the design patterns that power your coding feats!

Table of Contents

  1. Discovering patterns
  2. Benefits of pattern awareness
  3. Commonly used patterns
  4. Summary
  5. Conclusion

Discovering patterns

Ever experienced that 'aha' moment when you realize that what you've been doing intuitively has a name? That's exactly what happens when you recognize design patterns in your own solutions. It's like discovering a map that leads you through uncharted coding territories.

Here is one example of an 'aha' moment

jsx
import { useState, useEffect } from 'react';

// Before recognizing the pattern
const useDataFetch = (url) => {
    const [data, setData] = useState(null);
    const [isLoading, setIsLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        const fetchData = async () => {
            try {
                const response = await fetch(url);
                const jsonData = await response.json();
                setData(jsonData);
            } catch (error) {
                setError(error);
            } finally {
                setIsLoading(false);
            }
        };

        fetchData();
    }, [url]);

    return { data, isLoading, error };
};

// After recognizing the pattern (A Facade!)
const DataComponent = () => {
    const { data, isLoading, error } = useDataFetcher('https://api.example.com/data');

    if (isLoading) {
        return <div>Loading...</div>;
    }

    if (error) {
        return <div>Error: {error.message}</div>;
    }

    return <div>{JSON.stringify(data)}</div>;
};

Benefits of Pattern Awareness

Discovering and recognizing design patterns in your coding journey isn't just about putting labels on your natural solutions. It's like stumbling upon a chest of valuable treasures that can truly boost your front-end development skills. Let me show you why knowing these patterns is a big deal:

1. Structured Problem Solving: Design patterns are like tried-and-true playbooks for common coding challenges. When you use them, you're armed with battle-tested strategies that guide your problem-solving journey. This way, you can conquer challenges faster and with a whole lot more confidence.

2. Code Consistency: When you recognize design patterns, you're more likely to apply consistent solutions across your codebase. This consistency enhances the readability and maintainability of your code, making it easier for you and your fellow developers to understand and collaborate.

3. Accelerated Learning Curve As you explore various design patterns, you're essentially tapping into the collective knowledge of experienced developers. This exposure accelerates your learning curve, allowing you to grasp advanced concepts and techniques more quickly.

4. Efficient Collaboration: Design patterns serve as a common language among developers. When you discuss your solutions using recognized patterns, communication becomes seamless and more effective. This is particularly valuable when working on team projects or seeking help within the developer community.

5. Scalability and Adaptability: Solutions built around design patterns are often more scalable and adaptable. As your projects grow, the patterns provide a foundation for extending and modifying functionalities without causing major disruptions.

6. Problem Domain Mastery: Becoming proficient in design patterns helps you gain a deeper understanding of the problem domains you work in. You'll recognize recurring scenarios and know how to approach them using time-tested strategies.

7. Confidence in Decision-Making: Armed with the knowledge of design patterns, you can approach coding challenges with increased confidence. You'll have a toolkit of strategies to choose from, allowing you to make informed decisions based on the problem at hand.

Commonly used patterns

In the realm of front-end development, several design patterns play pivotal roles in solving recurring challenges. Let's take a glimpse into a few commonly used patterns and how they seamlessly integrate into your coding endeavors:

Singleton pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance. In the realm of front-end development, this pattern becomes particularly handy when managing shared resources that need to be accessed and modified from various parts of an application. One more commonly used example is the shopping cart, where you want to maintain a consistent and unified cart state across different components.

Examples:

1. Authentication Managing user authentication across your app is a prime example of the Singleton pattern. Let's consider a simplified scenario using React and a custom global state management approach.

jsx
import React, { createContext, useContext, useState } from 'react';

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  return (
    <AuthContext.Provider value={{ isAuthenticated, setIsAuthenticated }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => useContext(AuthContext);

// Usage in components
// ... (Navigation, Content, etc.)

2. Shopping Cart One more commonly used example is the shopping cart in an e-commerce application. With various components interacting with the cart, the Singleton pattern streamlines cart management. By centralizing the cart's state, you establish a single source of truth accessible from any corner of your app. Here's a simplified example implemented in React:

jsx
import React, { createContext, useContext, useState } from 'react';

const CartContext = createContext();

export const CartProvider = ({ children }) => {
  const [cartItems, setCartItems] = useState([]);

  const addItemToCart = (item) => {
    setCartItems((prevItems) => [...prevItems, item]);
  };

  const removeItemFromCart = (item) => {
    setCartItems((prevItems) => prevItems.filter((i) => i.id !== item.id));
  };

  return (
    <CartContext.Provider value={{ cartItems, addItemToCart, removeItemFromCart }}>
      {children}
    </CartContext.Provider>
  );
};

export const useCart = () => useContext(CartContext);

// Usage in components
// ... (CartButton, CartItemList, etc.)

Facade pattern

The Facade pattern acts as a simplified interface to a more complex system or set of components, making interactions with that system easier and more intuitive. In front-end development, especially with frameworks like React, the Facade pattern becomes a powerful tool for abstracting away intricate operations and exposing a user-friendly interface.

Examples:

1. Simplifying Complex State Management Managing intricate state changes can lead to cluttered and convoluted code. This is where the Facade pattern shines as a powerful ally. By encapsulating complex scenarios within a custom hook, you can create a clear and intuitive interface that hides the intricacies of state management. Let's delve into an example:

jsx
import { useState, useEffect } from 'react';

const useDataFetch = (url) => {
    const [data, setData] = useState(null);
    const [isLoading, setIsLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        const fetchData = async () => {
            try {
                const response = await fetch(url);
                const jsonData = await response.json();
                setData(jsonData);
            } catch (error) {
                setError(error);
            } finally {
                setIsLoading(false);
            }
        };

        fetchData();
    }, [url]);

    return { data, isLoading, error };
};

const DataComponent = () => {
    const { data, isLoading, error } = useDataFetcher('https://api.example.com/data');

    if (isLoading) {
        return <div>Loading...</div>;
    }

    if (error) {
        return <div>Error: {error.message}</div>;
    }

    return <div>{JSON.stringify(data)}</div>;
};

2. API Client Library Consider a scenario where your React app interacts with multiple APIs for various functionalities. Instead of scattering API calls throughout your codebase, you can create a Facade to manage these interactions seamlessly. You can also later-on change from using fetch into using axios.

jsx
// /lib/api-client.js
// ...Rest of the code
const apiClient = {
  get: async (url) => {
    // Can later on change to axios without any problems
    const response = await fetch(url);
    const data = await response.json();
    return data;
  },
  post: async (url, body) => {
    // Can later on change to axios without any problems
    const response = await fetch(url, {
      method: 'POST',
      body: JSON.stringify(body),
      headers: {
        'Content-Type': 'application/json',
      },
    });
    const data = await response.json();
    return data;
  },
  // Additional methods as needed
};
// ...Rest of the code

// /components/App.jsx
// ...Rest of the code
const App = () => {
  const [userData, setUserData] = useState(null);

  useEffect(() => {
    const fetchUserData = async () => {
      const data = await apiClient.get('https://api.example.com/user');
      setUserData(data);
    };

    fetchUserData();
  }, []);

  return (
    <div>
      <h1>Hello, {userData ? userData.name : 'Guest'}!</h1>
    </div>
  );
};

export default App;

3. 3rd Party Libraries like Google Map

js
// /lib/map-facade.js
const initialize = (apiKey) => {
  const script = document.createElement('script');
  script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}`;
  script.async = true;
  script.defer = true;
  document.head.appendChild(script);
};

const createMap = (element, options) => {
  return new window.google.maps.Map(element, options);
};

export { initialize, createMap };

// /components/MapComponent.js
import React, { useEffect, useRef } from 'react';
import { initialize, createMap } from '../lib/MapFacade'; // Import from the lib folder

const MapComponent = ({ apiKey, initialCoordinates }) => {
  const mapRef = useRef(null);

  useEffect(() => {
    initialize(apiKey);
    if (window.google) {
      mapRef.current = createMap(
        mapRef.current,
        {
          center: initialCoordinates,
          zoom: 12,
        }
      );
    }
  }, [apiKey, initialCoordinates]);

  return (
    <div ref={mapRef} style={{ width: '100%', height: '400px' }}></div>
  );
};

export default MapComponent;

Adapter pattern

The Adapter pattern bridges the gap between two incompatible interfaces, allowing them to work together seamlessly. It's like having a translator that ensures communication between entities with different languages. In front-end development, the Adapter pattern is often used to integrate third-party components or libraries that have differing interfaces.

Examples:

1. Lottie for web and mobile I encountered this situation while working on a single-codebase application that employed both React Native and React Native Web. For instance, when utilizing the "lottie" package, distinct versions were required depending on whether it was intended for mobile or web usage.

jsx
import React from 'react';
import LottieWeb from 'lottie-web'; // Import lottie-web for web
import LottieReactNative from 'lottie-react-native'; // Import lottie-react-native for mobile

const LottieAdapter = ({ animationData, width, height, platform }) => {
  if (platform === 'web') {
    return (
      <div style={{ width, height }}>
        <LottieWeb options={{ animationData }} width={width} height={height} />
      </div>
    );
  } else if (platform === 'mobile') {
    return (
      <LottieReactNative
        source={animationData}
        style={{ width, height }}
        autoPlay
        loop
      />
    );
  } else {
    return <div>Unsupported platform</div>;
  }
};

const App = () => {
  return (
    <div>
      <LottieAdapter
        animationData={/* Lottie JSON animation data */}
        width={200}
        height={200}
        platform="web"
      />
      <LottieAdapter
        animationData={/* Lottie JSON animation data */}
        width={200}
        height={200}
        platform="mobile"
      />
    </div>
  );
};

2. Responsive Navbar for Mobile and Web

jsx
import React from 'react';
import NavbarWeb from './NavbarWeb'; // Import web version of Navbar
import NavbarMobile from './NavbarMobile'; // Import mobile version of Navbar

const NavbarAdapter = ({ platform, items }) => {
  if (platform === 'web') {
    return <NavbarWeb items={items} />;
  } else if (platform === 'mobile') {
    return <NavbarMobile items={items} />;
  } else {
    return <div>Unsupported platform</div>;
  }
};

const App = () => {
  const navItems = [
    { label: 'Home', link: '/' },
    { label: 'About', link: '/about' },
    { label: 'Contact', link: '/contact' },
  ];

  return (
    <div>
      <NavbarAdapter platform="web" items={navItems} />
      <NavbarAdapter platform="mobile" items={navItems} />
    </div>
  );
};

Factory pattern

The Factory pattern is all about creating objects without specifying their exact class or type explicitly. It acts as a blueprint for creating objects while providing a centralized place to manage the object creation process. In React and front-end development, the Factory pattern can be particularly useful for creating components with varying configurations or for abstracting the creation of complex objects.

Examples:

1. Creating Diverse Cards Consider a scenario where you need to generate cards with varying content and styles. The Factory pattern enables you to dynamically create different card types based on a configuration, streamlining the creation process and promoting code reusability.

jsx
import React from 'react';

// Card components
const SimpleCard = ({ title, content }) => (
  <div className="simple-card">
    <h2>{title}</h2>
    <p>{content}</p>
  </div>
);

const FeaturedCard = ({ title, content, image }) => (
  <div className="featured-card">
    <img src={image} alt={title} />
    <h2>{title}</h2>
    <p>{content}</p>
  </div>
);

// CardFactory
const CardFactory = (props) => {
  switch (props.type) {
    case 'simple':
      return <SimpleCard {...props} />;
    case 'featured':
      return <FeaturedCard {...props} />;
    default:
      return null;
  }
};

const App = () => {
  const items = [
    { id: 1, type: 'simple', title: 'Simple Card', content: 'Just a simple card.' },
    { id: 2, type: 'featured', title: 'Featured Card', content: 'A card with a featured image.', image: 'https://example.com/featured-image.jpg' },
    // ... other items ...
  ];

  return (
    <div>
      {items.map((item) => (
        <CardFactory key={item.id} {...item} />
      ))}
    </div>
  );
};

export default App;

2. Creating a Heading component Let's consider a scenario where you need to implement heading components with varying styles and hierarchy levels. Traditionally, you might use separate components like <HeadingH1 /> and <HeadingH2 />, but what if you want to consolidate these into a single, more versatile <Heading /> component?

jsx
import React from 'react';

const H1 = (props: React.HtmlHTMLAttributes<HTMLHeadingElement>) => (
  <h1 className="text-4xl font-bold text-gray-800" {...props}></h1>
);

const H2 = (props: React.HtmlHTMLAttributes<HTMLHeadingElement>) => (
  <h2 className="text-xl font-semibold text-gray-700" {...props}></h2>
);

interface IHeading {
  h1: typeof H1;
  h2: typeof H2;
}

const Heading: IHeading = {
  h1: H1,
  h2: H2,
};

export default Heading;

// use case
<Heading.h1>Heading 1</Heading.h1>
<Heading.h2>Heading 2</Heading.h2>

Observer pattern

The Observer pattern establishes a one-to-many relationship between objects, where a single subject (observable) notifies multiple observers about changes in its state. This pattern is essential for handling event-driven scenarios, such as updating UI components when underlying data changes.

Examples:

1. Real-Time Web Sockets The Observer pattern is strikingly showcased in real-time chat applications, offering a powerful solution to enable real-time interactions. In this example, a WebSocket connection acts as the observable subject, while connected clients become observers. As one client sends a message, all connected clients receive instant notifications, resulting in synchronized updates across user interfaces. The Observer pattern's seamless orchestration of real-time events extends beyond chats to various applications like notifications, gambling, and gaming.

jsx
import React, { useState, useEffect } from 'react';

const ChatApp = () => {
  const [messages, setMessages] = useState([]);
  const [inputMessage, setInputMessage] = useState('');
  const socket = new WebSocket('ws://localhost:8080'); // WebSocket server URL

  useEffect(() => {
    const handleIncomingMessage = (event) => {
      const newMessage = JSON.parse(event.data);
      setMessages((prevMessages) => [...prevMessages, newMessage]);
    };

    socket.addEventListener('message', handleIncomingMessage);

    return () => {
      socket.removeEventListener('message', handleIncomingMessage);
      socket.close(); // Close the WebSocket connection on component unmount
    };
  }, []);

  const sendMessage = () => {
    if (inputMessage.trim() !== '') {
      socket.send(JSON.stringify({ text: inputMessage }));
      setInputMessage('');
    }
  };

  return (
    <div>
      <div className="message-list">
        {messages.map((message, index) => (
          <div key={index} className="message">
            {message.text}
          </div>
        ))}
      </div>
      <div className="input-section">
        <input
          type="text"
          value={inputMessage}
          onChange={(e) => setInputMessage(e.target.value)}
        />
        <button onClick={sendMessage}>Send</button>
      </div>
    </div>
  );
};

export default ChatApp;

2. Form Validations In React, the Observer pattern is so seamlessly integrated that its presence can often go unnoticed. Form validations, for example, vividly embody this pattern – as users input erroneous data, error messages instantaneously emerge, showcasing the Observer pattern in action.

jsx
import React, { useState } from 'react';

const FormValidationApp = () => {
  const { formData, errors, handleChange, handleSubmit } = useLoginForm();

  return (
    <div>
      <h1>Form Validation Example</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          name="email"
          value={formData.email}
          onChange={handleChange}
          placeholder="Email"
        />
        {errors.email && <div className="error-message">{errors.email}</div>}
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
          placeholder="Password"
        />
        {errors.password && <div className="error-message">{errors.password}</div>}
        <button type="submit">Submit</button>
      </form>

    </div>
  );
};

export default FormValidationApp;

Composite pattern

The Composite pattern is often used to compose objects into tree structures to represent part-whole hierarchies. In the context of React, this pattern can be applied to build complex UIs from smaller, reusable components. Let's consider an example where we create a nested list component using the Composite pattern:

jsx
import React from 'react';

// Leaf Component
const ListItem = ({ text }) => <li>{text}</li>;

// Composite Component
const NestedList = ({ items }) => (
  <ul>
    {items.map((item, index) => (
      <ListItem key={index} text={item.text} />
    ))}
  </ul>
);

const App = () => {
  const items = [
    { text: 'Item 1' },
    { text: 'Item 2' },
    {
      text: 'Item 3',
      children: [
        { text: 'Subitem 1' },
        { text: 'Subitem 2' },
      ],
    },
  ];

  return (
    <div>
      <h1>Nested List Example</h1>
      <NestedList items={items} />
    </div>
  );
};

export default App;

Decorator pattern

The Decorator pattern is used to dynamically add responsibilities or behaviors to objects without altering their structure. Here's a real-world scenario where the Decorator pattern can be applied:

1. Adding Logging to Functionality Imagine you have a front-end application that interacts with an API to fetch and display data. You want to add logging functionality to keep track of when data is fetched and displayed. Instead of modifying every API call or data display function, you can apply the Decorator pattern.

jsx
// Original API function
const fetchData = async (url) => {
  const response = await fetch(url);
  const data = await response.json();
  return data;
};

// Decorator for adding logging
const withLogging = (func) => {
  return async (...args) => {
    const result = await func(...args);
    console.log(`Function ${func.name} executed at ${new Date()}`);
    return result;
  };
};

// Apply the decorator to the API function
const fetchDataWithLogging = withLogging(fetchData);

// Using the decorated function
const App = () => {
  const data = fetchDataWithLogging('https://api.example.com/data');

  // ... render data ...
};

export default App;

2. Adding an "Animate on Scroll" animation to a component

jsx
import React from 'react';

// Original Button component
const Button = ({ label }) => <button className="button">{label}</button>;

// Decorator for adding "Animate on Scroll" behavior
const withAnimateOnScroll = (Component) => {
  return (props) => (
    <div className="animate-on-scroll">
      <Component {...props} />
    </div>
  );
};

// Apply the decorator to the Button component
const AnimatedButton = withAnimateOnScroll(Button);

// Using the decorated button
const App = () => {
  return (
    <div>
      <AnimatedButton label="Animated Button" />
    </div>
  );
};

export default App;

3. Adding Authorization

jsx
import React, { createContext, useContext } from 'react';

// Create a context for user roles
const UserContext = createContext();

// Provider component to provide user roles
const UserProvider = ({ children }) => {
  const userRoles = ['admin', 'user']; // Example user roles
  return <UserContext.Provider value={userRoles}>{children}</UserContext.Provider>;
};

// Original Component
const FeatureComponent = () => <div>This is a feature component.</div>;

// Decorator for adding authorization based on context
const withAuthorization = (Component, requiredRoles) => {
  return (props) => {
    const userRoles = useContext(UserContext);
    const isAuthorized = requiredRoles.some(role => userRoles.includes(role));

    if (isAuthorized) {
      return <Component {...props} />;
    } else {
      return <div>You are not authorized to view this component.</div>;
    }
  };
};

// Apply the decorator to the FeatureComponent
const AuthorizedFeature = withAuthorization(FeatureComponent, ['admin']);

// Using the decorated component within UserProvider
const App = () => {
  return (
    <UserProvider>
      <div>
        <FeatureComponent />
        <AuthorizedFeature />
      </div>
    </UserProvider>
  );
};

export default App;

Strategy pattern

The Strategy pattern is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each algorithm, and make them interchangeable. Each algorithm can be selected at runtime based on the specific requirements.

Examples:

1. Payment Methods in an E-commerce App Imagine you're building an e-commerce application that allows users to select different payment methods during checkout. You want to provide flexibility in handling various payment methods, such as credit card, PayPal, and mobile wallets. Instead of hardcoding each payment method's logic into the checkout process, you can use the Strategy pattern to encapsulate each payment method's behavior and dynamically switch between them.

jsx
// Payment strategy implementations
const creditCardPayment = (amount) => {
  // Logic for credit card payment
  console.log(`Processing credit card payment of $${amount}`);
};

const paypalPayment = (amount) => {
  // Logic for PayPal payment
  console.log(`Processing PayPal payment of $${amount}`);
};

const walletPayment = (amount) => {
  // Logic for mobile wallet payment
  console.log(`Processing mobile wallet payment of $${amount}`);
};

// Context using the Strategy pattern
const PaymentProcessor = ({ paymentStrategy, amount }) => {
  paymentStrategy(amount);
  return <div>Payment successful!</div>;
};

// Using different payment strategies
const App = () => {
  const orderAmount = 100;

  return (
    <div>
      <h1>Checkout</h1>

      <h2>Credit Card Payment</h2>
      <PaymentProcessor paymentStrategy={creditCardPayment} amount={orderAmount} />

      <h2>PayPal Payment</h2>
      <PaymentProcessor paymentStrategy={paypalPayment} amount={orderAmount} />

      <h2>Mobile Wallet Payment</h2>
      <PaymentProcessor paymentStrategy={walletPayment} amount={orderAmount} />
    </div>
  );
};

export default App;

2. Delivery Pricing Strategies

jsx
// Pricing strategy implementations
const standardDelivery = (distance) => {
  return distance * 2; // Standard price per kilometer
};

const expressDelivery = (distance) => {
  return distance * 3; // Higher price for express delivery
};

const premiumDelivery = (distance) => {
  return distance * 5; // Even higher price for premium delivery
};

// Context using the Strategy pattern
const DeliveryPriceCalculator = ({ pricingStrategy, distance }) => {
  const price = pricingStrategy(distance);
  return <div>Delivery cost: ${price}</div>;
};

// Using different pricing strategies
const App = () => {
  const deliveryDistance = 10; // Distance in kilometers

  return (
    <div>
      <h1>Delivery Pricing</h1>

      <h2>Standard Delivery</h2>
      <DeliveryPriceCalculator pricingStrategy={standardDelivery} distance={deliveryDistance} />

      <h2>Express Delivery</h2>
      <DeliveryPriceCalculator pricingStrategy={expressDelivery} distance={deliveryDistance} />

      <h2>Premium Delivery</h2>
      <DeliveryPriceCalculator pricingStrategy={premiumDelivery} distance={deliveryDistance} />
    </div>
  );
};

export default App;

Iterator pattern

Provides a way to access elements of a collection sequentially without exposing its underlying representation. Useful for traversing lists or collections of data.

Paginated Data Display In a front-end application, you often deal with large datasets that need to be displayed in a paginated manner. The Iterator pattern can help manage and iterate through paginated data efficiently, separating the data retrieval and iteration logic from the display logic.

jsx
import React, { useState, useRef, useEffect } from 'react';

// Iterator implementation for on-scroll pagination
const createIterator = (data, pageSize) => {
  let currentPage = 1;

  const nextPage = () => {
    currentPage++;
    return currentPage;
  };

  const hasNextPage = () => {
    return currentPage * pageSize < data.length;
  };

  const getCurrentPageData = () => {
    const startIndex = (currentPage - 1) * pageSize;
    const endIndex = startIndex + pageSize;
    return data.slice(startIndex, endIndex);
  };

  return {
    nextPage,
    hasNextPage,
    getCurrentPageData,
  };
};

// Component using the Iterator pattern for on-scroll pagination
const OnScrollPagination = ({ items, pageSize }) => {
  const iterator = useRef(createIterator(items, pageSize));
  const [currentPageData, setCurrentPageData] = useState(iterator.current.getCurrentPageData());

  const loadMoreOnScroll = () => {
    if (iterator.current.hasNextPage()) {
      iterator.current.nextPage();
      setCurrentPageData((prevData) => [...prevData, ...iterator.current.getCurrentPageData()]);
    }
  };

  useEffect(() => {
    window.addEventListener('scroll', loadMoreOnScroll);
    return () => {
      window.removeEventListener('scroll', loadMoreOnScroll);
    };
  }, []);

  return (
    <div>
      <ul>
        {currentPageData.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
      {iterator.current.hasNextPage() && (
        <p>Loading more...</p>
      )}
    </div>
  );
};

// Usage
const App = () => {
  const data = Array.from({ length: 20 }, (_, index) => `Item ${index + 1}`);
  const pageSize = 5;

  return (
    <div>
      <h1>On-Scroll Pagination</h1>
      <OnScrollPagination items={data} pageSize={pageSize} />
    </div>
  );
};

export default App;

Command pattern

Encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and undo operations.

Examples:

1. Floor Planner Imagine a floor planning application where users design layouts by placing and moving furniture items. To achieve undo and redo functionality, the Command pattern can be implemented. Users can perform actions and seamlessly navigate through layout changes.

jsx
import React, { useState } from 'react';

const FurnitureItem = ({ id, x, y, name, onSelect }) => (
  <div
    className="furniture-item"
    style={{ top: `${y}px`, left: `${x}px` }}
    onClick={() => onSelect(id)}
  >
    {name}
  </div>
);

const createMoveCommand = (itemId, newX, newY) => ({
  execute: (currentState) => ({
    ...currentState,
    furniture: currentState.furniture.map((item) =>
      item.id === itemId ? { ...item, x: newX, y: newY } : item
    ),
  }),
  undo: (currentState) => currentState.history.pop(),
});

const FloorPlanner = () => {
  const initialFurniture = [
    { id: 1, x: 50, y: 100, name: 'Sofa' },
    { id: 2, x: 200, y: 150, name: 'Table' },
    // ... other furniture items ...
  ];

  const [currentState, setCurrentState] = useState({
    furniture: initialFurniture,
    history: [],
  });

  const executeCommand = (command) =>
    setCurrentState((prev) => ({
      ...command.execute(prev),
      history: [...prev.history, prev],
    }));

  const selectFurnitureItem = (itemId) => {
    // Highlight the selected furniture item
  };

  return (
    <div className="floor-planner">
      {currentState.furniture.map((item) => (
        <FurnitureItem key={item.id} {...item} onSelect={selectFurnitureItem} />
      ))}
    </div>
  );
};

export default FloorPlanner;

2. Text Editor Consider a simple text editor application where users can perform basic text formatting actions such as bold, italic, and underline. The Command pattern can be used to implement these formatting commands, allowing users to apply and undo formatting changes.

jsx
import React, { useState } from 'react';

const TextEditor = () => {
  const [content, setContent] = useState('');
  const [history, setHistory] = useState([]);
  const [selectedText, setSelectedText] = useState('');

  const executeCommand = (command) => {
    setHistory([...history, content]);
    setContent(command.execute(content, selectedText));
  };

  const undo = () => {
    if (history.length > 0) {
      setContent(history.pop());
    }
  };

  const formatText = (format) => {
    const formatCommand = {
      bold: {
        execute: (text) => `**${text}**`,
      },
      italic: {
        execute: (text) => `_${text}_`,
      },
      underline: {
        execute: (text) => `__${text}__`,
      },
    };

    executeCommand(formatCommand[format]);
  };

  return (
    <div className="text-editor">
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        onSelect={(e) => setSelectedText(e.target.value.substring(e.target.selectionStart, e.target.selectionEnd))}
      />
      <div className="format-buttons">
        <button onClick={() => formatText('bold')}>Bold</button>
        <button onClick={() => formatText('italic')}>Italic</button>
        <button onClick={() => formatText('underline')}>Underline</button>
        <button onClick={undo}>Undo</button>
      </div>
    </div>
  );
};

export default TextEditor;

Proxy pattern

Provides a surrogate or placeholder for another object to control access to it. Useful for implementing lazy loading, access control, and caching.

1. Proxy Pattern for Image Loading Consider a web application that displays high-resolution images, which might impact performance. The Proxy pattern can be used to create a proxy object that loads and displays images only when they are actually viewed, helping to optimize the page loading time.

jsx
import React, { useState } from 'react';

const RealImage = ({ src, alt }) => (
  <img src={src} alt={alt} />
);

const ImageProxy = ({ src, alt }) => {
  const [loading, setLoading] = useState(true);
  const [imageLoaded, setImageLoaded] = useState(false);

  const handleImageLoad = () => {
    setLoading(false);
    setImageLoaded(true);
  };

  return (
    <div className="image-proxy">
      {loading && <div className="loading-indicator">Loading...</div>}
      {!loading && !imageLoaded && (
        <img
          src={src}
          alt={alt}
          style={{ display: 'none' }}
          onLoad={handleImageLoad}
        />
      )}
      {imageLoaded && <RealImage src={src} alt={alt} />}
    </div>
  );
};

const App = () => {
  return (
    <div className="image-gallery">
      <ImageProxy src="high_res_image_1.jpg" alt="Image 1" />
      <ImageProxy src="high_res_image_2.jpg" alt="Image 2" />
      <ImageProxy src="high_res_image_3.jpg" alt="Image 3" />
    </div>
  );
};

export default App;

Summary

Explore a selection of design patterns and their real-world applications in front-end development. Discover how these patterns simplify complex tasks, improve code organization, and enhance user experiences. Dive into common scenarios where design patterns shine, making your coding journey more efficient and effective.

Singleton Pattern -Authentication Management -Theme Management -Global State Management -Managing User Preferences -WebSocket Connection Manager

Factory Pattern -Creating UI Components with Different Variants -Building Complex Layouts with Dynamic Composition -Generating Dynamic Forms -Handling Multiple Data Sources for Widgets -Creating Customized Alerts and Notifications

Facade Pattern -Managing API Requests to Different Endpoints -Integrating Third-Party Libraries with Unified Interfaces -Simplifying Complex State Management Logic -Abstracting Complex Animation Sequences -Providing a Unified API for Device Sensors and Capabilities

Observer Pattern -Real-Time Chat Applications -Real-Time Notifications -Event Handling and Listeners -Form Validations and Error Display -Real-Time Collaboration Tools

Composite Pattern -Creating Nested Layouts with Widgets -Building Tree Structures for Navigation Menus -Hierarchical Organization Charts -Crafting Complex UI Elements from Smaller Components -Building Interactive Family Trees

Decorator Pattern -Adding Animations to UI Components -Enhancing User Interactions with Additional Features -Implementing Theming and Styling Enhancements -Extending Data Visualization Components -Improving Accessibility Features of UI Elements -Access Control for User Permissions

Strategy Pattern -Selecting Payment Gateway for E-Commerce Checkout -Media Handling Strategies for Different Screen Resolutions -Switching Between Different Charting Libraries -Optimizing Image Compression Strategies -Applying Different Sorting Algorithms

Iterator Pattern -Paginating and Loading Large Lists of Data -Implementing Carousel and Slideshow Components -Traversing and Manipulating Data Structures -Implementing Multi-Step Wizards and Onboarding Flows -Handling Slide Navigation in Presentation Tools

Chain of Responsibility Pattern -Event Propagation and Handling in Complex UIs -User Input Handling in Interactive Games -Form Validation with Multiple Validation Rules -Implementing Middleware in API Request Pipelines -Implementing Multi-Level Menus and Navigation Systems

Proxy Pattern -Access Control for User Permissions -Caching API Responses for Performance -Lazy Loading of Heavy Resources -Throttling and Rate Limiting for API Requests -Implementing Authentication and Authorization Checks

Conclusion

As we reach the end of our journey through these common design patterns in front-end development, we uncover a world of structured solutions that can significantly elevate our coding prowess. These patterns offer us more than just strategies; they provide a framework for thinking and problem-solving, fostering consistency and elegance in our code. By integrating these patterns into our toolbox, we empower ourselves to write cleaner, more maintainable code and to communicate more effectively with fellow developers.

In a field where innovation thrives, design patterns stand as timeless principles that guide us through the complexities of software development. By recognizing the recurring challenges they address and the solutions they offer, we gain insights that transcend specific projects, enriching our understanding of software architecture. Armed with this knowledge, we're ready to embark on new coding adventures, armed with a repertoire of techniques that can transform our ideas into reality.

Feel free to let me know if you'd like any further adjustments or if you're ready to move on to the next steps!

TAGS:

#Design Patterns

#Front-End Development

#Software Architecture

#React

#Coding Best Practices

go to article list