Common React Design Patterns
In the world of React development, we often encounter a set of challenges and issues that are pretty common across the board. To deal with these effectively, the React community has come up with a bunch of design patterns. Think of these as our go-to strategies for solving frequent design hurdles. These patterns help us structure our components, manage state more effectively, streamline data flow, and boost our app's performance. It's all about writing code that not only works well but is also easy to understand and update.
Why use patterns in React?
Design patterns not only streamline the development process but also significantly improve team collaboration and communication. By adhering to a common set of patterns, team members can understand each other's code more easily, facilitating a smoother integration of various components and services within a project. This shared understanding reduces the learning curve for new team members and decreases the likelihood of misinterpretations or errors when integrating new features or services.
Furthermore, design patterns contribute to the creation of a more robust and error-resistant codebase. Since these patterns have been tried and tested in various scenarios, their application helps in avoiding common pitfalls and design flaws that might not be evident during the initial stages of development.
Overall, the implementation of design patterns in software development projects offers a multitude of benefits that extend beyond the immediate advantages of code standardization and efficiency. Their role in facilitating team collaboration, ensuring code quality, and adapting to changing requirements makes them an indispensable tool in the arsenal of modern software development practices.
The High Order Component (HOC) Pattern
In a React application, we frequently need to apply the same logic in multiple components. This logic might involve adding specific styles to components, requiring authorization, or implementing a global state. One approach to reusing this logic across multiple components is by using the Higher-Order Component pattern.
A Higher-Order Component (HOC) is a component that accepts another component as its input. The HOC encapsulates the logic intended for application to the received component. Upon the application of this logic, the HOC yields the modified component with the additional functionality.
You may find the HOC pattern very similar to the Decorator pattern, and you would be right. The major difference between these two patterns is that HOC must always return a component.
Presentational and Container Component Pattern
The Container and Presentation pattern is a design approach that aims to separate the presentation logic from the business logic in a React code. This separation makes the code modular, testable, and aligns with the separation of concerns principle. In React applications, there are often scenarios where data needs to be fetched from a backend or store, or where a logic needs to be computed and displayed in a React component. In these cases, the Container/Presentational components pattern can be used to categorize the components into two distinct parts:
The container component is responsible for data fetching or computation.
The presentational component's role is to display data that is passed as props on the UI.
An example of Container component code:
import { useEffect } from "react";
import { Cryptocurrency } from "./types";
import CharacterList from "./CharacterList";
function Cryptocurrencies() {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [hasError, setHasError] = useState<boolean>(false);
const [cryptocurrencies, setCryptocurrecies] = useState<Cryptocurrency>([]);
const fetchData = async () => {
try {
setIsLoading(true);
const response = await fetch('/api/cryptocurrencies.json');
const data = await response.json();
setCharacters(data);
} catch (err) {
setHasError(true);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
return (
<CryptocurrenciesList loading={isLoading} hasError={error} data={cryptocurrencies} />
)
};
An example of Presentational component code:
import { Cryptocurrency } from "./types";
interface CryptocurrenciesListProps {
loading: boolean;
hasError: boolean;
data: Cryptocurrency[];
}
function CryptocurrenciesList({ loading, hasError, data }: CryptocurrenciesListProps){
if (loading) return <div>Loading...</div>
if (error) return <div>Something went wrong...</div>
if (!data.length) return null;
return (
<ul>
{data.map((cryptocurrency) => (
<li key={cryptocurrency.id}>{cryptocurrency.name}</li>
))}
</ul>
)
};
Conditional Rendering Pattern
In React, a powerful feature you can utilize is conditional rendering. This allows you to render different components or elements based on certain conditions, making your application dynamic and responsive to user interactions or other factors. Conditional rendering in React can be achieved using JavaScript syntax, such as if
statements, logical AND (&&
), and the ternary operator (? :
).
For instance, if
statements are straightforward to use but might require you to break your JSX into multiple segments to keep the logic inline. This approach is useful when you have complex conditions or multiple branches of logic to handle.
function OrderStatus({ isComplete }: { isComplete: boolean }) {
if (isComplete) {
return <p>Your order has been completed!</p>;
}
return <p>Your order is being processed.</p>;
}
The logical &&
operator is handy for cases where you want to render something based on a truthy condition. It is often used to include or exclude components from the render output, depending on the truthiness of the condition. For example, you might render a Make another order
button only if previous order is complete.
function OrderDetails({ isComplete }: { isComplete: boolean }) {
return (
<>
{/* Some order details would be here */}
{/*Show a button if order is complete*/}
{isComplete && <button>Make another order</button>}
</>
);
}
The ternary operator is a concise way to conditionally render one of two values or components. It is especially useful for inline rendering and can be thought of as a streamlined if-else statement. You can use it to decide between two components based on a condition directly within your JSX.
function OrderStatus({ isComplete }: { isComplete: boolean }) {
return (
<>
{isComplete ? (
<p>Your order has been completed!</p>
) : (
<p>Your order is being processed.</p>
)}
</>
);
}
Note: Remember, while conditional rendering is a powerful tool, it's essential to keep your components and render logic as simple and readable as possible. Overusing conditional rendering, especially inline, can make your components harder to read and maintain. Always consider the simplest approach to achieve your goal and keep your React applications clean and efficient.
Provider Pattern
In scenarios where we aim to share state or data across numerous, if not all, components within a React application, relying solely on props can quickly become impractical. This is especially true when a substantial portion of the components need access to certain pieces of data. We frequently encounter what's known as "prop drilling" in these situations—this is when we find ourselves passing props down through many layers of the component tree. Not only does this make refactoring a Herculean task (especially when the props are deeply integrated), but it also makes tracking the data's origin a real headache.
function App() {
const userData = { ... }
return (
<UserDataProvider>
<Header user={userData} />
<Content user={userData} />
</UserDataProvider>
)
}
function Content({user}) {
return (
<div>
<Recommendations user={user} />
{/*...*/}
</div>
)
}
function UserImage({user}) {
return (
<a href="/profile">
<img src={user.image} />
</a>
)
}
function Header({user}) {
// display a sign in button if user is not logged id
if (!user?.loggedIn) {
return <button>Sign in</button>
}
// otherwise display user image
return <UserImage user={user} />
}
function Recommendations({user}) {
if (!user?.loggedIn) {
return <p>Sign in to see your personal recommendations</p>
}
// otherwise display user image
return (
<ul>
{user.recommendations.map(recommendation => (
<li key={recommendation.id}>{recommendation.title}</li>
))}
</ul>
)
}
Navigating the complexities of prop drilling in a growing React application can become quite cumbersome. Imagine having to update the name of a prop across numerous components – it's a refactor that no developer looks forward to. As our app scales, the limitations of prop drilling become increasingly apparent.
That's precisely why leveraging the Provider Pattern is a game-changer. It's a strategy that allows us to circumvent the tediousness of passing data through each component layer. Instead of prop drilling, we can envelop our component tree with a Provider. This Provider, a higher-order component courtesy of React's Context API, enables us to inject data directly into the components that require it. By simply creating a Context with React's createContext
method, we set the stage for a more streamlined and efficient data flow within our applications.
const UserDataContext = React.createContext()
function UserDataProvider({children}) {
const someUserData = {...}
return <UserDataContext.Provider value={someUserData}>{children}</UserDataContext.Provider>
}
function useUserData() {
const context = React.useContext(UserDataContext)
if (context === undefined) {
throw new Error('useUserData must be used within a UserDataProvider')
}
return context
}
function App() {
return (
<UserDataProvider>
<Header />
<Content />
</UserDataProvider>
)
}
function Content() {
return (
<div>
<Recommendations />
{/*...*/}
</div>
)
}
function UserImage({user}) {
return (
<a href="/profile">
<img src={user.image} alt="App Store" />
</a>
)
}
function Header() {
const user = useUserData()
// display a sign in button if user is not logged id
if (!user?.loggedIn) {
return <button>Sign in</button>
}
// otherwise display user image
return <UserImage user={userData} />
}
function Recommendations() {
const user = useUserData()
if (!user?.loggedIn) {
return <p>Sign in to see your personal recommendations</p>
}
// otherwise display user image
return (
<ul>
{user.recommendations.map(recommendation => (
<li key={recommendation.id}>{recommendation.title}</li>
))}
</ul>
)
}
Render Props Pattern
The Render Props pattern enhances component reusability by allowing components to be passed as props to others, thereby facilitating the sharing of logic across various components. A render prop refers to a function prop utilized by a component to determine its rendering output.
This pattern is particularly effective in scenarios that require the sharing of state or behavior among components without necessitating the creation of a distinct component for each instance. Furthermore, the render prop pattern contributes to maximizing component versatility by enabling a prop on a component, whose value is a function returning a JSX element, to dictate the rendering. In this arrangement, the host component abstains from rendering anything other than what is specified by the render prop, thus delegating the rendering process to the render prop itself rather than embedding proprietary rendering logic.
const exchangeRates = {
btc: some-val,
eth: some-val
}
async function CurrencyConverter(props: {
renderBTC: ({ value: number }) => JSX.Element;
renderETH: ({ value: number }) => JSX.Element;
}) {
const [usdValue, setUsdValue] = useState(0);
return (
<>
<input value={usdValue} onChange={(e) => setUsdValue(e.target.value)} />
{props.renderBTC({ value: usdValue * exchangeRates.btc })}
{props.renderETH({ value: usdValue * exchangeRates.eth })}
</>
);
}
export default function App() {
return (
<CurrencyConverter
renderBTC={({ value }) => <div>{value} BTC</div>}
renderETH={({ value }) => <div>{value} ETH</div>}
/>
);
}