A Better File Structure For React/Redux Applications

https://marmelab.com/blog/2015/12/17/react-directory-structure.html

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!

Tip: 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/.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值