DEV Community

Cover image for How to structure a React project to be expandable
Aki Rautio
Aki Rautio

Posted on

How to structure a React project to be expandable

One of the biggest reasons I like about React is that there are very few restrictions on how to do things. This also includes the structure of a project. This freedom also has its downsides. Choosing a poor structure may cause some trouble once the project starts to get bigger. The most commons signs are that the changes in one component will break multiple unrelated features, and creating comprehensive tests starts to be impossible.

While preparing this post, I ended up searching what others have written and oh boy, there are a lot of guides. Most famous of them all probably is the following Twitter post:

Though I still believe that certain good practices will ease and speed up the development in the long run.

Limiting nesting with Atomic design

If you haven't heard about Atomic design before, I would suggest reading articles from Brad Frost and Katia Wheeler first. The key point is that the whole UI part of the project has the following five levels:

  • Atoms
  • Molecules
  • Organisms
  • Templates
  • Pages

This structure has two types of advantages:

  • It limits nesting to only five levels. While nesting itself isn't a bad practice, having a huge amount of layers makes a component harder to reuse and maintain. Even React documentation encourages to avoid too much nesting.

  • It defines the responsibilities and expectations for each component level. Every page should have common parts (Template) and page specific parts (Organisms). And all organisms are then composed of molecules that are composed of atoms.

I have found both of the features very helpful for my projects because they give reasoning behind the content splitting into components. For example, if an atom has a lot of complex logic, it probably isn't an atom after all.

Besides, what Katia wrote, I have been trying to ensure that all components import only other components under it. In other words, the molecules should only import atoms and never other molecules. If I need to connect two molecules, then I would do it on the organism level. This makes connections more manageable because dependencies in the project look less like a spider web and more like a tree.

import * as React from 'react'
import { ListContainer, Container, Title, Value } from './atoms'

const List = ({ items = [], component: Component, ...props } ) => (
  <ListContainer>
    {items.map(item =>
      <Component {...item} {...props} />
    )}
  </ListContainer>
)

const ListItem = ({ name, value, onChange }) => (
  <Container>
    <Title>{name}</Title>
    <Value onChange={onChange}>{value}</Value>
  </Container>
)

const ListSetup = () => (
  <List 
    component={ListItem} 
    items={[
      { name: 'Name', value: 'value'}
    ]}
    onChange={() => console.log('Do something')}
  />
)
Enter fullscreen mode Exit fullscreen mode

Reusing the list component is very easy in this example because it can take any component that presents a list item. The new list item component only needs to have the same properties, and it works out of the box.

Structuring the state management

At some point in a project, there is a need to handle a state one way or another. This can be either simply adding a state handling to component or using a global state.

I have found that separating the state and presentation makes development easier in the long run. It centralizes the state under a few components and makes rest to be stateless. Stateless components are a lot easier to unit test due to lack of transitions, and on stateful components, we can purely focus on state changes. For example:

import * as React from 'react'

const Input = ({ name, value, onChange } ) => ( 
  <input name={name} value={value} onChange={onChange}/>
) 

const FormInput = ({ name }) => {
  const [value, setInput] = React.useState()
  const onChange = ({ target: { value} }) => setInput(value)
  return(
    <Input name={name} value={value} onChange={onChange} />
  )
}
Enter fullscreen mode Exit fullscreen mode

Common components

Aside from the split into stateless and stateful components, it's better to split components into page specific and common components. The common components should present commonly used parts of the project, like Typography and Form elements.

I have seen a lot of benefits to make every atom and molecule level components to be common, but this won't work for all. When low-level components are made commonly usable, they will be generic enough to make a benefit for other parts of the project too.

Feature-based development

Another commonly used practice to structure a project is to group the components by the feature. This makes the project easily extendable because every new feature will have a new structure.

With Javascript and NPM packages, there are two ways to do feature-based structuring. Either split the features to different directories inside the package or make every feature to be a separate package.

One package and multiple features:

├── package.json
└── src
    ├── feature1
    │   └── Feature1.jsx
    └── feature2
        └── Feature2.jsx
Enter fullscreen mode Exit fullscreen mode

Multiple packages and multiple features:

├── package.json
└── packages
    ├── Feature1
    │   ├── package.json
    │   └── src
    │       └── index.js
    └── Feature2
        ├── package.json
        └── src
            └── index.js
Enter fullscreen mode Exit fullscreen mode

Separate packages are commonly used in bigger projects and packages that have a lot of independent elements. Separate packages give more control over the project since packages are versioned independently. It also helps to show what packages used in which part of the application.

On the downside, separate packages create more work when moving components between features since both source and target feature needs a new version. I would suggest using separate packages only when it brings real advantages over a single package and once there is a clear vision of how to split the project.

Putting good practices into a real project

To summarize the good practices, let's create an example structure to show they work in a real project:

  • Components directory for the common components like typography and form elements. The elements in here would be done either from atoms or molecules level but never beyond that.

  • Pages directory to handle page-specific content. A single page should be composed of organisms and organisms should use only common components and atoms.

  • Data directory for all transition or business logic related components to keep presentation and state separately. Most of the stateful components of the project should be under the Data directory, and these components should be treated as organisms when used in pages. If a global state package like Redux is used, the component in the Data directory should act as a gateway between a global state and a presentation.

├── package.json
└── src
    ├── components
    │   ├── FormElements
    │   │   ├── Field
    │   │   │   ├── atoms
    │   │   │   │   ├── Error.jsx
    │   │   │   │   ├── index.js
    │   │   │   │   └── Label.jsx
    │   │   │   ├── Field.jsx
    │   │   │   └── index.js
    │   │   ├── Form
    │   │   │   ├── Form.jsx
    │   │   │   └── index.js
    │   │   ├── index.js
    │   │   └── Input
    │   │       ├── atoms
    │   │       │   ├── index.js
    │   │       │   ├── InputContainer.jsx
    │   │       │   └── InputItem.jsx
    │   │       ├── index.js
    │   │       └── Input.jsx
    │   └── Typography
    │       ├── Heading1.jsx
    │       └── index.js
    ├── data
    │   └── Login
    │       ├── index.js
    │       └── Login.jsx
    ├── pages
    │   └── LoginPage
    │       ├── index.js
    │       ├── LoginPage.jsx
    │       └── organisms
    │           ├── LoginForm
    │           └── LoginLoading
    │               ├── index.js
    │               └── LoginLoading.jsx
    └── templates
        └── Public
            ├── index.js
            └── Public.jsx

Enter fullscreen mode Exit fullscreen mode

The same idea will work for separate packages with one three small adjustments.

  • A components package would include all common components
  • Login package would include LoginPage-page and Login-data.
  • PublicLayout package would include public layout.

By following these practices, I have been able to expand the project without major restructuring, and that has kept the focus on project targets. In the beginning, the development is a slight bit slower because creating a library of common component take time. Once there starts to be a component for every common situation, the phase speeds up a lot.

Another big advantage I have seen with this structure is that testing becomes a lot easier because the snapshot testing is simple with stateless components.

Are you using the same kind of structure with React, or have you had trouble to find a proper structure for the application? Let me know in the comments!

Thanks for reading!

Top comments (1)

Collapse
 
juniorbatistadev profile image
Junior Batista

Pretty cool