Flexible and Maintainable React / React Native Applications with the Decorator Pattern

K Samman
7 min readFeb 27, 2023

--

https://refactoring.guru/design-patterns/decorator

In the world of software development, there are many design patterns that developers can use to build better and more maintainable applications. One such pattern is the Decorator Design Pattern. In this article, we will explore what the Decorator pattern is, why and when it should be used, and how it can be implemented in a React Native application using TypeScript.

We will also provide real-life examples that illustrate how the Decorator pattern can be used to solve common problems when working with backend APIs.

What is the Decorator Design Pattern? The Decorator pattern is a structural design pattern that allows developers to add new behavior to an existing object dynamically without changing its original structure. This pattern is commonly used in situations where developers want to add new functionality or behavior to a class without modifying its existing code. In essence, the Decorator pattern lets developers wrap an object with one or more decorators that add new features or modify existing ones.

When should you use decorator pattern

There are several reasons why a developer might choose to use the Decorator pattern.

  • Extending functionality without modifying existing code: When you need to add new functionality to an existing object without changing its existing code, the Decorator pattern can be a good solution. You can wrap the object in one or more Decorator objects that add the desired behavior, without affecting the original object.
  • Creating flexible and reusable code: The Decorator pattern allows you to create a flexible and reusable code structure. You can add or remove Decorator objects at runtime, which makes it easy to modify the behavior of an object as needed.
  • Implementing open-closed principle: The Decorator pattern supports the open-closed principle, which states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. The Decorator pattern allows you to extend the functionality of an object without modifying its code, which helps maintain code stability and minimize the risk of introducing new bugs.
  • Supporting composition over inheritance: The Decorator pattern supports composition over inheritance, which means that it’s often a better choice to add new behavior by composing objects rather than inheriting behavior from a base class. With the Decorator pattern, you can easily compose objects to create complex behavior.

Usage of Decorator Pattern in React Native

To illustrate how the Decorator pattern can be used in a React Native application, I will provide two real-life examples.

Using the Decorator Pattern to Add New Behavior to a Component

https://refactoring.guru/design-patterns/decorator

we will use the Decorator pattern to add new behavior to a React Native component. The component we will use is a simple list of products fetched from a backend API. We will create a decorator that adds new formatting behavior to the component.

import React, { FC } from 'react';
import { View, Text } from 'react-native';

// Define the Component interface
interface Component {
render: () => JSX.Element;
}

// Define a concrete component
const ConcreteComponent: FC = () => {
return (
<View>
<Text>Concrete Component</Text>
</View>
);
};

// Define a base decorator class that implements the Component interface
abstract class Decorator implements Component {
protected component: Component;

constructor(component: Component) {
this.component = component;
}

abstract render(): JSX.Element;
}

// Define a concrete decorator that adds new behavior
class ConcreteDecorator extends Decorator {
render(): JSX.Element {
const component = this.component.render();
return (
<View>
<Text>Decorator Text</Text>
{component}
</View>
);
}
}

// Usage
const concreteComponent: Component = { render: ConcreteComponent };
const decoratedComponent: Component = new ConcreteDecorator(concreteComponent);

// Render the decorated component
decoratedComponent.render();

In the code example above, we start by defining the Component interface as a class. We then define a concrete component called ConcreteComponent that implements this interface.

Next, we define an abstract class called Decorator that implements the Component interface and has a reference to a Component object. This class acts as a base class for all decorators.

We then define a concrete decorator called ConcreteDecorator that extends the Decorator class and adds new behavior to the render() method.

Finally, we create an instance of the ConcreteComponent class and pass it to the ConcreteDecorator constructor to create a decorated component. We can then call the render() method on the decorated component to see the new behavior added by the decorator.

Below is the tests for above example.

//Tests

import { FC } from 'react';

describe('Decorator Pattern - Example 1', () => {
interface Component {
render: () => JSX.Element;
}

const ConcreteComponent: FC = () => {
return <div>Concrete Component</div>;
};

abstract class Decorator implements Component {
protected component: Component;

constructor(component: Component) {
this.component = component;
}

abstract render(): JSX.Element;
}

class ConcreteDecorator extends Decorator {
render(): JSX.Element {
const component = this.component.render();
return (
<>
<div>Decorator Text</div>
{component}
</>
);
}
}

const concreteComponent: Component = { render: ConcreteComponent };
const decoratedComponent: Component = new ConcreteDecorator(concreteComponent);

it('renders ConcreteComponent with text', () => {
expect(concreteComponent.render()).toEqual(<div>Concrete Component</div>);
});

it('renders ConcreteDecorator with text', () => {
expect(decoratedComponent.render()).toEqual(
<>
<div>Decorator Text</div>
<div>Concrete Component</div>
</>,
);
});
});

Using the Decorator Pattern to Format Data from a Backend API

Imagine that you are building a mobile app that interacts with a backend API to fetch and display data. You have a set of components that display different types of data, such as a list of products, a list of orders, and a list of customers. Each of these components needs to fetch data from the API and display it.

