DEV Community

Douglas Pujol
Douglas Pujol

Posted on

How to Create Global and Dynamic Components with Strategy Pattern in React

We had an interesting challenge in our team: how to develop components globally, where the data and behaviors would vary by country? In one of the discussions with my colleagues, André Peixoto and Guilherme Cardoso, several solutions emerged, and today I’ll share one that perfectly suited our context.

In our case, the first component we implemented with this approach was a dynamic table. This component needed to display columns and data that varied depending on the country, with specific display rules, interactions, and data handling.

The Challenge: A Dynamic Table for Each Country

Imagine having a table that needs to render different types of data for each country, varying the number of columns, data formats, and even user interactions with it. How can we solve this?
You might consider a few options, and let’s discuss some of them:

  1. Create a table for each country. This approach could work but would bring several problems:

    • Code duplication: Each table for every country would lead to duplicated logic, increasing maintenance effort.
    • Difficulty in synchronizing changes: If a bug is found, you’d need to fix it in multiple places.
    • Scalability: As new countries are added, this solution would become unsustainable.
  2. Create a single table with conditionals. Another approach could be to develop a single table with various conditionals to render each type of cell or header based on the country. However, this also presents issues:

    • Complex code: As the table grows, the number of conditionals increases, making the code hard to understand and maintain.
    • Tight coupling: The code would become highly coupled, complicating the addition of new functionalities.
    • Difficult maintenance: Maintenance becomes a nightmare when the project grows and needs adjustments for new scenarios.

Benefits of the Strategy Pattern

A cleaner and more scalable approach we adopted was using the Strategy Pattern. But what is the Strategy Pattern?

The Strategy Pattern is a behavioral design pattern that defines a family of algorithms and encapsulates them so they can be interchangeable. In other words, you can switch between different strategies without modifying the client that uses them.

Benefits of the Strategy Pattern

Implementing a component using the Strategy Pattern brings several important advantages:

  1. Separation of responsibilities: Each country has its own strategy for rendering data, making the code more modular and easier to understand.
  2. Reduction of duplicated code: We don't need to replicate the table logic for each country, as the differences are abstracted into specific strategies.
  3. Scalability: New countries or changes to existing ones can be added easily without impacting the codebase.
  4. Ease of maintenance: If there’s a bug or a new rule for a country, we just need to adjust its specific strategy without affecting the rest of the application.

Downsides of the Strategy Pattern

While the Strategy Pattern is extremely useful, it is not without its disadvantages. One important consideration is the need to configure multiple separate strategies, which can lead to some degree of fragmentation. In larger systems, it may be necessary to ensure that strategies are maintained in an organized and cohesive manner.

Implementation: Our Dynamic Table

Now, let’s see how the table was implemented in practice using the Strategy Pattern.

First, we created a Table component that receives the data and the specific column strategy for rendering:

import React from 'react';

export interface Column<T> {
  key: string;
  label: string;
  render: (row: T) => JSX.Element;
}

export interface TableProps<T = unknown>  {
  data?: T[];
  strategy?: {
    columns: Column<T>[];
  } | unknown ;
}

const Table =<T,>({ data, strategy }: TableProps<T>) => {

  const defaultStrategy = { columns: [] as Column<T>[] };
  const currentStrategy = strategy ? (strategy as { columns: Column<T>[] }) : defaultStrategy;

  return (
    <table>
      <thead>
        <tr>
          {currentStrategy?.columns?.map((col) => (
            <th key={col.key} className="px-4 py-2 border border-dashed border-border">
              {col.label}
            </th>
          ))}
        </tr>
      </thead>
      <tbody>
        {currentStrategy && data && data.length > 0  ? data?.map((row, index) => (
          <tr key={index}
            className="hover:bg-hoverTable"
          >
            {currentStrategy?.columns?.map((col) => col.render(row))}
          </tr>
        )) : (
          <tr>
            <td colSpan={currentStrategy?.columns?.length || 1} className="px-4 py-2 text-center">
              No data available
            </td>
          </tr>
        )}
      </tbody>
    </table>
  );
};

export default  React.memo(Table);

Enter fullscreen mode Exit fullscreen mode

With this basic structure, we ensured that the columns and content are rendered dynamically based on the specific strategy for the country. For each country, we defined a different strategy, as shown in the example below:

