Exploring Design Patterns in React with Typescript

Savan Chhayani
TechVerito
Published in
5 min readJul 26, 2024
Photo by Greg Rakozy on Unsplash

In modern development, React and Typescript are powerful combinations that can help you build robust, maintainable, and scalable applications. To leverage these technologies effectively, understanding various design patterns is essential. In this blog post, we’ll dive into some common design patterns used in React with Typescript, complete with real-world examples.

1. Component Composition

Component composition is the foundation of React. It involves building complex UIs from simple, reusable components. Instead of creating large, monolithic components, you break down UI into smaller, manageable pieces.

Example:

// Card component to display individual user information
const Card: React.FC<{title: string; content: string}> = ({title, content}) => (
<div className="card">
<h2>{title}</h2>
<p>{content}</p>
</div>
);

// App component to display a list of user cards
const App = React.FC = () => (
<div>
<Card title="User 1" content="User 1 details" />
<Card title="User 2" content="User 2 details" />
</div>
);

Real-world example: In social media application, you can use component composition to create reusable components like Card for user profiles, Post for individual posts, and Comment for comments, which can be composed into larger components like Feed and Profile.

2. Higher-Order Components (HOC)

A Higher-Order Component (HOC) is a function that takes a component and returns a new component with added props or functionality. HOCs are useful for reusing component logic.

Example:

const withLoading = <P extends object>(Component: React.ComponentType<P>) => {
return (props: P & { isLoading: boolean }) => (
props.isLoading ? <div>Loading...</div> : <Component {...props} />
);
};

const DataComponent: React.FC<{ data: string }> = ({ data }) => (
<div>{data}</div>
)

const DataComponentWithLoading = withLoading(DataComponent);

// Usage
<DataComponentWithLoading isLoading={true} data="Loaded data" />;

Real-world example: In an e-commerce application, you can use a HOC to add loading states to various components like ProductList, ProductDetail, and Cart, ensuring a consistent user experience across the application.

3. Render Props

Render props is a technique for sharing code between components using a prop whose value is function. This pattern allows you to pass in a function that returns a React element, giving you control over rendering.

Example:

const DataFetcher: React.FC<{ render: (data: any) => JSX.Element }> = ({ render }) => {
const [data, setData] = React.useState(null);

React.useEffect(() => {
fetchData.then(setData);
}, []);

return data ? render(data) : <div>Loading...</div>;
};

const App: React.FC = () => (
<DataFetcher render={(data) => <div>{data}</div>} />
);

Real-world example: In a news application, a NewsFetcher component can use render props to fetch news articles and pass them to various components for rendering, such as NewsList, TopHeadlines, and CategoryNews.

4. Custom Hooks

Custom hooks are a way to encapsulate reusable logic in a function that can be used across multiple components. They allow you to extract and share logic without duplicating code.

Example:

const useFetch = (url: string) => {
const [data, setData] = React.useState(null);
const [loading, setLoading] = React.useState(true);

React.useEffect(() => {
fetch(url)
.then((response) => response.json())
.then((data) => {
setData(data);
setLoading(false);
});
}, [url]);

return { data, loading };
};

const App: React.FC = () => {
const { data, loading } = useFetch('https://api.example.com/data');

return loading ? <div>Loading...</div> : <div>{JSON.stringify(data)}</div>;
};

Real-world example: In a weather application, you can create a useWeather hook to fetch weather data from an API and use it in components like WeatherToday, WeatherForecast, and WeatherMap.

5. Context API

The Context API allows you to share states across the entire component tree without passing props down manually at every level. It provides a way to manage the global state of your application.

Example:

const UserContext = React.createContext<{ user: string | null; setUser: (user: string) => void } | undefined>(undefined);

const UserProvider: React.FC = ({ children }) => {
const [user, setUser] = React.useState<string | null>(null);

return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};

const useUserContext = () => {
const context = React.useContext(UserContext);
if (!context) {
throw new Error('useUserContext must be used within a UserProvider');
}
return context;
};

const App: React.FC = () => {
const { user, setUser } = useUserContext();

return (
<div>
<p>{user ? `Hello, ${user}` : 'No user logged in'}</p>
<button onClick={() => setUser('John Doe')}>Login</button>
</div>
);
};

// Usage
<UserProvider>
<App />
</UserProvider>;

Real-world example: In a blog platform, you can use Context API to manage user authentication state, allowing components like Header, Sidebar, and Profile to access and update the user’s information without prop drilling.

6. Controlled vs Uncontrolled components

Controlled components are form inputs that derive their values from state and update via onChange handlers. Uncontrolled components use refs to access DOM elements directly.

Example of Controlled Component:

const ControlledInput: React.FC = () => {
const [value, setValue] = React.useState('');

return (
<input value={value} onChange={(e) => setValue(e.target.value)} />
);
};

Example of Uncontrolled Component:

const UncontrolledInput: React.FC = () => {
const inputRef = React.useRef<HTMLInputElement>(null);

const handleClick = () => {
if (inputRef.current) {
alert(inputRef.current.value);
}
};

return (
<div>
<input ref={inputRef} />
<button onClick={handleClick}>Alert Value</button>
</div>
);
};

Real-world example: In a survey application, you might use controlled components for inputs that need real-time validation and uncontrolled components for large forms where you want to handle the data submission in a single step.

7. Error Boundaries

Error boundaries are components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI. They help prevent the entire application from crashing due to an error in a component.

Example:

type Props = {
fallback: React.ReactNode;
children: React.ReactNode;
};

type State = {
hasError: boolean;
};

class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError() {
return { hasError: true };
}

componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
console.error("Error caught by ErrorBoundary", error, errorInfo);
}

render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}

// Usage
const App: React.FC = () => (
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<SomeComponent />
</ErrorBoundary>
);

Real-world example: In a financial dashboard application, you can use error boundaries to catch and handle errors in components like StockChart, TransactionHistory, and PortfolioSummary, ensuring the rest of the application remains functional.

Conclusion

These design patterns can greatly improve the quality and maintainability of your React and Typescript applications. By using component composition, HOCs, render props, custom hooks, the Context API, controlled vs uncontrolled components, and error boundaries, you can build more modular, reusable, and error-resistant code. Experiment with these patterns in your projects to see how they can enhance your development process.

Happy Coding ⌨️

--

--

Savan Chhayani
TechVerito

Senior Consultant At Techverito React | JavaScript | TypeScript | Kotlin | Spring Boot | Docker