Skip to main content
โšก Calmops

React Component Patterns

React Component Patterns

Component patterns are reusable solutions for common React problems. This article covers essential patterns for building scalable applications.

Introduction

Component patterns provide:

  • Code reusability
  • Separation of concerns
  • Cleaner architecture
  • Better maintainability
  • Scalable solutions

Understanding patterns helps you:

  • Structure components effectively
  • Share logic between components
  • Handle complex state
  • Build flexible UIs
  • Write maintainable code

Presentational vs Container Components

Presentational Components

// โœ… Good: Pure presentational component
function UserCard({ user, onEdit, onDelete }) {
  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <p>Email: {user.email}</p>
      <p>Role: {user.role}</p>
      <div className="actions">
        <button onClick={() => onEdit(user.id)}>Edit</button>
        <button onClick={() => onDelete(user.id)}>Delete</button>
      </div>
    </div>
  );
}

// โœ… Good: Reusable button component
function Button({ children, variant = 'primary', onClick, disabled }) {
  return (
    <button
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  );
}

// โœ… Good: List component
function UserList({ users, onUserClick }) {
  return (
    <ul className="user-list">
      {users.map(user => (
        <li key={user.id} onClick={() => onUserClick(user.id)}>
          {user.name}
        </li>
      ))}
    </ul>
  );
}

// โœ… Good: Form component
function LoginForm({ onSubmit, isLoading }) {
  const [email, setEmail] = React.useState('');
  const [password, setPassword] = React.useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    onSubmit({ email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
}

Container Components

// โœ… Good: Container component
function UserListContainer() {
  const [users, setUsers] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    fetchUsers()
      .then(setUsers)
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);

  const handleEdit = (userId) => {
    // Handle edit logic
  };

  const handleDelete = (userId) => {
    setUsers(users.filter(u => u.id !== userId));
  };

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      <h1>Users</h1>
      {users.map(user => (
        <UserCard
          key={user.id}
          user={user}
          onEdit={handleEdit}
          onDelete={handleDelete}
        />
      ))}
    </div>
  );
}

// โœ… Good: Data fetching container
function PostsContainer() {
  const [posts, setPosts] = React.useState([]);
  const [page, setPage] = React.useState(1);

  React.useEffect(() => {
    fetchPosts(page).then(setPosts);
  }, [page]);

  return (
    <div>
      <PostsList posts={posts} />
      <Pagination page={page} onPageChange={setPage} />
    </div>
  );
}

Render Props Pattern

Basic Render Props

// โœ… Good: Render props component
function MouseTracker({ render }) {
  const [position, setPosition] = React.useState({ x: 0, y: 0 });

  const handleMouseMove = (e) => {
    setPosition({ x: e.clientX, y: e.clientY });
  };

  return (
    <div onMouseMove={handleMouseMove}>
      {render(position)}
    </div>
  );
}

// Usage
function App() {
  return (
    <MouseTracker
      render={(position) => (
        <div>
          <p>Mouse position: {position.x}, {position.y}</p>
        </div>
      )}
    />
  );
}

// โœ… Good: Data provider with render props
function DataProvider({ url, render }) {
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  return render({ data, loading, error });
}

// Usage
function UserProfile() {
  return (
    <DataProvider
      url="/api/user"
      render={({ data, loading, error }) => {
        if (loading) return <p>Loading...</p>;
        if (error) return <p>Error: {error.message}</p>;
        return <div>{data.name}</div>;
      }}
    />
  );
}

// โœ… Good: Form handler with render props
function FormHandler({ onSubmit, render }) {
  const [values, setValues] = React.useState({});
  const [errors, setErrors] = React.useState({});

  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues({ ...values, [name]: value });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      await onSubmit(values);
    } catch (err) {
      setErrors(err.errors);
    }
  };

  return render({
    values,
    errors,
    handleChange,
    handleSubmit
  });
}

