Let’s discuss some of the common design patterns used often when using React library.
Provider
Many times in a React Application we might get a nested structure of components, wherein data present in the component might be needed for some components that are lying deep within its children.
One way to tackle this issue, is to pass the same prop through multiple layers until that component is reached who needs the data. This is called prop-drilling. This can get really messy as, if the prop changes at one place, we’ll have to change that same prop at all places where it was passed. Plus, it’ll be inefficient as many of the components involved in prop drilling might not even have a use of the prop.
Another better way is to wrap the children with a Provider, to which a value is passed which is made available to all the components wrapped inside the Provider via useContext hook.
A Provider is a Higher Order Component provided to us via a context object which is created using createContext method which React has provided for us.
export const DataContext = React.createContext();
const App = () => {
const data = {...} // the data which we need all the children components to be able to access
return (
<DataContext.Provider value={data}>
<Comp1 />
<Comp2 />
<Comp3 />
</DataContext.Provider>
)
}
// and then in any of the child components, data passed via Provider can be retrieved with the help of useContext hook
const Comp1 = () => {
const { data } = React.useContext(DataContext);
return <section>{data.name}</section>
}
The Provider pattern is commonly used to store global data which might be needed throughout the application. A common example is a Theme state which contains all the colors and typography settings.
Container/Presentational
One way to enforce separation of concerns is to have a set of Presentational and Container components.
Presentational Component
The job of Presentational component is to just display the data being passed to it by the container component. It usually doesn’t have its own state, but can have some states for UI purposes only. They can’t modify the props that are passed to it directly. It contains all the styles as well which is needed by the UI.
const DogImages = (dogs = []) => {
return (
<ul>
{dogs.map((dog) => (
<li key={dog.id}>
<Dog data={dog} />
</li>
))}
</ul>
);
};
Container Component
The job of the container component is simply to just provide the right data to Presentational component. It makes the necessary request to the server to fetch the required data, and passes it down to its children which is usually the Presentational component. It might contain its own state to store the data but not the styling that the UI needs.
const DogsContainer = () => {
const [dogs, setDogs] = useState([]);
useEffect(() => {
async function fetchDogs() {
const res = await fetch('<url-to-fetch-dogs>');
const data = (await res.json()) || [];
setDogs(data);
}
fetchDogs();
}, []);
return <DogImages dogs={dogs} />;
};
Such kind of a system makes it easy to write unit test cases for the presentational component. Also, such presentational components can be reused throughout the application and is much easier to understand, as those are usually pure functions. This pattern is used to separate application logic from the rendering logic, but since hooks came into picture, the same goal can be achieved via hooks.
Render Props
A render prop is a function that returns a JSX/React element. Render props can be used in a component to make the component reusable. A simple example of it would look like -
const Title = (props) => props.render();
const App = () => {
return <Title render={() => <h1>Sanjeet Tiwari</h1>} />;
};
Usually the component which has this render prop just renders whatever was sent in the render prop. We can have multiple render props in the component as well.
const Title = (props) => {
return (
<>
{props.firstRender()}
{props.secondRender()}
{props.thirdRender()}
</>
);
};
const App = () => {
return (
<Title
firstRender={() => <h1>Sanjeet Tiwari</h1>}
secondRender={() => <h2>I am a developer</h2>}
thirdRender={() => <h3>I like my job</h3>}
/>
);
};
Render Props are not just used to render some UI, but it can also be used to pass some data to the render prop. It is useful whenever there is a need to lift the state, so that data is accessible for multiple components. If we lift the state from local state to the parent component state, then there is a chance that the entire tree might get rerendered because of teh state change.
To overcome this, Render props can be used. Let’s take an example of an app which takes in a temp in celsius, and converts it to Kelvin and Fahrenheit -
const Input = (render) => {
const [val, setVal] = useState();
return (
<>
<input value={val} onChange={(e) => setVal(e.target.value)} />
{props.render(val)}
</>
);
};
// in App.js
const App = () => {
return (
<Input
render={(val) => (
<>
<Kelvin temp={val} />
<Fahrenheit temp={val} />
</>
)}
/>
);
};
In this way we kind of brought the components onto the same level who are sharing a state, without having to lift the state to the App
component.
The common prop - children
is also technically a render prop and can have a function as its value as well instead of some React elements.
We can change the above App
and Input
component, wherein, instead of passing the render prop explicitly, we will pass it as children.
const Input = () => {
const [val, setVal] = useState()
return (
<>
<input value={val} onChange={e => setVal(e.target.value)} />
{props.children(val)}
</>
)
}
// In App.js
const App = () => {
return (
<Input>
{val => (
<>
<Kelvin temp={val} />
<Fahrenheit temp={val} />
<>
)}
</Input>
)
}
What render props achieve, can be achieved by using the HOC pattern, and inturn can be achieved via Hooks in some cases.
Higher Order Components (HOC)
A HOC is a component which receives another component as a parameter. It adds additional features to the component which was passed to it as a parameter and returns a component with the additional logic.
It can be used to reuse the same piece of logic for any component throughout the application. It can have many applications. One of the basic examples is a HOC which adds padding to the any component.
const withPadding = (Component) => {
return (props) => (
<div style={{ padding: '2rem' }}>
<Component {...props}>
</div>
)
}
const PaddedButton = withPadding(Button);
Composing
Since HOC adds additional functionalities to existing components, multiple HOCs can be applied on a component by passing one HOC as the parameter for other.
Lets take the above example. If we want to add a withHover
HOC as well which will show a tooltip on hover over the component, it’ll look something like this -
const HoverEnabledPaddedButton = withHover(
withPadding(Button),
'Its a normal button'
);
As evident from the above example, an HOC can take multiple parameters other than the component itself.
A special note
An HOC can also add another prop to the component in order to give the component additional behavior or feature. Here, we’ll have to make sure that this extra prop is not colliding or overriding any of the original props of the component. Let’s take an example of withData
HOC which fetches the data using the URL passed to it, and passes it on the component.
const withData = (Component, url = '') => {
return (props) => {
const [data, setData] = useState();
useEffect(() => {
async function fetchData() {
const res = await fetch(url);
const data = await res.json();
setData(data);
}
fetchData();
}, []);
if (!data) return <h1>Loading...</h1>;
return <Component {...props} data={data} />;
};
};
Here the prop data
should not collide with any of the original props of Component
.
Best use cases for a HOC
Currently, HOCs can get easily replaced by Hooks. The best scenarios to use HOCs would be -
- When the same uncustomized behavior is needed by multiple components in the application.
- The component to be passed as parameter can work standalone, i.e. without the HOC.
Compound Component
In many applications, we have seen a group of components that are used to accomplish a certain task and are associated to each other. Compound components share a common logic and state and are used for a specific common feature. Menus, Dropdowns, and many other examples can be there for Compound components.
const Menu = ({ children }) => {
const [open, setOpen] = useState(false);
return <div>{children}</div>;
};
// We can have a MenuItem component which will always be used inside Menu
const MenuItem = ({ children }) => {
return <div>{children}</div>;
};
The Sub component can actually be made a property of the Base component, like this -
Menu.MenuItem = MenuItem;
Compound components share state among them. Let’s say if we want to give the MenuItem
s the ability to toggle the menu, we need to pass the open
and setOpen
to the sub components. There are 2 ways to do this -
Using React.Children.map and React.cloneElement
In this way, each of the children of the base component will be first cloned and then, will be given additional props via React.cloneElement
.
const Menu = ({ children }) => {
const [open, setOpen] = useState(false);
return (
<div>
{React.Children.map(children, (child) => {
return React.cloneElement(child, { open, setOpen });
})}
</div>
);
};
The disadvantages of this method is that, the additional props will only be available to the first level of children. If there are any MenuItem
s deeply nested in the structure, they won’t be able to access the additional props.
Also, these additional props should not result in name collision with any of the existing props.
Using Context API
Simply, a context will be created, and the Provider will reside in the Base component. All the sub components will access the common state via context with the help of useContext
hook.
const MenuContext = React.createContext();
const Menu = ({ children }) => {
const [open, setOpen] = useState(false);
const data = { open, setOpen };
return <MenuContext.Provider value={data}>{children}</MenuContext.Provider>;
};
// And in the MenuItem
const MenuItem = ({ children }) => {
const { open, setOpen } = useContext(MenuContext);
// do whatever is needed with the data
return <div>{children}</div>;
};
Compound Component pattern is beneficial when we don’t want to handle the state for a group of components by ourselves, instead, we want the group of components to handle or manage the state themselves.
Context Module
Context Module pattern is used whenever we need to manage and share the state for a particular feature, wherein the entire fleet of functions and hooks for that feature reside in a module. A reducer is also placed within the same module which, when used with useReducer
hook, defines the capabilities of the feature in the same module.
Let’s take an example of count
feature, which, as the name suggests, focuses on keeping a count, and require methods to increment
or decrement
.
const CountContext = React.createContext();
const defaultState = { count: 0 };
const countReducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error('Invalid action type');
}
};
const CountProvider = ({ children }) => {
const { state, dispatch } = useReducer(countReducer, defaultState);
return (
<CountContext.Provider value={{ state, dispatch }}>
{children}
</CountContext.Provider>
);
};
export const useCount = () => {
const context = useContext(CountContext);
if (!context) throw new Error('Please use this hook inside CountProvider');
return context;
};
export const incrementCounter = (dispatch) => {
dispatch({ type: 'increment' });
};
export const decrementCounter = (dispatch) => {
dispatch({ type: 'decrement' });
};
export default CountProvider;
Here, a Provider has been exported as default export which can be then used to wrap a certain section of application where the count feature will be used. A hook useCount
has been provided which can be imported by all those components who need to access/modify the count state. And 2 functions incrementCounter
and decrementCounter
have been exported which can be used by the components to modify the state.
Hence, all tools to handle a state can be bundled together in a single module.