Understanding Reusable Components and the DRY Principle

Understanding Reusable Components and the DRY Principle

Reusable components and the DRY principle (Don't Repeat Yourself) are essential to developing effective, maintainable software, especially in development. This post will examine these ideas, discuss their significance, and offer useful React code examples to show readers how to put them to use.

What are Reusable Components?

In React, reusable components are UI building blocks that can be used multiple times across your application. Instead of creating a similar structure repeatedly, you abstract the shared logic, structure, and design into a single component, which can be reused with slight variations.

Here are a few examples of reusable components in React:

1. Button Component

Instead of creating separate buttons for login, signup, or any other actions, you can create a generic button component that accepts props for customization:

import React from "react";

function Button({ text, styleClass, onClick }) {
  return (
    <button className={styleClass} onClick={onClick}>
      {text}
    </button>
  );
}

export default Button;

Usage:

<Button text="Login" styleClass="btn-primary" onClick={handleLogin} />
<Button text="Signup" styleClass="btn-secondary" onClick={handleSignup} />

In this case, the Button component is reusable across different parts of your application by passing different text, styleClass, and onClick handlers as props.

2. Input Field Component

Similarly, a reusable input field component can be used for both login and signup forms, or any other form in the application:

function Input({ type, placeholder, value, onChange }) {
  return (
    <input
      type={type}
      placeholder={placeholder}
      value={value}
      onChange={onChange}
    />
  );
}

export default Input;

Usage:

<Input
  type="text"
  placeholder="Enter your username"
  value={username}
  onChange={(e) => setUsername(e.target.value)}
/>
<Input
  type="password"
  placeholder="Enter your password"
  value={password}
  onChange={(e) => setPassword(e.target.value)}
/>

3. Form Component

Taking this concept further, you can create a reusable form component that handles various forms (e.g., login, signup, reset password) by dynamically rendering fields and buttons based on the form type:

import React from "react";
import Input from "./Input";
import Button from "./Button";

function Form({ fields, onSubmit, buttonText }) {
  return (
    <form onSubmit={onSubmit}>
      {fields.map((field, index) => (
        <Input
          key={index}
          type={field.type}
          placeholder={field.placeholder}
          value={field.value}
          onChange={field.onChange}
        />
      ))}
      <Button text={buttonText} styleClass="btn-primary" />
    </form>
  );
}

export default Form;

Usage for login:

<Form
  fields={[
    {
      type: "text",
      placeholder: "Username",
      value: username,
      onChange: (e) => setUsername(e.target.value),
    },
    {
      type: "password",
      placeholder: "Password",
      value: password,
      onChange: (e) => setPassword(e.target.value),
    },
  ]}
  onSubmit={handleLoginSubmit}
  buttonText="Login"
/>

And for signup:

<Form
  fields={[
    {
      type: "text",
      placeholder: "Username",
      value: username,
      onChange: (e) => setUsername(e.target.value),
    },
    {
      type: "email",
      placeholder: "Email",
      value: email,
      onChange: (e) => setEmail(e.target.value),
    },
    {
      type: "password",
      placeholder: "Password",
      value: password,
      onChange: (e) => setPassword(e.target.value),
    },
  ]}
  onSubmit={handleSignupSubmit}
  buttonText="Signup"
/>

Benefits of Reusable Components

By creating components like Button, Input, and Form, you make your code:

  • Consistent: Reusable components ensure that buttons, forms, and input fields behave and look the same throughout your application.

  • Maintainable: When you need to update styles or add new functionality, you only need to change the component once, and the changes will reflect across your app.

  • Efficient: Instead of duplicating code, you save time by reusing the same components with minor adjustments through props.

The DRY Principle: Don't Repeat Yourself

I'll go over every step of this DRY example utilizing React as we go through it. We'll look at an example where we follow the DRY principle and use reusable components to develop a login and signup form.

Scenario

We are building two forms:

  1. Login Form - It will have a username and password field.

  2. Signup Form - It will have a username, email, and password field.

Without the DRY principle, we might duplicate a lot of code between these forms. Instead, we will use reusable components to keep the code clean, maintainable, and efficient.


1. Reusable Input Component

First, let's create a reusable Input component. We don’t want to repeat the code for text fields (username, email, password), so we’ll abstract it into a reusable component.

import React from "react";

function Input({ type, placeholder, value, onChange }) {
  return (
    <input
      type={type}
      placeholder={placeholder}
      value={value}
      onChange={onChange}
    />
  );
}

export default Input;

Explanation:

  • The Input component takes four props:

    • type: Specifies the type of the input field (e.g., "text", "email", "password").

    • placeholder: The placeholder text for the input field.

    • value: The value of the input field (usually coming from React state).

    • onChange: A function that updates the state when the input changes.

This component is reusable and can be used for any input field across the app.


2. Reusable Button Component

Next, we’ll create a reusable Button component, which can be customized for different actions (e.g., "Login", "Signup").

import React from "react";

function Button({ text, styleClass, onClick }) {
  return (
    <button className={styleClass} onClick={onClick}>
      {text}
    </button>
  );
}

export default Button;

Explanation:

  • The Button component is customizable via three props:

    • text: The text inside the button (e.g., "Login", "Signup").

    • styleClass: A CSS class for button styling.

    • onClick: The function to handle what happens when the button is clicked (such as form submission).


3. Reusable Form Component

Here’s where we bring everything together. We create a generic Form component that renders the required input fields dynamically and handles form submission.

import React from "react";
import Input from "./Input";
import Button from "./Button";

function Form({ fields, onSubmit, buttonText }) {
  return (
    <form onSubmit={onSubmit}>
      {fields.map((field, index) => (
        <Input
          key={index}
          type={field.type}
          placeholder={field.placeholder}
          value={field.value}
          onChange={field.onChange}
        />
      ))}
      <Button text={buttonText} styleClass="btn-primary" />
    </form>
  );
}

export default Form;

Explanation:

  • The Form component accepts three props:

    • fields: An array of objects, where each object represents an input field. The object contains properties like type, placeholder, value, and onChange.

    • onSubmit: A function that gets called when the form is submitted.

    • buttonText: The text inside the button (e.g., "Login" or "Signup").

This component iterates over the fields array and renders an Input component for each field. It also renders a Button for form submission.


4. Login and Signup Forms (Using DRY Components)

Now that we have reusable components, we can create the login and signup forms by passing in the relevant fields and functionality as props.

Login Form

import React, { useState } from "react";
import Form from "./Form";

function LoginForm() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");

  const handleLoginSubmit = (event) => {
    event.preventDefault();
    console.log("Login:", { username, password });
    // Perform login logic here...
  };

  return (
    <Form
      fields={[
        {
          type: "text",
          placeholder: "Username",
          value: username,
          onChange: (e) => setUsername(e.target.value),
        },
        {
          type: "password",
          placeholder: "Password",
          value: password,
          onChange: (e) => setPassword(e.target.value),
        },
      ]}
      onSubmit={handleLoginSubmit}
      buttonText="Login"
    />
  );
}