// Usage
function LoginForm() {
  return (
    <FormHandler
      onSubmit={(values) => loginUser(values)}
      render={({ values, errors, handleChange, handleSubmit }) => (
        <form onSubmit={handleSubmit}>
          <input
            name="email"
            value={values.email || ''}
            onChange={handleChange}
          />
          {errors.email && <p>{errors.email}</p>}
          <button type="submit">Login</button>
        </form>
      )}
    />
  );
}

Compound Components Pattern

Basic Compound Components

// โœ… Good: Compound component pattern
const Tabs = ({ children }) => {
  const [activeTab, setActiveTab] = React.useState(0);

  return (
    <div className="tabs">
      {React.Children.map(children, (child, index) =>
        React.cloneElement(child, { activeTab, setActiveTab, index })
      )}
    </div>
  );
};

Tabs.TabList = ({ children, activeTab, setActiveTab }) => (
  <div className="tab-list">
    {React.Children.map(children, (child, index) =>
      React.cloneElement(child, { activeTab, setActiveTab, index })
    )}
  </div>
);

Tabs.Tab = ({ children, index, activeTab, setActiveTab }) => (
  <button
    className={`tab ${activeTab === index ? 'active' : ''}`}
    onClick={() => setActiveTab(index)}
  >
    {children}
  </button>
);

Tabs.Panel = ({ children, index, activeTab }) => (
  activeTab === index ? <div className="panel">{children}</div> : null
);

// Usage
function App() {
  return (
    <Tabs>
      <Tabs.TabList>
        <Tabs.Tab>Tab 1</Tabs.Tab>
        <Tabs.Tab>Tab 2</Tabs.Tab>
        <Tabs.Tab>Tab 3</Tabs.Tab>
      </Tabs.TabList>
      <Tabs.Panel>Content 1</Tabs.Panel>
      <Tabs.Panel>Content 2</Tabs.Panel>
      <Tabs.Panel>Content 3</Tabs.Panel>
    </Tabs>
  );
}

// โœ… Good: Accordion compound component
const Accordion = ({ children }) => {
  const [openIndex, setOpenIndex] = React.useState(null);

  return (
    <div className="accordion">
      {React.Children.map(children, (child, index) =>
        React.cloneElement(child, { openIndex, setOpenIndex, index })
      )}
    </div>
  );
};

Accordion.Item = ({ children, index, openIndex, setOpenIndex }) => (
  <div className="accordion-item">
    {React.Children.map(children, (child) =>
      React.cloneElement(child, { index, openIndex, setOpenIndex })
    )}
  </div>
);

Accordion.Header = ({ children, index, openIndex, setOpenIndex }) => (
  <button
    className="accordion-header"
    onClick={() => setOpenIndex(openIndex === index ? null : index)}
  >
    {children}
  </button>
);

Accordion.Panel = ({ children, index, openIndex }) => (
  openIndex === index ? <div className="accordion-panel">{children}</div> : null
);

// Usage
function App() {
  return (
    <Accordion>
      <Accordion.Item>
        <Accordion.Header>Section 1</Accordion.Header>
        <Accordion.Panel>Content 1</Accordion.Panel>
      </Accordion.Item>
      <Accordion.Item>
        <Accordion.Header>Section 2</Accordion.Header>
        <Accordion.Panel>Content 2</Accordion.Panel>
      </Accordion.Item>
    </Accordion>
  );
}

Higher-Order Components (HOC)

Basic HOC

// โœ… Good: HOC for authentication
function withAuth(Component) {
  return function AuthComponent(props) {
    const [user, setUser] = React.useState(null);
    const [loading, setLoading] = React.useState(true);

    React.useEffect(() => {
      checkAuth()
        .then(setUser)
        .finally(() => setLoading(false));
    }, []);

    if (loading) return <p>Loading...</p>;
    if (!user) return <p>Not authenticated</p>;

    return <Component {...props} user={user} />;
  };
}

// Usage
function Dashboard({ user }) {
  return <div>Welcome, {user.name}</div>;
}