export const tableStrategies = {
  BR: {
    columns: [
      {
        key: 'number',
        label: 'Número',
        render: (row: BrazilData): JSX.Element => (
          <td key={`${row.number}-br`} className="px-4 py-2 border border-dashed border-border">{row.number}</td>
        ),
      },
      {
        key: 'name',
        label: 'Nome',
        render: (row: BrazilData): JSX.Element => (
          <td key={`${row.number}-name`} className="px-4 py-2 border border-dashed border-border">{row.name}</td>
        ),
      },
      // ...other columns
    ] as BrazilColumn[],
  },
  ES: {
    columns: [
      {
        key: 'shirtNumber',
        label: 'Número de Camiseta',
        render: (row: SpainData): JSX.Element => (
          <td key={`${row.shirtNumber}-es`} className="px-4 py-2 border border-dashed border-border">{row.shirtNumber}</td>
        ),
      },
      {
        key: 'name',
        label: 'Nombre',
        render: (row: SpainData): JSX.Element => (
          <td key={`${row.name}-name`} className="px-4 py-2 border border-dashed border-border">{row.name}</td>
        ),
      },
      // ...other columns
    ] as SpainColumn[],
  },
};

Enter fullscreen mode Exit fullscreen mode

Moreover, using the render method within the context of the Strategy Pattern adds simplicity. Each table column has a render, function that determines how the cell will be displayed for each specific type of data. This provides great flexibility and clarity for customizing the display without unnecessary complexity.

Now, simply call the Table component with the appropriate strategy and data for each country:

<Table
  strategy={tableStrategies.BR}
  data={brazilData}
/>
Enter fullscreen mode Exit fullscreen mode

A Simple Example with Country Selection

For educational purposes, I created a simulation in the project where users can select different countries through a select dropdown. Depending on the chosen country, the data and the table rendering strategy are updated dynamically:

import React from 'react';
import {  CountryCode } from './types';
import  Table  from './components/Table';
import { americanData, brazilData, southKoreaData, spainData } from './mocks';
import { tableStrategies } from './strategies';

export default function Home() {

  const [strategy, setStrategy] = React.useState<unknown>({});
  const [selectedCountry, setSelectedCountry] = React.useState<CountryCode>('BR');
  const [data, setData] = React.useState<unknown[]>([null])

  const updateDataAndStrategy =React.useCallback((country: CountryCode) => {
    switch (country) {
      case 'BR':
        setData(brazilData);
        setStrategy(tableStrategies[country]);
        break;
      case 'US':
        setData(americanData);
        setStrategy(tableStrategies[country]);
        break;
      case 'ES':
        setData(spainData);
        setStrategy(tableStrategies[country]);
        break;
      case 'KR':
        setData(southKoreaData);
        setStrategy(tableStrategies[country]);
        break;
      default:
        setData([]);
        setStrategy({});
        break;
    }
  }, []);

  const handleChanged = (event: React.ChangeEvent<HTMLSelectElement>) => {
    const selected = event.target.value as CountryCode;
    setSelectedCountry(selected);
    updateDataAndStrategy(selected);
  };

  return (
    <>
      <h1>## Table</h1>
      <select onChange={handleChanged} value={selectedCountry} className="mb-2">
        <option value="US">United States</option>
        <option value="AR">Argentina</option>
        <option value="BR">Brazil</option>
        <option value="KR">South Korea</option>
        <option value="ES">Spain</option>
      </select>

      <Table
        strategy={strategy}
        data={data}
       />
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

In this example, the select component allows the user to choose the desired country, and the table automatically updates to display the specific data and rendering strategy for that country. Check out the project link

Completed table from my project on GitHub

Switching Between Countries

With the country selector, we can easily switch between different strategies, simulating the custom rendering of the table according to the specific configurations of each country. This approach allows us to visualize, in real time, how the content adapts to different regions and contexts.

Photo of my table on GitHub, I am switching countries to demonstrate the strategy pattern

Conclusion

With this approach using the Strategy Pattern, we ensure cleaner, more modular, and scalable code. Additionally, if in the future we need to add new countries or adjust the rules for an existing country, we can do so easily without impacting the rest of the system.

I developed a project on GitHub that contains all this code and a practical example of how to apply this technique.

If you want to see the complete code, check out the repository on
GitHub.

Thanks, folks! :)

Top comments (0)