React design patterns π
Software development patterns aim to solve common problems that developers are dealing with. Knowing them is crucial in building an application that is easy to manage. We look at some of the most popular ones which are efficient for cross-cutting concerns, global data sharing, and the separation of concerns. I will use react with typescript in examples to show how to deal with types using these patterns.
What you will learn
Hook pattern
One of the most popular pattern today in react is I think hook pattern. It helps us separate the logic into smaller pieces.
In the below example, we write a component that is responsible for printing the dimensions of the window screen.
We can feel that we have two different jobs to do. One is to calculate the current screen dimension and the second one is to print these dimensions.
We know that to follow the SOLID principle, components should have only one reason to live, so we should put our logic into two components.
The first one is responsible for calculating dimensions we put into the custom hook useWindowDimensions
and the second one into the functional component
WindowDimension
.
export const Example = () => {
return (
<div>
<WindowDimension />
</div>
);
};
const WindowDimension = () => {
const { width, height } = useWindowDimensions();
return (
<div>
<p>Width: {width}</p>
<p>Height: {height}</p>
</div>
);
};
const useWindowDimensions = () => {
const [width, setWidth] = React.useState(0);
const [height, setHeight] = React.useState(0);
React.useEffect(() => {
setWidth(window.innerWidth);
setHeight(window.innerHeight);
const handleResize = () => {
setWidth(window.innerWidth);
setHeight(window.innerHeight);
};
window.addEventListener("resize", handleResize);
return () => window.addEventListener("resize", handleResize);
}, []);
return { width, height };
};
Resize the window to see the result
In this simple example, we can see the power of the hook pattern. We can easily separate logic using them. And I think itβs clean and easy to read.
Hooks are simply functions that can return some value. We can use others hooks inside them but we have to follow some rules. Hooks should be called at the beginning of the function and they cannot be called inside any condition like the if statement.
Render props pattern
Render props pattern helps us accomplish tasks where we need to access the inner state of the component by other components without lifting that state outside.
The simplest example is the one below. The component TextProp
have one property render and only what itβs doing is calling that render method.
export const Example = () => {
return (
<div>
<TextProp render={() => <h1>Loerm ipsum</h1>} />
<TextProp render={() => <h2>Loerm ipsum</h2>} />
</div>
);
};
interface ITextProp {
render: () => JSX.Element;
}
const TextProp = ({ render }: ITextProp) => {
return render();
};
Result
Loerm ipsum
Loerm ipsum
A more useful example is with sharing state. We can have a Date Input component that has a date state and we want to print that date in US and GB format. Without lifting the state above what we can do is pass the render method to the DateInput component with the date parameter.
export const Example = () => {
return (
<div>
Format US
<DateInput render={(date) => <FormatUS date={date} />} />
Format GB
<DateInput render={(date) => <FormatGB date={date} />} />
</div>
);
};
interface IDateInput {
render: (date: string) => JSX.Element;
}
const DateInput = ({ render }: IDateInput) => {
const [date, setDate] = React.useState("");
return (
<div>
<input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
{render(date)}
</div>
);
};
interface IFormat {
date: string;
}
const FormatUS = ({ date }: IFormat) => {
if (!isValidDate(date)) {
return <span></span>;
}
return <span>{new Date(date).toLocaleDateString("en-US")}</span>;
};
const FormatGB = ({ date }: IFormat) => {
if (!isValidDate(date)) {
return <span></span>;
}
return <span>{new Date(date).toLocaleDateString("en-GB")}</span>;
};
function isValidDate(text: string) {
return isNaN(Date.parse(text)) ? false : true;
}
Result
The above example can be rewritten using children prop.
export const RenderPropChildrenExample = () => {
return (
<div>
Format US
<DateInput>{(date) => <FormatUS date={date} />}</DateInput>
Format GB
<DateInput>{(date) => <FormatGB date={date} />}</DateInput>
</div>
);
};
interface IDateInput {
children: (date: string) => JSX.Element;
}
const DateInput = ({ children }: IDateInput) => {
const [date, setDate] = React.useState("");
return (
<div>
<span className="text-black">
<input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
</span>
{children(date)}
</div>
);
};
Result
Render prop is a good technique when we need to preserve the state of the component inside it.
Compound Pattern
Sometimes we can have situations when the group of components works together to accomplish our task, like the select input. To create such components we can utilize Compound Pattern.
Below we create Menu with the option to toggle it. We start with the wrapper component that owns the state and shares it with all the children.
const MenuContext = React.createContext({
open: false,
toggle: (open: boolean) => {},
});
interface IMenu extends PropsWithChildren {}
const Menu = ({ children }: IMenu) => {
const [open, toggle] = React.useState(false);
return (
<MenuContext.Provider value={{ open, toggle }}>
{children}
</MenuContext.Provider>
);
};
The state will be propagated to the children components by the react context. Next, we build a component to toggle the menu.
const Toggle = () => {
const { open, toggle } = React.useContext(MenuContext);
return (
<button
className="p-2 bg-orange-600 text-white"
onClick={() => toggle(!open)}
>
Toggle
</button>
);
};
To access the state of the menu we use useContext
hook.
Next, we build wrapper for our menu items.
interface IList extends PropsWithChildren {}
const List = ({ children }: IList) => {
const { open } = React.useContext(MenuContext);
return (
<>
{open && (
<div className="flex flex-col-reverse divide-y divide-y-reverse">
{children}
</div>
)}
</>
);
};
Showing menu options depend on the open state.
And finally our Menu item component.
interface IItem extends PropsWithChildren {
onClick: () => void;
}
const Item = ({ children, onClick }: IItem) => {
return (
<button onClick={onClick} className="p-2 bg-emerald-900 text-white w-32">
{children}
</button>
);
};
All these components can be defined as properties of the Menu component like below. In that way, we can signal that these components work together.
Menu.Toggle = Toggle;
Menu.List = List;
Menu.Item = Item;
To use it we defined a simple Menu.
export const CompoundPatternExample = () => {
return (
<div>
<Menu>
<Menu.Toggle />
<Menu.List>
<Menu.Item onClick={() => {}}>Edit</Menu.Item>
<Menu.Item onClick={() => {}}>Delete</Menu.Item>
</Menu.List>
</Menu>
</div>
);
};
Result
The compound pattern is good when we need to create a flexible API for our component. The client of our component can arrange child components differently, itβs because react context is available to all children of the wrapper component, they donβt have to be a direct child.