const ProtectedDashboard = withAuth(Dashboard);

// โœ… Good: HOC for theme
function withTheme(Component) {
  return function ThemedComponent(props) {
    const [theme, setTheme] = React.useState('light');

    const toggleTheme = () => {
      setTheme(theme === 'light' ? 'dark' : 'light');
    };

    return (
      <div className={`theme-${theme}`}>
        <Component {...props} theme={theme} toggleTheme={toggleTheme} />
      </div>
    );
  };
}

// Usage
function App({ theme, toggleTheme }) {
  return (
    <div>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}

const ThemedApp = withTheme(App);

// โœ… Good: HOC for data fetching
function withDataFetching(url) {
  return function DataFetchingComponent(Component) {
    return function Wrapper(props) {
      const [data, setData] = React.useState(null);
      const [loading, setLoading] = React.useState(true);
      const [error, setError] = React.useState(null);

      React.useEffect(() => {
        fetch(url)
          .then(res => res.json())
          .then(setData)
          .catch(setError)
          .finally(() => setLoading(false));
      }, []);

      return (
        <Component
          {...props}
          data={data}
          loading={loading}
          error={error}
        />
      );
    };
  };
}

// Usage
function UserList({ data, loading, error }) {
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {data?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

const UserListWithData = withDataFetching('/api/users')(UserList);

Custom Hooks Pattern

Reusable Logic with Hooks

// โœ… Good: Custom hook for form handling
function useForm(initialValues, onSubmit) {
  const [values, setValues] = React.useState(initialValues);
  const [errors, setErrors] = React.useState({});
  const [isSubmitting, setIsSubmitting] = React.useState(false);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues({ ...values, [name]: value });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    try {
      await onSubmit(values);
    } catch (err) {
      setErrors(err.errors);
    } finally {
      setIsSubmitting(false);
    }
  };

  const reset = () => {
    setValues(initialValues);
    setErrors({});
  };

  return { values, errors, isSubmitting, handleChange, handleSubmit, reset };
}

// Usage
function LoginForm() {
  const { values, handleChange, handleSubmit } = useForm(
    { email: '', password: '' },
    (values) => loginUser(values)
  );

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" value={values.email} onChange={handleChange} />
      <input name="password" value={values.password} onChange={handleChange} />
      <button type="submit">Login</button>
    </form>
  );
}

// โœ… Good: Custom hook for API calls
function useFetch(url, options = {}) {
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url, options);
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

// โœ… Good: Custom hook for local storage
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = React.useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
}

// Usage
function Preferences() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  return (
    <div>
      <p>Theme: {theme}</p>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </div>
  );
}

Best Practices

  1. Keep components focused:

    // โœ… Good: Single responsibility
    function UserCard({ user }) {
      return <div>{user.name}</div>;
    }
    
    // โŒ Bad: Too many responsibilities
    function UserCard({ user }) {
      return (
        <div>
          {user.name}
          {/* Fetching, filtering, sorting, etc. */}
        </div>
      );
    }
    
  2. Use composition over inheritance:

    // โœ… Good: Composition
    function Button({ children, ...props }) {
      return <button {...props}>{children}</button>;
    }
    
    // โŒ Bad: Inheritance
    class Button extends React.Component {
      // ...
    }
    
  3. Prop drilling alternatives:

    // โœ… Good: Use context
    const ThemeContext = React.createContext();
    
    function App() {
      return (
        <ThemeContext.Provider value="dark">
          <Component />
        </ThemeContext.Provider>
      );
    }
    
    // โŒ Bad: Prop drilling
    function App() {
      return <Component theme="dark" />;
    }
    

Summary

Component patterns are essential. Key takeaways:

  • Use presentational and container components
  • Apply render props for flexible components
  • Use compound components for complex UIs
  • Leverage HOCs for cross-cutting concerns
  • Create custom hooks for reusable logic
  • Keep components focused and composable
  • Choose patterns based on use case

Next Steps

Comments