Introduction
React provides developers with many options to create application designs through its versatile nature. The many ways React facilitates development are beneficial, but cause codebases to become complicated for scaling and testing, and maintenance. The introduction of design patterns generates a solution to these challenges.
This blog examines critical React design patterns which developers should use to develop clean, sustainable and expansible codebases. These practices will enable solo coders and teamwork to develop robust professional React applications.
Prerequisites
Node.js and npm
React
Typescript
React Hooks and Custom Hooks
Higher Order Functions
Why Use React Design Patterns?
Developer solutions to recurrent software development difficulties appear as reusable abstract patterns. In the case of React.
These coding practices enhance readability in addition to enabling smooth teamwork between different development teams.
Uniform structures will support bug reduction.
Elements of the system should have the capacity for reuse and extensive testing.
The design pattern standardizes code structure to enhance the process of refactoring and new developer onboarding.
Presentational and Container Components Pattern :
The software principle of separation of concerns achieves excellent implementation support in React through its introduction of Presentational and Container components.
UX elements receive attention through presentational components that define visual aspects (UI).
Such components concentrate on system operation and handling of logical and data elements.
Presentational Component(UI) :
// PresentationalComponent.tsx
type UserCardProps = {
name: string;
email: string;
};
const UserCard: React.FC = ({ name, email }) => (
{name}
{email}
);
export default UserCard;
Container Component(Logic) :
// ContainerComponent.tsx
import UserCard from '../components/UserCard';
import { useEffect, useState } from 'react';
type User = {
name: string;
email: string;
};
const UserCardContainer = () => {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user')
.then((res) => res.json())
.then(setUser);
}, []);
return user ? : Loading...
;
};
export default UserCardContainer;
Higher-Order Components :
An HOC functions through a process where it accepts a component to produce another component that inherits additional functionality.
- The behavior addition process does not impact the wrapped component.
- The framework provides context-based injection for adding props which include authorization and theme features.
- The implementation of permission wrappers and analytics hooks as well as other related functionality.
// hoc/withAdminGuard.tsx
import React from 'react';
type WithAdminProps = {
isAdmin: boolean;
};
export function withAdminGuard(
WrappedComponent: React.ComponentType
) {
return ({ isAdmin, ...props }: WithAdminProps & P) => {
if (!isAdmin) return
Access denied
;
return ;
};
}
Usage :
//components/Dashboard.tsx
const Dashboard = () => Admin Dashboard
;
export default withAdminGuard(Dashboard);
Custom Hooks :
Hooks represent the main benefit of modern React applications. Rephrase the complex effects and state management logic from components by moving them into customizable hooks known as custom hooks.
- All custom hooks begin with use (such as useAuth and useFetch, and useFormState).
- When using Hooks in your applications, always maintain their purity while allowing defined side effects to occur when necessary.
- Documentation of future input and output will enhance the developer experience.
- Custom hooks establish themselves as the main approach for creating reusable logic components.
// hooks/useWindowSize.ts
import { useEffect, useState } from 'react';
type WindowSize = {
width: number;
height: number;
};
export const useWindowSize = (): WindowSize => {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () =>
setSize({ width: window.innerWidth, height: window.innerHeight });
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
};
Compound Components :
Compound components built with React Context represent an elegant solution to share internal state between related components that avoid the need for drilling props.
// components/Tabs.tsx
import React, { createContext, useContext, useState } from 'react';
type TabsContextType = {
activeIndex: number;
setActiveIndex: (index: number) => void;
};
const TabsContext = createContext(undefined);
export const Tabs = ({ children }: { children: React.ReactNode }) => {
const [activeIndex, setActiveIndex] = useState(0);
return (
{children}
);
};
export const TabList = ({ children }: { children: React.ReactNode }) => (
{children}
);
export const Tab = ({ index, children }: { index: number; children: React.ReactNode }) => {
const context = useContext(TabsContext);
if (!context) throw new Error('Tab must be used within Tabs');
const isActive = context.activeIndex === index;
return (
);
};
export const TabPanel = ({ index, children }: { index: number; children: React.ReactNode }) => {
const context = useContext(TabsContext);
if (!context || context.activeIndex !== index) return null;
return {children};
};
Render Props :
A single prop object allows us to replace multiple individual parameter passes by combining related values.
Using props as an object simplifies code structure and improves maintainability because it allows transmission of various connected values to a component.
// components/MouseTracker.tsx
import { useState } from 'react';
type MouseProps = {
render: (x: number, y: number) => React.ReactNode;
};
export const MouseTracker: React.FC = ({ render }) => {
const [coords, setCoords] = useState({ x: 0, y: 0 });
return (
setCoords({ x: e.clientX, y: e.clientY })}
>
{render(coords.x, coords.y)}
);
};
Usage :
Mouse at ({x}, {y})
} />
Controlled Component :
Functional forms require slight attention during their implementation. The value within the controlled input exists in the React state directly.
Uncontrolled components maintain their state inside the DOM while exposing this state through ref.
Controlled represents data that is more predictable and simpler to debug. The performance advantages favor uncontrolled elements when dealing with big forms and external integration points.
The most effective method involves controlled input for user-generated data alongside uncontrolled input for imported and read-only data.
// components/ControlledInput.tsx
import { useState } from 'react';
const ControlledInput = () => {
const [value, setValue] = useState('');
return (
setValue(e.target.value)}
placeholder="Type here..."
/>
);
};
Use TypeScript :
Through typed functionality, TypeScript enables JavaScript users can discover errors in advance while developing code that remains easy to understand. Defining component data requirements enables better reliability and simplified maintenance of your React application through TypeScript.
Frontend Setup in React :
We’ll write the code to get a better understanding of React code patterns:
Step-by-step Implementation
Create a React App :
Let’s create a React app using the following command :
//Create a react app using the following command
npx create-react-app react-code-pattern --template typescript
After the setup is complete, navigate to your project folder :
// Navigate to the project folder
cd react-code-pattern
npm start
This will start the development server, and you will be able to visit the app at http://localhost:3000.
- Project Structure :
Make sure your project has the following folder structure :

You can find the file codes in their respective section above.
Anti-Patterns to Avoid :
Our discussion of well-known patterns should include a mention of these problematic patterns.
- Props drilling too deep
- A requirement for maintaining clean React code involves achieving clear code alongside capabilities for future expansion.
- Overusing any in TypeScript
- Ignoring cleanup in useEffect
- State items that do not belong together should not be stored within a single useState hook call.
Conclusion
A requirement for maintaining clean React code involves achieving clear code alongside capabilities for future expansion. Your code achieves lower maintenance costs with enhanced reliability due to the proper implementation of container components as well as custom hooks and compound components when using TypeScript. The adoption of proper techniques starts with small-scale testing to be used in future maintenance operations.