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:
-
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.
-
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:
- Separation of responsibilities: Each country has its own strategy for rendering data, making the code more modular and easier to understand.
- Reduction of duplicated code: We don't need to replicate the table logic for each country, as the differences are abstracted into specific strategies.
- Scalability: New countries or changes to existing ones can be added easily without impacting the codebase.
- 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);
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[],
},
};
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}
/>
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}
/>
</>
);
}
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
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.
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! :)