Author:
Learn some advanced React patterns and how to use them.
React is an open-source JavaScript library designed for building user interfaces, particularly for single-page applications where smooth and efficient updates are crucial. Developed and maintained by Facebook, React employs a declarative syntax that enables developers to describe the desired UI state, and it automatically updates the user interface when the underlying data changes.
What it brings to the table:
Organises UI into reusable components.
Simplifies the process of building and updating UI.
Enhances performance by minimizing DOM manipulations.
Ensures a unidirectional flow of data for predictable behavior.
React components are the fundamental building blocks of a React application, encapsulating a self-contained piece of user interface and its corresponding functionality. These components can be thought of as custom, reusable HTML elements that developers compose to create complex and interactive UIs. Each React component is designed to manage its own state, allowing for a modular and maintainable code structure.
Keep in mind, when I say "advanced react", we are not going into a deep dive of some
super complex patterns or concepts,
basic knowledge of react is needed to follow along.
Firstly let's look at the benefits that we can achieve when using the react patterns
that we will discuss in this article:
Render Props is a design pattern in React that involves passing a function as a prop to a component, allowing the component to delegate the rendering of its content to the passed function.
It maybe sounds complicated, but it really isn't... Let's see it in a example:
function ToggleOn({on, children}) {
return on ? children : null
}
function ToggleOff({on, children}) {
return on ? null : children
}
function ToggleButton({on, toggle, ...props}) {
return <Switch on={on} onClick={toggle} {...props} />
}
Above we have a set of components to work with. We will be using the Toggle example across multiple patterns so make sure you understand what these components do.
Now let's say that we want to use these component in our react application. The naive way would be:
function App() {
const [on, setOn] = React.useState(false)
const toggle = () => setOn(!on)
return (
<>
<ToggleOn on={on}>The button is on</ToggleOn>
<ToggleOff on={on}>The button is off</ToggleOff>
<ToggleButton on={on} toggle={toggle} />
<SomeLargeComponent />
</>
)
}
There is one obvious problem in the "naive" approach. Can you spot it ?
The problem is that the state is "global" in a sense and is exposed to all of the components, including "SomeLargeComponent". If you know how react works under the hood, you know that a change of "on" state will trigger a re-render of all components in App, including the "SomeLargeComponent". We would want to avoid that.
Another thing that would be nice if we separated the logic so we have a "presentation" component and a "container" component.
We can achieve this using render props:
function Toggle({ render }) {
const [on, setOn] = React.useState(false);
const toggle = () => setOn(!on);
return render({ on, toggle });
}
function App() {
return (
<Toggle
render={({ on, toggle }) => (
<>
<ToggleOn on={on}>The button is on</ToggleOn>
<ToggleOff on={on}>The button is off</ToggleOff>
<ToggleButton on={on} toggle={toggle} />
</>
)}
/>
<SomeLargeComponent />
);
}
We created a Toggle component that will be entirely responsible for handling state internally. We provide the Toggle component a render prop called "render" which is a function that accepts state (or whatever we want) that is passed down from the Toggle component where the state and logic is being held.
We can also use the children prop as a render prop. In that case the code would look like this:
function Toggle({ children }) {
const [on, setOn] = React.useState(false);
const toggle = () => setOn(!on);
return children({ on, toggle });
}
function App() {
return (
<Toggle
{({ on, toggle }) => (
<>
<ToggleOn on={on}>The button is on<ToggleOn/>
<ToggleOff on={on}>The button is off<ToggleOff/>
<ToggleButton on={on} toggle={toggle} />
</>
)}
/>
<SomeLargeComponent />
);
}
This way we saved "SomeLargeComponent" from re-rendering, and solved the problem of logic separation.
The issues that we tried to solve with render props, have largely been replaced by React Hooks. As Hooks changed the way we can add reusability and data sharing to components, they can replace the render props pattern in many cases. Besides this, I also wanted to go over render props, because it is still being used and you will probably use, or have used, this pattern before.
Compound Components in React is a design pattern where a set of components work together to form a cohesive unit, allowing the composition of complex UI structures while maintaining a clear and intuitive API. Unlike typical components that operate independently, compound components are designed to be used together as a group to achieve a specific functionality or appearance. This pattern enhances code organization, promotes reusability, and provides a more declarative and intuitive interface for developers.
It will be more clear with an example, let's just remind our self what does the "naive" solution to the toggle problem looks like:
function App() {
const [on, setOn] = React.useState(false)
const toggle = () => setOn(!on)
return (
<>
<ToggleOn on={on}>The button is on<ToggleOn/>
<ToggleOff on={on}>The button is off<ToggleOff/>
<ToggleButton on={on} toggle={toggle} />
<SomeLargeComponent/>
</>
)
}
Let's see what the Compound Components solution would look like:
function Toggle({children}) {
const [on, setOn] = React.useState(false)
const toggle = () => setOn(!on)
return React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
on,
toggle,
})
})
}
function App() {
return (
<>
<Toggle>
<ToggleOn>The button is on<ToggleOn/>
<ToggleOff>The button is off<ToggleOff/>
<ToggleButton />
<Toggle/>
<SomeLargeComponent/>
</>
)
}
We can see that we have something similar to the render props solution. The main difference in the App component is that we don't have prop drilling anymore (keep in mind we are still using the same set of toggle components), which is awesome. The state is being shared implicitly within the new defined Toggle component.
The key part of this pattern is this:
React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
on,
toggle,
})
})
Here we are taking the children provided to the component, going through each child, cloning the child and finally the last parameter provided to the "cloneElement" function manually passing "props" to the new react element. This way all of the children get the props needed and they are provided from the Toggle component which again has all the logic and state inside it.
This looks cool, but it does have flaws. For example, if we wanted to have something like this:
function App() {
return (
<>
<Toggle>
<ToggleOn>The button is on<ToggleOn/>
<ToggleOff>The button is off<ToggleOff/>
<span>Hello<span/>
<ToggleButton />
</>
<SomeLargeComponent/>
</>
)
}
We would get an error indicating that we can't clone a "span" element. Solution?
function Toggle({children}) {
const [on, setOn] = React.useState(false)
const toggle = () => setOn(!on)
return React.Children.map(children, (child, index) => {
if (typeof child.type === 'string') return child
return React.cloneElement(child, {
on,
toggle,
})
})
}
Adding a simple condition would solve this problem, because the type of a custom made component is always an object. By checking if the type is a string, we can just return the element as is and not clone it.
But what if we want something like this:
function App() {
return (
<>
<Toggle>
<ToggleOn>The button is on<ToggleOn/>
<ToggleOff>The button is off<ToggleOff/>
<div>
<span>Hello<span/>
</div>
<ToggleButton />
</>
<SomeLargeComponent />
</>
)
}
Unfortunately, using the previous solution will not work for this use case, and it is not that easy to make this code work using the previous solution. If we really wanted to, we could make some sort of recursive algorithm that would traverse the children, but that is definitely an overkill. What would be a better option is...
It is basically still the same pattern as compound components, the main difference is that the parent state is not being provided directly to children components, but rather via context (link do https://legacy.reactjs.org/docs/context). This provides a more dynamic and scalable approach to managing shared state within a component hierarchy.
There is quite a bit of refactoring that needs to be done. Let's start by creating our context:
const ToggleContext = React.createContext()
Now let's create a hook to use our new context:
function useToggle() {
return React.useContext(ToggleContext)
}
In our Toggle component we need to wrap the children with a context provider. We pass the local state To the provider. When we do this, in simple terms, the provided state values get stored in the context instance. This enables that the children of the Toggle component have access to the values that were provided.
function Toggle({children}) {
const [on, setOn] = React.useState(false)
const toggle = () => setOn(!on)
return (
<ToggleContext.Provider value={{on, toggle}}>
{children}
<ToggleContext.Provider/>
)
}
Now we need to change our toggle components. They will no longer be accepting the needed state in props, but rather use the hook useToggle that we created to access the necessary state.
function ToggleOn({children}) {
const {on} = useToggle()
return on ? children : null
}
function ToggleOff({children}) {
const {on} = useToggle()
return on ? null : children
}
function ToggleButton({...props}) {
const {on, toggle} = useToggle()
return <Switch on={on} onClick={toggle} {...props} />
}
Finally we can have as many nesting elements as we would like, and this code would work as expected.
function App() {
return (
<>
<Toggle>
<ToggleOn>The button is on<ToggleOn/>
<ToggleOff>The button is off<ToggleOff/>
<div>
<span>Hello<span/>
</div>
<ToggleButton />
</>
<SomeLargeComponent/>
</>
)
}
This pattern enables component composition and code reuse by wrapping one or more components with a function. This function takes a component as an input and returns a new component with additional props, state, or behavior. HOCs are often used for cross-cutting concerns, such as authentication, logging, or data fetching, allowing us to encapsulate and share functionality across different parts of the application.
Let's say, that we always wanted to add a certain styling to multiple components in our application. Instead of creating a style object locally each time, we can simply create a HOC that adds the style objects to the component that we pass to it:
function withStyles(Component) {
return props => {
const style = { padding: '0.2rem', margin: '1rem' }
return <Component style={style} {...props} />
}
}
const Button = () = <button>Click me!<button/>
const Text = () => <p>Hello World!<p/>
const StyledButton = withStyles(Button)
const StyledText = withStyles(Text)
As we se we created a function withStyles, that is in fact a HOC. Key word "with" is always used for HOCs. The HOC withStyles is accepting a component and returning an anonymous component (component that does not have a name). This enables us to have different side effects, props and behavior on the component that was wrapped with the HOC.
Let's see a "bigger" example, just to get a "feel" for what can this HOC pattern be used for:
function withLoader(Element, url) {
return (props) => {
const [data, setData] = useState(null);
useEffect(() => {
async function getData() {
const res = await fetch(url);
const data = await res.json();
setData(data);
}
getData();
}, []);
if (!data) {
return <div>Loading...<div/>;
}
return <Element {...props} data={data} />;
};
}
As we can see, we have a HOC withLoader. This HOC can arguably be called withFetch, but that is besides the point. The HOC accepts two arguments, the component and a URL. Inside the HOC we return a component in which we fetch data using the provided URL. We handle the loading condition inside it as well, and finally return the provided component with props and the newly fetched data.
It is very simple to use, which we may see in the next code snippet:
function DogImages(props) {
return props.data.message.map((dog, index) => (
<img src={dog} alt="Dog" key={index} />
));
}
export default withLoader(
DogImages,
"https://dog.ceo/api/breed/labrador/images/random/6"
);
We can see in the above example that inside the DogImages wrapped component, we have the data fetched so we can list out the dog images.
There are other react patterns out there like: Controlled Components, Container and Presentational Components, State Reducer Pattern, Control Props Pattern and many more. Every pattern has its own use cases. There is no perfect pattern. These patterns that I covered in more detail are just some of the patterns, which i have personally found to be the most common in my career, and it's always nice to know your options and limitations when writing code.
Author: