DEV Community

Tomer Raitz
Tomer Raitz

Posted on • Originally published at bugfender.com

React design patterns (part 2)

alt text

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>;
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>);
};
Enter fullscreen mode Exit fullscreen mode

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: TitleImage and Button.
  • 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>;
}
Enter fullscreen mode Exit fullscreen mode

Image.js :

import React from "react";
export default function Image(props) {
  const { src, alt } = props || {};
  return <img src={src} alt={alt} />;
}
Enter fullscreen mode Exit fullscreen mode

Button.js :

import React from "react";
export default function Button(props) {
  const { children, ...attributes } = props;
  return <button {...attributes}>{children}</button>;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 and Option components.
  • We want the Option to be vivid, with different colors.
  • The Option color will influence the Select 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>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • The App renders the Select and the Option components.
  • Option.Blue and Option.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;
Enter fullscreen mode Exit fullscreen mode
  • Here you can see the Implementation of Option.Blue and Option.Red. As will be apparent, we render the Option component and just add a property to props.
  • The setStyle comes from Select. 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>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • 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, when Select receives children, we can't access the child props - but with the help of React.Children and React.cloneElement we can do it. As you can see, we can pass valuesetStyle, and style 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)

Collapse
 
canastro profile image
Ricardo Canastro
Option.Blue = function (props) {
  props.style.backgroundColor = "blue";
  return Option(props);
};
Enter fullscreen mode Exit fullscreen mode

You shouldn't mutate an argument that is passed as reference. It's safer to create a copy and change the color there.

return Option({ 
  ...props, 
  style: { ...props.style, backgroundColor: 'blue' } 
});
Enter fullscreen mode Exit fullscreen mode
Collapse
 
tomeraitz profile image
Tomer Raitz

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!

Collapse
 
wutangpaul profile image
Paul McClean

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.

Collapse
 
tomeraitz profile image
Tomer Raitz

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)

  • big projects - In big projects, you are right. You shouldn't break your components into small parts because it's tough to manage them and "remember" what you already have.
  • small - medium projects - In my opinion, it's a good practice to break your components into small parts in this case. Because when you will work on a big codebase, you will have this design pattern as part of your abilities (you just need to modify it to fit big projects design).

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.

Collapse
 
wutangpaul profile image
Paul McClean

Other than that, some great tips here. Learned a lot.