Template Method Pattern

The Template method design pattern is a behavioral design pattern that defines the skeleton of an algorithm in an abstract class, and lets subclasses implement some steps of the algorithm without changing its structure. The template method is a method in the abstract class that contains the invariant parts of the algorithm and calls other abstract or hook methods that are implemented by the subclasses. This way, the template method ensures that the algorithm is always followed, while allowing subclasses to customize some aspects of it.

Use cases

Some use cases for the template method design pattern are:

Here are some examples of the template method design pattern in React, TypeScript and functional components:

1. Data Fetching Component

Suppose you want to create a reusable component that fetches some data from an API and renders it in a list. However, you want to allow different components to fetch different data sources and render different list items. You can use the template method pattern to achieve this.

First, you create an abstract component that defines the template method and the abstract methods for fetching and rendering data:

// AbstractDataFetcher.tsx
import React, { useEffect, useState } from "react";
 
type Data = any; // You can use a more specific type if you want
 
// Convert the AbstractDataFetcher class component to a functional component
export function AbstractDataFetcher({
  fetchData,
  renderItem,
}: {
  fetchData: () => Promise<Data[]>;
  renderItem: (data: Data) => JSX.Element;
}) {
  // Use the useState hook to manage the data state
  const [data, setData] = useState<Data[]>([]);
 
  // Fetch data using the fetchData prop
  useEffect(() => {
    fetchData().then((data) => setData(data));
  }, [fetchData]);
 
  // Render data using the renderItem prop
  return <ul>{data.map(renderItem)}</ul>;
}
// AbstractDataFetcher.tsx
import React, { useEffect, useState } from "react";
 
type Data = any; // You can use a more specific type if you want
 
// Convert the AbstractDataFetcher class component to a functional component
export function AbstractDataFetcher({
  fetchData,
  renderItem,
}: {
  fetchData: () => Promise<Data[]>;
  renderItem: (data: Data) => JSX.Element;
}) {
  // Use the useState hook to manage the data state
  const [data, setData] = useState<Data[]>([]);
 
  // Fetch data using the fetchData prop
  useEffect(() => {
    fetchData().then((data) => setData(data));
  }, [fetchData]);
 
  // Render data using the renderItem prop
  return <ul>{data.map(renderItem)}</ul>;
}

Then, you create concrete components that inherit from the abstract component and implement the abstract methods:

// UserFetcher.tsx
import React from "react";
import { AbstractDataFetcher } from "./AbstractDataFetcher";
 
type User = {
  id: number;
  name: string;
  email: string;
};
 
// Convert the UserFetcher class component to a functional component
export function UserFetcher() {
  // Define the helper functions as regular functions
  async function fetchData(): Promise<User[]> {
    const response = await fetch("https://jsonplaceholder.typicode.com/users");
    const data = await response.json();
    return data;
  }
 
  function renderItem(user: User): JSX.Element {
    return (
      <li key={user.id}>
        {user.name} ({user.email})
      </li>
    );
  }
 
  // Return the AbstractDataFetcher component with the helper functions as props
  return <AbstractDataFetcher fetchData={fetchData} renderItem={renderItem} />;
}
// UserFetcher.tsx
import React from "react";
import { AbstractDataFetcher } from "./AbstractDataFetcher";
 
type User = {
  id: number;
  name: string;
  email: string;
};
 
// Convert the UserFetcher class component to a functional component
export function UserFetcher() {
  // Define the helper functions as regular functions
  async function fetchData(): Promise<User[]> {
    const response = await fetch("https://jsonplaceholder.typicode.com/users");
    const data = await response.json();
    return data;
  }
 
  function renderItem(user: User): JSX.Element {
    return (
      <li key={user.id}>
        {user.name} ({user.email})
      </li>
    );
  }
 
  // Return the AbstractDataFetcher component with the helper functions as props
  return <AbstractDataFetcher fetchData={fetchData} renderItem={renderItem} />;
}
// PostFetcher.tsx
import React from "react";
import { AbstractDataFetcher } from "./AbstractDataFetcher";
 
type Post = {
  id: number;
  title: string;
  body: string;
};
 
