React Patterns: Context API, A Practical Example of the Publisher-Subscriber Pattern and Dependency Inversion Principle

admin  

In this post we are going to create a simple React application with React Context to showcase the use of the Dependency Inversion Principle (DIP). React Context is a React feature for managing state globally across a React app. It's particularly useful for sharing data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language.

What is React Context?

React Context is a React feature for managing state globally across a React app. It's particularly useful for sharing data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language.

  1. Context Creation: React.createContext() is invoked to create a Context object for a specific data theme (e.g., UserContext for user data).

  2. Context Provider: The Context object includes a Provider component that allows consuming components to subscribe to context changes. You wrap the Provider around the parent component of those that need access to the context data. The Provider takes a value prop to pass the data down the component tree.

  3. Consuming Context: To consume the context, components can use the Context.Consumer component or the useContext hook for functional components.

  4. Updating Context: While Context provides a way to pass data down the component tree without having to pass props manually at every level, changing the context data usually requires using a combination of Context and state management using useState or useReducer.

React Context is ideal for passing data that needs to be accessed by many components at different nesting levels, or when some data needs to be updated across many components at once, but overusing Context can lead to performance issues in large applications, as it can cause unnecessary re-renders if not implemented carefully.

Let's create a simple React application that demonstrates the use of React Context. I has the following components:

App: This is the root component that uses UserContextProvider to wrap other components.

UserContext: This is the context itself, created using React.createContext(). It provides a way to share the user data across components.

UserContextProvider: This component provides the UserContext to its children.

HighLevelComponent and LowLevelComponent: These components will consume the context provided by UserContextProvider, demonstrating how different levels of components can depend on the same context. This is a direct application of the depedency inversion pattern.

We need to create a simple create app then add the following code:

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

// Create a Context for user data
const UserContext = createContext(null);

// Context Provider component
function UserContextProvider({ children }) {
    const [user, setUser] = useState({ name: "John Doe", role: "User" });

    // The value that will be provided to descendants
    const contextValue = { user, setUser };

    return (
        <UserContext.Provider value={contextValue}>
            {children}
        </UserContext.Provider>
    );
}

// A high-level component that consumes the context
function HighLevelComponent() {
    const { user } = useContext(UserContext);

    return <div>High Level: Welcome, {user.name}!</div>;
}

// A low-level component that also consumes the context
function LowLevelComponent() {
    const { user } = useContext(UserContext);

    return <div>Low Level: Role - {user.role}</div>;
}

// App component
function App() {
    return (
        <UserContextProvider>
            <HighLevelComponent />
            <LowLevelComponent />
        </UserContextProvider>
    );
}

export default App;

Let's take a look on the following class diagram:

In the following sequence diagram shows the flow of creating and providing context, as well as how it's consumed by different components in the application. It highlights the decoupling of components from each other, as they all depend on the shared UserContext instead of directly on parent or sibling components:

The flow as shown in the diagram:

  1. App: Initiates the application and wraps UserContextProvider around the components that need access to the context.
  2. UserContextProvider: Creates the UserContext and provides it to its children components.
  3. UserContext: The context that holds the shared state.
  4. HighLevelComponent and LowLevelComponent: These components consume the UserContext to access the shared state.

Behind the scene, the React Context API implements the Publisher-Subscriber pattern, which is a variant of the Observer design pattern. This pattern is well-suited for React's component-based architecture and is effectively utilized in the Context API to manage and propagate state and data through a component tree.

The Publisher-Subscriber Pattern consists of 2 components:

  • Publisher (Provider): In the Context API, the Provider component acts as the publisher. It holds the state or data that needs to be distributed across various components in the application. When the state or data in the Provider changes, it notifies all of its subscribers (consumers).
  • Subscribers (Consumers): Components that need access to the Context data are the subscribers. They "subscribe" to the Context by using the useContext hook or the Consumer component. When the data in the Context changes, all subscriber components automatically receive the updated data.

The Context API is event-driven. When the data within a Provider changes, this triggers a re-render of the components that are subscribed to that particular context, ensuring they always have the most up-to-date data.

Context API decouples the Provider component from its consuming components. Consumers don't need to know where the data comes from or how it's managed. They just need to know how to subscribe to it, simplifying component relationships and improving modularity.

By implementing the Publisher-Subscriber pattern, React enables applications to manage global state, sharing data across multiple components, and avoiding prop drilling and to pass data through multiple layers of components not directly interested in the data triggering the changes only of the ones which subscribe to get the updates.