This article originally appeared at bugfender.com: React Design Patterns (Part 2).
This article is the second part of the React Design Patterns article. If you missed the first part, go to part 1 of the series.
This time we'll be talking about the Context
pattern, the Presentational and Container Components
pattern, and the Compound Components
pattern.
Context
According to the React documentation:
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
In simple terms, If you have a global state that needs to pass through several component levels, you can use Context
. For example: if you have a theme
that influences all the components, Context
will streamline the process.
Note. There is one potential snag to bear in mind when using Context
: it can make the components less reusable. The Context
data will be available in the Provider
scope, so you can't use it outside the Provider
. I found a great video that explains this issue and tells you how to avoid 'prop drilling.'
Let's see an example of Context in action:
import React, { useContext, createContext } from "react";
import "./styles.css";
let data = {
title: "Welcome"
};
const Context = createContext();
export default function App() {
return (
<Context.Provider value={data}>
<div className="App">
<Card />
</div>
</Context.Provider>
);
}
const Card = () => {
return (
<div className="card">
<CardItem />
</div>
);
};
const CardItem = () => {
return (
<div className="CardItem">
<Title />
</div>
);
};
const Title = () => {
const data = useContext(Context);
return <h1>{data.title}</h1>;
};
As we can see in this (elementary) example, we have three levels of components, and we only use the data.title
in the last level. This way, we don't need to pass the props to all the levels.
A few tips on context syntax
I always apply this syntax when using context. However, there are some things I found out when I wrote it again:
- In the case of "static data" (like the example), we actually don't need the
Provider
. we can fulfil that function ourselves:
let data = {
title: "Welcome"
};
const Context = createContext(data);
export default function App() {
return (
<div className="App">
<Card />
</div>
);
}
At the other end of the scale, we can use the Customer
instead of useContext
, like this:
const Title = () => {
return (<Context.Consumer>
{(data) => <h1>{data.title}</h1>}
</Context.Consumer>);
};
Presentational and Container Components
These components (also known as Smart And Dumb Components
) are among the best-known React patterns. There are no references to them in the React documentation, but Dan Abramov's article provides an excellent guide.
In simple terms, Presentational And Container Components
refer to the separation of the business logic components from the UI views.
Let's look at another scenario:
- We need to build a
Card
component. - Inside the card, we have three other components:
Title
,Image
andButton
. - The button changes the picture after a click on it.
Before we start working on our components, let's create two folders: 'Presentational' and 'Container.' Now, let's build the three Presentational
components :
Title.js :
import React from "react";
export default function Title(props) {
const { children, ...attributes } = props;
return <h1 {...attributes}>{children}</h1>;
}
Image.js :
import React from "react";
export default function Image(props) {
const { src, alt } = props || {};
return <img src={src} alt={alt} />;
}
Button.js :
import React from "react";
export default function Button(props) {
const { children, ...attributes } = props;
return <button {...attributes}>{children}</button>;
}
Finally, we can build in the Container folder component, known as the Card
.
Card.js :
import React, { useEffect, useState } from "react";
import Title from "../Presentational/Title";
import Image from "../Presentational/Image";
import Button from "../Presentational/Button";
export default function Card() {
const [card, setCard] = useState({});
const [srcIndex, setSrcIndex] = useState(0);
useEffect(() => {
setCard({
title: "Card Title",
image: {
imagesArray: [
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTh87QN4DkF7s92IFSfm7b7S4IR6kTdzIlhbw&usqp=CAU",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRjFnHdaH1i1m_xOaJfXTyq4anRFwRyCg1p1Q&usqp=CAU"
],
alt: "card image"
}
});
}, []);
const { image } = card;
const changeImage = () =>
setSrcIndex((index) =>
index < image.imagesArray.length - 1 ? index + 1 : index - 1
);
return (
<div className="card">
<Title className="title-black">{card.title && card.title}</Title>
<Image
src={image && image.imagesArray[srcIndex]}
alt={image && image.alt}
/>
<Button onClick={changeImage}>Change Picture</Button>
</div>
);
}
If you want to see the full code, check it out here.
Note! Many of you may be wondering why you needed to separate into different components. You could just write them inside Card
, right?
Well when we separate the components, we can reuse them anywhere. But even more importantly, it is much easier to implement other patterns like HOC
or Render Props
.
Compound Components
In my opinion, this is one of the most intricate patterns to understand, but I will try to explain it as simply as I can.
When we talk about Compound Components
, the most simple way is to think about select
and option
in HTML. You can look at them as a group of components that have a basic functionality. There are states that are managed globally (like in the context
pattern) or from the container (like in presentational and container
patterns).
Compound components
are really a mixture of these two. It's almost as if they each have their owned states and they manage them from within.
Let's look at the next scenario:
- We need to develop
Select
andOption
components. - We want the
Option
to be vivid, with different colors. - The
Option
color will influence theSelect
color.
Let's see the example:
App.js
import React from "react";
import "./styles.css";
import Select from "./Select";
import Option from "./Option";
export default function App() {
return (
<div>
<Select>
<Option.Blue>option 1</Option.Blue>
<Option.Red>option 2</Option.Red>
<Option>option 3</Option>
</Select>
</div>
);
}
- The
App
renders theSelect
and theOption
components. -
Option.Blue
andOption.Red
are 'colors components.'
Option.js
sdsdimport React, { useEffect } from "react";
function Option(props) {
const { children, style, value, setStyle } = props;
useEffect(() => {
if (setStyle) setStyle({ backgroundColor: style.backgroundColor });
}, [setStyle, style]);
return (
<option value={value} style={style}>
{children}
</option>
);
}
Option.Blue = function (props) {
props.style.backgroundColor = "blue";
return Option(props);
};
Option.Red = function (props) {
props.style.backgroundColor = "red";
return Option(props);
};
export default Option;
- Here you can see the Implementation of
Option.Blue
andOption.Red
. As will be apparent, we render theOption
component and just add a property to props. - The
setStyle
comes fromSelect
. It's for changing the select color to the color of the selected option.
Select.js
import React, { useState } from "react";
export default function Select(props) {
const { children } = props;
const [style, setStyle] = useState({});
const findOptionActive = (e) => {
const index = e.target.value * 1;
const optionStyle = { ...e.nativeEvent.target[index].style };
if (optionStyle) setStyle({ backgroundColor: optionStyle.backgroundColor });
};
const childrenWithProps = React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
...child.props,
value: index,
setStyle:
index === 0 && Object.keys(style).length === 0 ? setStyle : null,
style: { backgroundColor: "white" }
});
});
return (
<select onChange={findOptionActive} style={style}>
{childrenWithProps}
</select>
);
}
- Now, we have a select function with the attributes of
onChange
style
. -
findOptionActive
gets the style of the option and changes the style of select accordingly, - The magic really happens in
childrenWithProps
. Normally, whenSelect
receiveschildren
, we can't access the child props - but with the help ofReact.Children
andReact.cloneElement
we can do it. As you can see, we can passvalue
,setStyle
, andstyle
as props.
To get the Full Code, click here.
This exercise gives you good practice, and if you want to try it yourself (maybe in another pattern), add your solution in a comment below.
Conclusion
This article was intended to show you different patterns in React. You don't need to use any of the patterns if you don't want to, but it's good for a developer to know design patterns on any framework or language, to understand different syntax levels when they see a new codebase.
I hope you enjoyed the tutorial and learned something new. If you know any other pattern or have further information on any of the topics mentioned in the article, please add a comment below.
Top comments (5)
You shouldn't mutate an argument that is passed as reference. It's safer to create a copy and change the color there.
Thank you for your input. It's a very important point. You are probably right, but I'm not sure because as I studied this pattern, I saw that the way I wrote it was the conventional way to write it. Still, it's a really good point, thank you!
With your Card example, I would be wary of advising people to abstract things as you have done there. A lot of times (especially in react and front-end development) people use overuse abstraction because they think they might reuse a particular component in the future. Bear in mind the "rule of three". Some level of duplication is okay. Abstraction shouldn't be the default.
Thank you, I glad you enjoyed my article. About your comment, I think you are partly correct. I think you can split my answer into two parts - real-world projects (very big projects) and side/learning projects (small-medium project)
But of course, there isn't only one correct way, mostly if you talk about design pattern please don't take my examples as is. Try yourself, and modify them to your needs.
Other than that, some great tips here. Learned a lot.