Redux: organising containers, components, actions and reducers
Asked Answered
B

1

6

The question:

What is the most maintainable and recommended best practice for organising containers, components, actions and reducers in a large React/Redux application?

My opinion:

Current trends seem to organise redux collaterals (actions, reducers, sagas...) around the associated container component. e.g.

/src
    /components
        /...
    /contianers
        /BookList
            actions.js
            constants.js
            reducer.js
            selectors.js
            sagas.js
            index.js
        /BookSingle
            actions.js
            constants.js
            reducer.js
            selectors.js
            sagas.js
            index.js        
    app.js
    routes.js

This works great! Although there seems to be a couple of issues with this design.

The Issues:

When we need to access actions, selectors or sagas from another container it seems an anti-pattern. Let's say we have a global /App container with a reducer/state that stores information we use over the entire app such as categories and enumerables. Following on from the example above, with a state tree:

{
    app: {
        taxonomies: {
            genres: [genre, genre, genre],
            year: [year, year, year],
            subject: [subject,subject,subject],
        }   
    }
    books: {
        entities: {
            books: [book, book, book, book],
            chapters: [chapter, chapter, chapter],
            authors: [author,author,author],
        }
    },
    book: {
        entities: {
            book: book,
            chapters: [chapter, chapter, chapter],
            author: author,
        }
    },
}   

If we want to use a selector from the /App container within our /BookList container we need to either recreate it in /BookList/selectors.js (surely wrong?) OR import it from /App/selectors (will it always be the EXACT same selector..? no.). Both these appraoches seem sub-optimal to me.

The prime example of this use case is Authentication (ah... auth we do love to hate you) as it is a VERY common "side-effect" model. We often need to access /Auth sagas, actions and selectors all over the app. We may have the containers /PasswordRecover, /PasswordReset, /Login, /Signup .... Actually in our app our /Auth contianer has no actual component at all!

/src
    /contianers
        /Auth
            actions.js
            constants.js
            reducer.js
            selectors.js
            sagas.js

Simply containing all the Redux collaterals for the various and often un-related auth containers mentioned above.

Belligerency answered 14/7, 2016 at 23:42 Comment(1)
I am curious, with your current structure how are you using your selector? Say a component uses BookList selectors functions, can you show me your mapStateToProps function? are you passing the state through? or the state.booklistWeary
C
5

I personally use the ducks-modular-redux proposal.

It's not the "official" recommended way but it works great for me. Each "duck" contains a actionTypes.js, actionCreators.js, reducers.js, sagas.js and selectors.js files. There is no dependency to other ducks in these files to avoid cyclic dependency or duck circle, each "duck" contains only the logic that it have to managed.

Then, at the root I have a components and a containers folders and some root files :

components/ folder contains all the pure components of my app

containers/ folder contains containers created from pure components above. When a container need a specific selector involving many "ducks", I write it in the same file where I wrote the <Container/> component since it is relative to this specific container. If the selector is shared accros multiple containers, I create it in a separate file (or in a HoC that provides these props).

rootReducers.js : simply exposes the root reducers by combining all reducers

rootSelectors.js exposes the root selector for each slice of state, for example in your case you could have something like :

/* let's consider this state shape

state = {
    books: {
        items: {  // id ordered book items
            ...
        }
    },
    taxonomies: {
        items: {  // id ordered taxonomy items
            ...
        }
    }
}

*/
export const getBooksRoot = (state) => state.books

export const getTaxonomiesRoot = (state) => state.taxonomies

It let us "hide" the state shape inside each ducks selectors.js file. Since each selector receive the whole state inside your ducks you simply have to import the corresponding rootSelector inside your selector.js files.

rootSagas.js compose all the sagas inside your ducks and manage complex flow involving many "ducks".

So in your case, the structure could be :

components/
containers/
ducks/
    Books/
        actionTypes.js
        actionCreators.js
        reducers.js
        selectors.js
        sagas.js
    Taxonomies/
        actionTypes.js
        actionCreators.js
        reducers.js
        selectors.js
        sagas.js
rootSelectors.js
rootReducers.js
rootSagas.js

When my "ducks" are small enough, I often skip the folder creation and directly write a ducks/Books.js or a ducks/Taxonomies.js file with all these 5 files (actionTypes.js, actionCreators.js, reducers.js, selectors.js, sagas.js) merged together.

Courtier answered 15/7, 2016 at 10:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.