A Better File Structure For React/Redux Applications

François Zaninotto
François ZaninottoDecember 17, 2015
#popular#js#react

Most of the examples I could find about React/Redux applications (either client side or universal) are very simple. They choose to organize files by nature (action, component, container, reducer). The result is a directory structure looking like the following:

actions/
    CommandActions.js
    UserActions.js
components/
    Header.js
    Sidebar.js
    Command.js
    CommandList.js
    CommandItem.js
    CommandHelper.js
    User.js
    UserProfile.js
    UserAvatar.js
containers/
    App.js
    Command.js
    User.js
reducers/
    index.js
    command.js
    user.js
routes.js

The Redux Book follows this convention, and I know at least two Redux boilerplate repositories following it, too: 3ree, and react-redux-universal-hot-example.

That's nice, but what happens when I need to add code about a new domain, including actions, components, and a reducer? For instance, if I want to deal with a catalog of products, I need to add files in all of these directories, ending with:

actions/
    CommandActions.js
    ProductActions.js   <= Here
    UserActions.js
components/
    Header.js
    Sidebar.js
    Command.js
    CommandList.js
    CommandItem.js
    CommandHelper.js
    Product.js          <= Here
    ProductList.js      <= Here
    ProductItem.js      <= Here
    ProductImage.js     <= Here
    User.js
    UserProfile.js
    UserAvatar.js
containers/
    App.js
    Command.js
    Product.js          <= Here
    User.js
reducers/
    index.js
    foo.js
    bar.js
    product.js          <= Here
routes.js

You see where this is going. Fast forward two months from now, and the components/ directory contains dozens of files, and I need to open 4 files in 4 different directories each time I touch a single feature.

Why not group files by domain instead? To make the difference between actions, components, and reducers, I can still use a file suffix:

app/
    Header.js
    Sidebar.js
    App.js
    reducers.js
    routes.js
command/
    Command.js
    CommandContainer.js
    CommandActions.js
    CommandList.js
    CommandItem.js
    CommandHelper.js
    commandReducer.js
product/
    Product.js
    ProductContainer.js
    ProductActions.js
    ProductList.js
    ProductItem.js
    ProductImage.js
    productReducer.js
user/
    User.js
    UserContainer.js
    UserActions.js
    UserProfile.js
    UserAvatar.js
    userReducer.js

I can make things even a little easier to read by merging a container and the related component. Redux makes the difference between containers, which are connected to the state, and components, which are dumb and stateless. Most tutorials reflect this difference with two separate files:

// in Product.js
export default function Product({ name, description }) {
    return <div>
        <h1>{ name }</h1>
        <div className="description">
            {description}
        </div>
    </div>
}

// in ProductContainer.js
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as ProductActions from './ProductActions';
import Product from './Product';

function mapStateToProps(state) {
    return {...state};
}

function mapDispatchToProps(dispatch) {
    return bindActionCreators({
        ...ProductActions,
    }, dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(Product);

The only practical interest to separate component and container is to facilitate the unit tests of the component (without using Redux at all). In 99% of the cases, the component is never used outside of the container. Well, ES6 allows to export more than one element, right? Then I can merge those two scripts into a single file, where the export default is the container, and export Product is the component:

// in Product.js
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as ProductActions from './ProductActions';

// component part
export function Product({ name, description }) {
    return <div>
        <h1>{ name }</h1>
        <div className="description">
            {description}
        </div>
    </div>
}

// container part
function mapStateToProps(state) {
    return {...state};
}

function mapDispatchToProps(dispatch) {
    return bindActionCreators({
        ...ProductActions,
    }, dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(Product);

That way, a unit test on the component can simply import { Product } from './Product.js'. Now the directory structure counts one less file per directory:

app/
    Header.js
    Sidebar.js
    App.js
    reducers.js
    routes.js
command/
    Command.js         // component & container
    CommandActions.js
    CommandList.js
    CommandItem.js
    CommandHelper.js
    commandReducer.js
product/
    Product.js         // component & container
    ProductActions.js
    ProductList.js
    ProductItem.js
    ProductImage.js
    productReducer.js
user/
    User.js            // component & container
    UserActions.js
    UserProfile.js
    UserAvatar.js
    userReducer.js

And while we're talking about tests, they usually live in their own test/ directory, far from the runtime code:

src/
    app/
        Header.js
        Sidebar.js
        App.js
        reducers.js
        routes.js
    command/
        Command.js
        CommandActions.js
        CommandList.js
        CommandItem.js
        CommandHelper.js
        commandReducer.js
    product/
        Product.js
        ProductActions.js
        ProductList.js
        ProductItem.js
        ProductImage.js
        productReducer.js
    user/
        User.js
        UserActions.js
        UserProfile.js
        UserAvatar.js
        userReducer.js
test/
    app/
        Header.js
        Sidebar.js
        App.js
        reducers.js
        routes.js
    command/
        Command.js
        CommandActions.js
        CommandList.js
        CommandItem.js
        CommandHelper.js
        commandReducer.js
    product/
        Product.js
        ProductActions.js
        ProductList.js
        ProductItem.js
        ProductImage.js
        productReducer.js
    user/
        User.js
        UserActions.js
        UserProfile.js
        UserAvatar.js
        userReducer.js

I find it harder to spot missing tests for components, or to navigate the file structure once the domain expands. So I try to keep tests in the same directory as the element they test - simply using a -spec.js suffix. If this were Python, tests would even be in the same file! All the scripts related to a bounded context, including tests, are now grouped in a single directory - easy to read and reason about.

src/
    app/
        Header.js
        Header-spec.js
        Sidebar.js
        Sidebar-spec.js
        App.js
        App-spec.js
        reducers.js
        reducers-spec.js
        routes.js
        routes-spec.js
    command/
        Command.js
        Commands-spec.js
        CommandActions.js
        CommandActions-spec.js
        CommandList.js
        CommandList-spec.js
        CommandItem.js
        CommandItem-spec.js
        CommandHelper.js
        CommandHelper-spec.js
        commandReducer.js
        commandReducer-spec.js
    product/
        Product.js
        Product-spec.js
        ProductActions.js
        ProductActions-spec.js
        ProductList.js
        ProductList-spec.js
        ProductItem.js
        ProductItem-spec.js
        ProductImage.js
        ProductImage-spec.js
        productReducer.js
        productReducer-spec.js
    user/
        User.js
        User-spec.js
        UserActions.js
        UserActions-spec.js
        UserProfile.js
        UserProfile-spec.js
        UserAvatar.js
        UserAvatar-spec.js
        userReducer.js
        userReducer-spec.js

Configuring the test runner (either Jest or Mocha) is easy: just make it run the tests in ./src/**/*-spec.js.

This directory structure grows well with the project size. And when it's time to split an app into independent repos to ease reusability across projects, then the code refactoring is really lightweight. I highly recommend it!

If you're interested in advanced Redux usage, check out admin-on-rest, a React toolkit for building admin interfaces on top of REST APIs that we released in 2016.

Edit: It seems the Reddit community heard about this post; the discussion continues there: https://www.reddit.com/r/reactjs/comments/47mwdd/a_better_file_structure_for_reactredux/.

Did you like this article? Share it!