DEV Community

loading...
Cover image for Scalable React Components architecture

Scalable React Components architecture

Carlos Cuesta
Lead Front End Engineer @ulabox • 25 Years old • JavaScript, ReactJS and OpenSource ❤️
Originally published at carloscuesta.me ・4 min read

Originally posted at carloscuesta's blog

It's been a while since I've started working with React and React-Native in production. One of the greatest things about React is the flexibility the library gives to you. Meaning that you are free to decide how do you want to implement almost every detail of your project for example the architecture and structure.

However this freedom on the long term, could lead to a complex and messy codebase, specially if you don't follow a pattern. In this post I'll explain a simple way to organize and structure React Components.

A Component is a JavaScript function or class that returns a piece of UI.

We're going to create an EmojiList component and then we are going to refactor it breaking it up into smaller isolated pieces applying the folder pattern. Here's how our component looks like:

emojilist

EmojiList

As I mentioned before, we can start really simple and small, without following any pattern. This is our EmojiList component contained in a single function.

Open Demo on CodeSandbox

If you open the CodeSandbox sidebar you'll see that our file tree looks like this:

.
├── components
│   ├── EmojiList.js
│   └── styles.js
└── index.js
Enter fullscreen mode Exit fullscreen mode

There's nothing wrong with this approach. But on larger codebases that kind of component becomes hard to maintain, because there a lot of things in it: state, ui, data... Take a look at our component code below 👇

EmojiList.js

import React from "react"

import styles from "./styles"

class EmojiList extends React.Component {
  state = {
    searchInput: "",
    emojis: []
  }

  render() {
    const emojis = this.state.emojis.filter(emoji =>
      emoji.code.includes(this.state.searchInput.toLowerCase())
    )

    return (
      <ul style={styles.list}>
        <input
          style={styles.searchInput}
          placeholder="Search by name"
          type="text"
          value={this.state.searchInput}
          onChange={event => this.setState({ searchInput: event.target.value })}
        />
        {emojis.map((emoji, index) => (
          <li key={index} style={styles.item}>
            <div style={styles.icon}>{emoji.emoji}</div>
            <div style={styles.content}>
              <code style={styles.code}>{emoji.code}</code>
              <p style={styles.description}>{emoji.description}</p>
            </div>
          </li>
        ))}
      </ul>
    )
  }
}

export default EmojiList
Enter fullscreen mode Exit fullscreen mode

A step to improve this code, would be to create separate components into the same file and then using them at the main component. However, you'll be sharing styles among other things and that could be confusing.

Refactor

Let's start refactoring the single component into multiple ones by breaking up the UI into a component hierarchy.

emojilist-breakdown

If we take a look at the image, it's easy to identify that we can break up our UI in three different components: 🛠

  • EmojiList: Combines the smaller components and shares the state down.
    • SearchInput: Receives user input and displays the search bar.
    • EmojiListItem: Displays the List Item for each emoji, with the icon, name and description.

We're going to create a folder for each component, with two files, an index.js that is going to hold all the code for the component and the styles.js. That's one of the good things about this pattern. Every component defines his own UI and styles, isolating this piece of code from another components that doesn't need to know anything about them.

Open Demo on CodeSandbox

Notice that inside the EmojiList folder, (that is a component), we add two nested components that only will be used within the EmojiList component. Again, that's because these two components aren't going to be used out of that context. This helps reducing the visual clutter a lot.

.
├── EmojiList
│   ├── EmojiListItem
│   │   ├── index.js
│   │   └── styles.js
│   ├── SearchInput
│   │   ├── index.js
│   │   └── styles.js
│   ├── index.js
│   └── styles.js
└── index.js
Enter fullscreen mode Exit fullscreen mode

Now let's isolate and separate the code into the three components from the smallest to the biggest one:

EmojiListItem/

This component renders every emoji item that will appear on the list.

import React from "react"

import styles from "./styles"

const EmojiListItem = (props) => (
  <li style={styles.item}>
    <div style={styles.icon}>{props.emoji}</div>
    <div style={styles.content}>
      <code style={styles.code}>{props.code}</code>
      <p style={styles.description}>{props.description}</p>
    </div>
  </li>
)

export default EmojiListItem
Enter fullscreen mode Exit fullscreen mode

SearchInput/

This component receives the user input and updates the state of the parent component.

import React from "react"

import styles from "./styles"

const SearchInput = (props) => (
  <input
    style={styles.searchInput}
    placeholder="Search by name"
    type="text"
    value={props.value}
    onChange={props.onChange}
  />
)

export default SearchInput
Enter fullscreen mode Exit fullscreen mode

EmojiList/

This is the top level component, holds the state and data of our example and imports the other components to recreate the whole UI of our tiny application. Isolating components makes the render method more readable and easier to understand ✨.

import React from "react"

import SearchInput from "./SearchInput"
import EmojiListItem from "./EmojiListItem"
import styles from "./styles"

class EmojiList extends React.Component {
  state = {
    searchInput: "",
    emojis: []
  }

  render() {
    const emojis = this.state.emojis.filter(emoji =>
      emoji.code.includes(this.state.searchInput.toLowerCase())
    )

    return (
      <ul style={styles.list}>
        <SearchInput
          onChange={(event) => this.setState({ searchInput: event.target.value })}
          value={this.state.searchInput}
        />
        {emojis.map((emoji, index) => (
          <EmojiListItem
            key={index}
            code={emoji.code}
            description={emoji.description}
            emoji={emoji.emoji}
          />
        ))}
      </ul>
    )
  }
}

export default EmojiList
Enter fullscreen mode Exit fullscreen mode

That's basically the architecture that I use at the company I'm working on. I'm pretty satisfied with the experience of using this pattern. Our components turned out a lot easier to maintain and use. Anyway there are no silver bullets on Software Engineering, so figure what works best for you or your team!

Discussion (2)

Collapse
frenkix profile image
Goran Jakovljevic • Edited

Thanks for the article.

You have the input as a child of ul, you should wrap it inside li or semantically, move it above UL.

I like the idea behind folder pattern, but I am not a fan of having tons of index.js files. I suppose you set the editor to show the folder name of the opened file as well or else you would be having lots of index.js files and it would be impossible to switch between them.

I like the container component pattern. You have a container component that does all the data fetching and formatting, and then you have the view component that only acts as the view/presentational component. Its actually very similar to folder pattern except it uses different names and in most cases does not contain folders but it depends on use/case and how many child views main component has.

Collapse
carloscuesta profile image
Carlos Cuesta Author

Yeah semantically, the Input should be placed out of the ul element.

In my opinion, tools should not change the way we structure and code our projects. Tools must be improved and changed if needed. On my specific use case I use Atom and I switch between files using the ⌘+P and the name of the folder.

Thanks for sharing your opinion!