export function PostFetcher() {
  // Implement the fetchData method to fetch posts from an API
  async function fetchData(): Promise<Post[]> {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts");
    const data = await response.json();
    return data;
  }
 
  // Implement the renderItem method to render a post as a list item
  function renderItem(post: Post): JSX.Element {
    return (
      <li key={post.id}>
        <h3>{post.title}</h3>
        <p>{post.body}</p>
      </li>
    );
  }
 
  // Return the AbstractDataFetcher component with the helper functions as props
  return <AbstractDataFetcher fetchData={fetchData} renderItem={renderItem} />;
}
// PostFetcher.tsx
import React from "react";
import { AbstractDataFetcher } from "./AbstractDataFetcher";
 
type Post = {
  id: number;
  title: string;
  body: string;
};
 
export function PostFetcher() {
  // Implement the fetchData method to fetch posts from an API
  async function fetchData(): Promise<Post[]> {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts");
    const data = await response.json();
    return data;
  }
 
  // Implement the renderItem method to render a post as a list item
  function renderItem(post: Post): JSX.Element {
    return (
      <li key={post.id}>
        <h3>{post.title}</h3>
        <p>{post.body}</p>
      </li>
    );
  }
 
  // Return the AbstractDataFetcher component with the helper functions as props
  return <AbstractDataFetcher fetchData={fetchData} renderItem={renderItem} />;
}

Finally, you can use these concrete components in your app:

// App.tsx
import React from "react";
import { UserFetcher } from "./UserFetcher";
import { PostFetcher } from "./PostFetcher";
 
function App() {
  return (
    <div>
      <h1>Users</h1>
      <UserFetcher />
      <h1>Posts</h1>
      <PostFetcher />
    </div>
  );
}
 
export default App;
// App.tsx
import React from "react";
import { UserFetcher } from "./UserFetcher";
import { PostFetcher } from "./PostFetcher";
 
function App() {
  return (
    <div>
      <h1>Users</h1>
      <UserFetcher />
      <h1>Posts</h1>
      <PostFetcher />
    </div>
  );
}
 
export default App;

2. Form component:

Suppose you want to create a reusable component that renders a form with some inputs and a submit button. However, you want to allow different components to have different inputs and validation rules. You can use the template method pattern to achieve this.

First, you create an abstract component that defines the template method and the abstract methods for rendering and validating inputs:

// AbstractForm.tsx
import React, { FormEvent, useState } from "react";
 
type Input = {
  name: string;
  value: string;
};
 
// Define the props type for the abstract form component
type AbstractFormProps = {
  // The prop for getting the initial inputs
  getInitialInputs: () => Input[];
  // The prop for rendering an input label
  renderInput: (input: Input) => JSX.Element;
  // The prop for validating the inputs
  validateInputs: (inputs: Input[]) => boolean;
  // The prop for submitting the form
  submitForm: (inputs: Input[]) => void;
};
 
// Define the abstract form component as a functional component
export function AbstractForm({
  getInitialInputs,
  renderInput,
  validateInputs,
  submitForm,
}: AbstractFormProps) {
  // Use the useState hook to manage the inputs state
  const [inputs, setInputs] = useState<Input[]>(getInitialInputs());
 
  // Define the event handlers as regular functions
  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    const { name, value } = event.target;
    setInputs((inputs) =>
      inputs.map((input) => (input.name === name ? { ...input, value } : input))
    );
  }
 
  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (validateInputs(inputs)) {
      submitForm(inputs);
    } else {
      alert("Invalid inputs");
    }
  }
 
  // Return the JSX element as usual
  return (
    <form onSubmit={handleSubmit}>
      {inputs.map(renderInput).map((input) => (
        <div key={input.props.name}>
          {input}
          <input
            type='text'
            name={input.props.name}
            value={inputs.find((i) => i.name === input.props.name)?.value || ""}
            onChange={handleChange}
          />
        </div>
      ))}
      <button type='submit'>Submit</button>
    </form>
  );
}
// AbstractForm.tsx
import React, { FormEvent, useState } from "react";
 
type Input = {
  name: string;
  value: string;
};
 
