4 React Design Patterns You Should Know
This lesson is a preview from our interactive course
In this lesson, we are going to introduce you to four common React patterns that can help you write cleaner and better code. All of the patterns below are built on top of commonly occurring problems, similar to what design patterns are used for.
Child-to-Parent Communication
Child-to-parent communication, also known as bottom-up communication, often becomes a problem when building React applications. Naturally, data flows from top to bottom in React. While technically there is no way to switch the direction, we can work around this issue with callback functions. Take the following as an example:
import React from 'react'
const Child = ({ onClick }) => {
const data = {
text: 'Click on me!'
};
const handleClick = () => onClick(data);
return <button onClick={handleClick}>{data.text}</button>;
}
const App = () => {
// Inside the function we have access to the data
const setState = data => {
console.log(data);
};
return <Child onClick={setState} />;
}
export default App;
In the above example, we have a parent (App
) and Child
component. We want to retrieve the data stored inside the Child
component and use it inside App
. To achieve this, we can pass an onClick
event listener to the Child
component with a function (setState
) that expects the data as its parameter.
The key here is to invoke the passed function with the data inside the child, making it available inside the parent as well. This is what happens on the highlighted line.
Lifting state
Of course, there are also cases where we don't want to wait for a user event to happen. We just want to pass the data as soon as the component is rendered. In this case, we cannot rely on an onClick
listener. Instead, we need to lift the state up:
import React from 'react'
const Child = ({ text }) => {
return <button onClick={() => console.log(text)}>{text}</button>;
}
const App = () => {
const data = {
text: 'Click on me!'
};
return <Child text={data.text} />;
}
export default App;
In this example, we change the way App
and Child
are rendered. Functionally, everything stayed the same, but now we have the state of the Child
one level up, inside the App
. This means we can access the necessary data in the correct component, and pass any required data down to our Child
component via props.
Higher-order components
Higher-order components (HOC for short), are a pattern used for reusing component logic. Just like higher-order functions in JavaScript, React uses the same compositional nature but with components.
Let's look at a simple common example of a higher-order component: a conditional wrapper.
import React from 'react'
const ConditionalWrapper = ({ condition, wrapper, children }) => {
return condition ? wrapper(children) : children;
}
const App = () => {
const renderWithLink = false;
return (
<ConditionalWrapper
condition={renderWithLink}
wrapper={children => (
<a href="#">{children}</a>
)}
>
<img
src="https://webtips.dev/assets/img/logo.png"
style={{ background: 'white' }}
/>
</ConditionalWrapper>
);
}
export default App;
Higher-order components are pure functions. Rather than mutating existing components, they return new components.
The ConditionalWrapper
component expects three different props:
condition
: A condition to be evaluated as a booleanwrapper
: A component that should be used as a wrapperchildren
: The component that should be rendered either with or without the wrapper depending on the condition.
In this example, the img
inside the ConditionalWrapper
is wrapped with a link conditionally. As the current condition is false
, the image is rendered as is. Change the condition to true
to see how it will be wrapped with an anchor.
Whenever multiple components need to be wrapped and decorated with additional functionality, we usually have a great candidate for a HOC. Some other commonly used examples of HOC include components like Authenticate
, Loading
, or WrapWithStyles
.
Reinforce your knowledge!
Which of the following is the correct way to turn renderWithLink
into a hook?
1.) const [renderWithLink, updateRenderWithLink] = useEffect(false);
2.) const [renderWithLink, updateRenderWithLink] = useState(false);
3.) const [renderWithLink, updateRenderWithLink] = useState(false, false);
Render props
Render props are another common technique used for uncoupling the rendering logic from a component. This is what we did with the above HOC. The wrapper
prop acts as a render prop that renders the elements passed to it when the passed condition is true
. When working with render props, this prop is called render
by convention. Let's see an example:
import React from 'react'
const User = ({ render }) => {
const userData = {
name: 'John'
};
return render(userData);
}
const App = () => {
return (
<React.Fragment>
<User render={user => <h1>Welcome {user.name}!</h1>} />
<User render={user => <h2>Welcome {user.name}!</h2>} />
</React.Fragment>
);
}
export default App;
Here, we can follow the same steps that we did for the ConditionalWrapper
component. We need to call the render
prop as a function to render the JSX passed to it. By passing the userData
as a parameter to the render prop, we can access it during creation.
Essentially, we just decoupled the rendering logic from the component, making it possible for each User
component to define its own render logic. We can reuse the same component over and over while rendering different things.
Provider pattern
Last but not least, we already had an introduction to the useContext
hook in the Hooks section. This hook is often used in a provider pattern to provide a global state to an application. In this pattern, all providers are created at the top of the React tree to make a context globally accessible.
// Using a separate component for collecting providers in one place
const ApplicationProvider = ({ children }) => (
<UserProvider value={user}>
<MetaProvider value={meta}>
{children}
</MetaProvider>
</UserProvider>
)
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<ApplicationProvider>
<App />
</ApplicationProvider>
</React.StrictMode>
);
Often, many providers are required for an application to hold many types of data. It is a common practice to collect these providers into a single component to keep the number of JSX elements to a minimum for the root of the application.
Reinforce your knowledge!
When is the default value used within a createContext
call?
Rocket Launch Your Career
Speed up your learning progress with our mentorship program. Join as a mentee to unlock the full potential of Webtips and get a personalized learning experience by experts to master the following frontend technologies: