DEV Community

Cover image for Design patterns - Chain of Responsibility in React
Reza Nazari
Reza Nazari

Posted on • Edited on

Design patterns - Chain of Responsibility in React

Chain of Responsibility Design Pattern

The Chain of Responsibility is a behavioral design pattern that passes requests sequentially through a chain of handlers. The order of these handlers is predetermined. Upon receiving a request, each handler decides whether it is capable of processing it. If so, it processes the request; otherwise, it forwards the request to the next handler in the chain. Some handlers may choose not to perform any action on the request and simply pass it along.

The key advantages of this approach include:

Clear separation of concerns: Each handler is responsible solely for its own specific task – much like an assembly line in a factory, with the difference that here each station may skip the task and pass the item to the next station.

Reduced coupling: The sender of the request does not need to know which handler will ultimately process it; it only needs to know the chain. This significantly reduces dependencies between system components.

Flexibility and extensibility: New handlers can be easily added to the chain, or their order can be changed, without affecting other parts of the system.

A common real‑world example is authentication in web applications: a user request passes through a password validation layer, then an access‑level verification layer, and finally a logging layer – each layer responding as needed or forwarding the request onward.

By leveraging this capability, complex systems can be managed in a well‑organized, modular, and extensible manner.

Below is an example of how this pattern can be used in a frontend application for a login page.

export interface FormContext {
  email: string;
  password: string;
  errors: string[];
}

export type Middleware = (context: FormContext, next: () => void) => void;

export const createMiddlewareChain = (middlewares: Middleware[]) => {
  return (initialContext: FormContext): FormContext => {
    let index = 0;

    const next = (): void => {
      if (index < middlewares.length) {
        const middleware = middlewares[index];
        index++;
        middleware(initialContext, next);
      }
    };

    next();
    return initialContext;
  };
};

export const emailMiddleware: Middleware = (context, next) => {
  if (!context.email || context.email.trim().length === 0) {
    context.errors.push("Email is required");
    return;
  }
  next();
};

export const formatMiddleware: Middleware = (context, next) => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (context.email && !emailRegex.test(context.email)) {
    context.errors.push("Invalid email format");
    return;
  }
  next();
};

export const passwordMiddleware: Middleware = (context, next) => {
  if (!context.password || context.password.trim().length === 0) {
    context.errors.push("Password is required");
    return;
  }
  next();
};

export const passwordLengthMiddleware: Middleware = (context, next) => {
  if (context.password && context.password.length < 6) {
    context.errors.push("Password must be at least 6 characters");
    return;
  }
  next();
};


Enter fullscreen mode Exit fullscreen mode
interface SignupFormProps {
  onSubmitSuccess?: (data: { email: string; password: string }) => void;
}

const SignupForm: React.FC<SignupFormProps> = ({ onSubmitSuccess }) => {
  const [email, setEmail] = useState<string>("");
  const [password, setPassword] = useState<string>("");
  const [errors, setErrors] = useState<string[]>([]);
  const [isSubmitting, setIsSubmitting] = useState<boolean>(false);

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
    e.preventDefault();
    setErrors([]);
    setIsSubmitting(true);

    const context: FormContext = {
      email: email.trim(),
      password: password.trim(),
      errors: [],
    };

    const chain = createMiddlewareChain([
      emailMiddleware,
      formatMiddleware,
      passwordMiddleware,
      passwordLengthMiddleware,
    ]);

    const result = chain(context);

    if (result.errors.length > 0) {
      setErrors(result.errors);
      setIsSubmitting(false);
      return;
    }

    setErrors(["✅ Registration successful!"]);
    setIsSubmitting(false);

    if (onSubmitSuccess) {
      onSubmitSuccess({ email: result.email, password: result.password });
    }

    setTimeout(() => {
      setEmail("");
      setPassword("");
      setErrors([]);
    }, 2000);
  };

  return (
    <form onSubmit={handleSubmit} aria-label="Signup form">
      <h3>📝 Signup Form</h3>

      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          value={email}
          onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
            setEmail(e.target.value)
          }
          autocomplete="off"
          placeholder="example@email.com"
          disabled={isSubmitting}
          aria-describedby={errors.length > 0 ? "error-message" : undefined}
        />
      </div>

      <div>
        <label htmlFor="password">Password</label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
            setPassword(e.target.value)
          }
          placeholder="Minimum 6 characters"
          disabled={isSubmitting}
          aria-describedby={errors.length > 0 ? "error-message" : undefined}
        />
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Processing..." : "Sign Up"}
      </button>

      {errors.length > 0 && (
        <div
          id="error-message"
          role={errors[0].includes("") ? "status" : "alert"}
        >
          {errors.map((err, index) => (
            <div key={index}>{err}</div>
          ))}
        </div>
      )}
    </form>
  );
};

export default SignupForm;
Enter fullscreen mode Exit fullscreen mode

Top comments (0)