// Define the props type for the abstract form component
type AbstractFormProps = {
  // The prop for getting the initial inputs
  getInitialInputs: () => Input[];
  // The prop for rendering an input label
  renderInput: (input: Input) => JSX.Element;
  // The prop for validating the inputs
  validateInputs: (inputs: Input[]) => boolean;
  // The prop for submitting the form
  submitForm: (inputs: Input[]) => void;
};
 
// Define the abstract form component as a functional component
export function AbstractForm({
  getInitialInputs,
  renderInput,
  validateInputs,
  submitForm,
}: AbstractFormProps) {
  // Use the useState hook to manage the inputs state
  const [inputs, setInputs] = useState<Input[]>(getInitialInputs());
 
  // Define the event handlers as regular functions
  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    const { name, value } = event.target;
    setInputs((inputs) =>
      inputs.map((input) => (input.name === name ? { ...input, value } : input))
    );
  }
 
  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (validateInputs(inputs)) {
      submitForm(inputs);
    } else {
      alert("Invalid inputs");
    }
  }
 
  // Return the JSX element as usual
  return (
    <form onSubmit={handleSubmit}>
      {inputs.map(renderInput).map((input) => (
        <div key={input.props.name}>
          {input}
          <input
            type='text'
            name={input.props.name}
            value={inputs.find((i) => i.name === input.props.name)?.value || ""}
            onChange={handleChange}
          />
        </div>
      ))}
      <button type='submit'>Submit</button>
    </form>
  );
}

Then, you create concrete components that inherit from the abstract component and implement the abstract methods:

// LoginForm.tsx
import React from "react";
import { AbstractForm } from "./AbstractForm";
 
type Input = {
  name: string;
  value: string;
};
 
// Define the login form component as a functional component
export function LoginForm() {
  // Define the helper functions as regular functions
  function getInitialInputs(): Input[] {
    return [
      { name: "username", value: "" },
      { name: "password", value: "" },
    ];
  }
 
  function renderInput(input: Input): JSX.Element {
    return <label htmlFor={input.name}>{input.name}</label>;
  }
 
  function validateInputs(inputs: Input[]): boolean {
    const username = inputs.find((input) => input.name === "username")?.value;
    const password = inputs.find((input) => input.name === "password")?.value;
    return username && password && username.length >= 3 && password.length >= 6;
  }
 
  function submitForm(inputs: Input[]): void {
    const username = inputs.find((input) => input.name === "username")?.value;
    const password = inputs.find((input) => input.name === "password")?.value;
    console.log(
      `Logging in with username: ${username} and password: ${password}`
    );
    // You can use any login API here
  }
 
  // Return the AbstractForm component with the helper functions as props
  return (
    <AbstractForm
      getInitialInputs={getInitialInputs}
      renderInput={renderInput}
      validateInputs={validateInputs}
      submitForm={submitForm}
    />
  );
}
// LoginForm.tsx
import React from "react";
import { AbstractForm } from "./AbstractForm";
 
type Input = {
  name: string;
  value: string;
};
 
// Define the login form component as a functional component
export function LoginForm() {
  // Define the helper functions as regular functions
  function getInitialInputs(): Input[] {
    return [
      { name: "username", value: "" },
      { name: "password", value: "" },
    ];
  }
 
  function renderInput(input: Input): JSX.Element {
    return <label htmlFor={input.name}>{input.name}</label>;
  }
 
  function validateInputs(inputs: Input[]): boolean {
    const username = inputs.find((input) => input.name === "username")?.value;
    const password = inputs.find((input) => input.name === "password")?.value;
    return username && password && username.length >= 3 && password.length >= 6;
  }
 
  function submitForm(inputs: Input[]): void {
    const username = inputs.find((input) => input.name === "username")?.value;
    const password = inputs.find((input) => input.name === "password")?.value;
    console.log(
      `Logging in with username: ${username} and password: ${password}`
    );
    // You can use any login API here
  }
 
  // Return the AbstractForm component with the helper functions as props
  return (
    <AbstractForm
      getInitialInputs={getInitialInputs}
      renderInput={renderInput}
      validateInputs={validateInputs}
      submitForm={submitForm}
    />
  );
}

Advantages

Disadvantages