export default LoginForm;

Signup Form

import React, { useState } from "react";
import Form from "./Form";

function SignupForm() {
  const [username, setUsername] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const handleSignupSubmit = (event) => {
    event.preventDefault();
    console.log("Signup:", { username, email, password });
    // Perform signup logic here...
  };

  return (
    <Form
      fields={[
        {
          type: "text",
          placeholder: "Username",
          value: username,
          onChange: (e) => setUsername(e.target.value),
        },
        {
          type: "email",
          placeholder: "Email",
          value: email,
          onChange: (e) => setEmail(e.target.value),
        },
        {
          type: "password",
          placeholder: "Password",
          value: password,
          onChange: (e) => setPassword(e.target.value),
        },
      ]}
      onSubmit={handleSignupSubmit}
      buttonText="Signup"
    />
  );
}

export default SignupForm;

Explanation:

  • LoginForm and SignupForm are now minimal, clean, and reusable. They pass down the appropriate fields to the Form component.

  • We have used React’s useState hook to manage the form state.

  • Each form has its own onSubmit handler to process the form data upon submission.

Full App Component

You can now use both the LoginForm and SignupForm in your application like this:

import React from "react";
import LoginForm from "./LoginForm";
import SignupForm from "./SignupForm";

function App() {
  return (
    <div className="App">
      <h1>Login</h1>
      <LoginForm />
      <h1>Signup</h1>
      <SignupForm />
    </div>
  );
}

export default App;

The Benefits of Following DRY

By following the DRY principle:

  • We avoid code duplication. The Form, Input, and Button components are reused between login and signup forms.

  • If we need to change the design of a button or input field, we only need to modify the Button or Input component.

  • Our code is maintainable. Each form only needs to define its unique fields, while the generic form logic is handled in a single, reusable component.

  • The forms are scalable. If you need another form (e.g., for password reset), you can use the same Form component and pass different fields.

For understanding of the DRY principle and reusable components in action, check out this detailed tutorial on YouTube:

Watch Now on YouTube!

Conclusion

The concepts of reusable components and the DRY principle are critical for building scalable, maintainable, and efficient React applications. By creating modular, reusable components and avoiding redundant code, you can significantly improve the development process, enhance maintainability, and ensure consistency throughout your app.

By applying these practices, you’ll not only write cleaner code but also save time and effort in the long run. React’s component-based architecture naturally encourages this approach, making it easier to follow the DRY principle and create reusable components.

Happy coding!

Follow me on : Github Linkedin