One issue that you might encounter is that the data returned by the API may not always be in the format that your components expect. For example, the API may return data in a different structure or with different field names than what your components are designed to handle. This can lead to errors in your app or require you to write custom parsing code in each component.

To solve this problem, you can use the Decorator pattern to create a set of data formatting decorators that can be applied to each component. Each decorator would be responsible for formatting the data in a specific way, such as renaming fields or restructuring the data.

import React, { Component } from 'react';
import { View, Text } from 'react-native';

interface Product {
id: number;
name: string;
price: number;
}

interface ProductListComponentProps {
products: Product[];
}

class ProductListComponent extends Component<{}, ProductListComponentProps> {
state = {
products: [],
};

async componentDidMount() {
const products = await fetch('/api/products');
this.setState({ products });
}

render() {
const { products } = this.props;
return (
<View>
<Text>Product List</Text>
{products.map((product: Product) => (
<View key={product.id}>
<Text>{product.name}</Text>
<Text>{product.price}</Text>
</View>
))}
</View>
);
}
}

type ProductDataFormatter = (component: Component) => Component;

const ProductDataFormatter: ProductDataFormatter = (component) => {
return class extends Component {
state = {
products: [],
};

async componentDidMount() {
const products = await fetch('/api/products');
const formattedProducts = formatProductData(products); // Format the data using a helper function
this.setState({ products: formattedProducts });
}

render() {
const { products } = this.state;
return React.createElement(component, { products });
}
};
};

const DecoratedProductListComponent = ProductDataFormatter(ProductListComponent);

<DecoratedProductListComponent />;
//Tests
import { Component } from 'react-native';

describe('Decorator Pattern - Example 2', () => {
interface Product {
id: number;
name: string;
price: number;
}

interface ProductListComponentProps {
products: Product[];
}

class ProductListComponent extends Component<{}, ProductListComponentProps> {
state = {
products: [],
};

async componentDidMount() {
const products = await fetch('/api/products');
const json = await products.json();
this.setState({ products: json });
}

render() {
const { products } = this.props;
return (
<>
<Text>Product List</Text>
{products.map((product: Product) => (
<View key={product.id}>
<Text>{product.name}</Text>
<Text>{product.price}</Text>
</View>
))}
</>
);
}
}

type ProductDataFormatter = (component: Component) => Component;

const formatProductData = (products: Product[]): Product[] => {
return products.map((product) => {
return {
...product,
name: product.name.toUpperCase(),
price: product.price.toFixed(2),
};
});
};

const ProductDataFormatter: ProductDataFormatter = (component) => {
return class extends Component {
state = {
products: [],
};

async componentDidMount() {
const products = await fetch('/api/products');
const json = await products.json();
const formattedProducts = formatProductData(json);
this.setState({ products: formattedProducts });
}

render() {
const { products } = this.state;
return React.createElement(component, { products });
}
};
};

const DecoratedProductListComponent = ProductDataFormatter(ProductListComponent);

it('renders ProductListComponent with unformatted data', () => {
const wrapper = shallow(<ProductListComponent products={[{ id: 1, name: 'Product 1', price: 10.99 }]} />);
expect(wrapper.find('Text').at(0).prop('children')).toBe('Product List');
expect(wrapper.find('Text').at(1).prop('children')).toBe('Product 1');
expect(wrapper.find('Text').at(2).prop('children')).toBe(10.99);
});

it('renders DecoratedProductListComponent with formatted data', async () => {
const wrapper = shallow(<DecoratedProductListComponent />);
await wrapper.instance().componentDidMount();
wrapper.update();
expect(wrapper.find('Text').at(0).prop('children')).toBe('Product List');
expect(wrapper.find('Text').at(1).prop('children')).toBe('PRODUCT 1');
expect(wrapper.find('Text').at(2).prop('children')).toBe('10.99');
});
});

In the code example above, we start by defining a ProductListComponent that displays a list of products fetched from the API. We then define a ProductDataFormatter decorator that formats the product data returned by the API using a helper function called formatProductData. The decorator returns a new component that passes the formatted data to the original component as a prop.

We then create a DecoratedProductListComponent by calling the ProductDataFormatter decorator and passing in the ProductListComponent as a prop. When the DecoratedProductListComponent is rendered, it fetches and formats the product data before passing it to the ProductListComponent as a prop.

The Decorator Design Pattern is a valuable tool for developers to add new functionality to an object without modifying its existing code. In React Native applications, the Decorator pattern can help simplify complex code, extend component behavior, and format data from backend APIs.

Using the Decorator pattern can make React Native applications more modular, flexible, and maintainable, ultimately making them easier to work with and extend. The article concludes by encouraging developers to use the Decorator pattern to solve common problems in React Native development.

Your feedback is important to me! If you enjoyed my article, please give it a 👏 and consider following me for more informative and insightful content.

--

--

K Samman

Tech Lead at a Leading Consultancy in United Kingdom