DEV Community

Sascha Becker
Sascha Becker

Posted on • Originally published at Medium on

Structure your react apps the mantra way

The holy grail of discussion is what is the best way to structure your project. The answer is, there is none. But here is one I fell in love with.

Then mantra way!

First of let’s talk about what makes a structure good. There are certain elements that make good use of organisation.

  • Easy to navigate
  • Scales well over time
  • Swap logic is fast and without many breaks

So what exactly is mantra? I’m not talking about meditate while creating folders. It’s the mantra js spec application architecture developed by kadira back when meteor js was the cool kid.

It described an architecture that should be maintainable and future proof. They claimed once set up it will satisfy your inner geek for up to five years. Not even kadira lasted that long by the way, nor will maybe meteor.

So I moved on, turned my back from meteor and dived into the raw package land. And mantra js was the one thing that I loved and tried to embed.

Let’s take a look of what a project could look like. Here is an example of a website with multiple routes, components, actions and reducers.

Stay with me. It’s not that confusing as it seems.

Modules

Let’s break it down and we start with the modules folder. It’s the core idea of mantra and what makes it so great in my opinion.

Modules is business concern first. That means I don’t split it into

  • components
  • containers
  • reducers
  • actions

right here. Instead mantra adds a layer on top to achieve better maintainability and hopefully makes this future proof.

One important rule is that you don’t cascade modules. If a module should have a submodule create it separately. The advantage is that you see all modules at a glance. You don’t have to open every module to see what’s inside.

Module content

But what’s inside a module?

Components folder holds every dumb component you need in that module and that module alone. Separation of concern is key here. You can read more about here.

**import** React, { Component } **from**'react'
**import** Text **from**'../../core/components/Text'
**import** \* **as** colors **from**'material-ui/styles/colors'
**import** GameTile **from**'./GameTile'

**export default class** Games **extends** Component {
 render () {
**const** {
 mobile
 } = **this**.props

**return** (
 \<div\>
 some content
 \</div\>
 )
 }
}
Enter fullscreen mode Exit fullscreen mode

Containers folder holds the logic. What should be pushed into my dumb components? What data should be fetched (via dispatching actions or redux state)? All those concerns should be stored in here.

**import** React, { Component } **from**'react'
**import** { connect } **from**'react-redux'
**import** Games **from**'../components/Games'
**import** { actions **as** coreActions } **from**'../../core'

**class** Container **extends** Component {
 componentDidMount () {
**this**.props.dispatch(coreActions.setMenuIndex(1))
 }

render () {
**return** (
 \<Games
 {... **this**.props}
 /\>
 )
 }
}

**export default** connect((state) =\> {
**return** {
 mobile: state.core.responsive.mobile
 }
})(Container)
Enter fullscreen mode Exit fullscreen mode

As you can see I introduced a “core module” to make shared actions happen. In some point of time you can’t isolate modules completely. Some actions have to be shared and I think that’s a good compromise.

Routes folder holds, as the name suggests, all the routes that belong to that module. In this case we have a “/games” route that needs to be hooked to the games container.

**import** React **from**'react'
**import** { Route } **from**'react-router-dom'
**import** Games **from**'../containers/Games'

**export default** (store) =\> {
**return** (
 \<Route exact path='/games' component={Games}/\>
 )
}
Enter fullscreen mode Exit fullscreen mode

Additional routes for example “/games/:id” are also welcome here.

Additional folders

Actions and reducers also need a place to live.

Actions folder holds the action types for redux calls.

**export const** MENU\_TOGGLE = 'MENU\_TOGGLE'
**export const** SET\_RESPONSIVE\_BREAKPOINT = 'SET\_RESPONSIVE\_BREAKPOINT'
**export const** SET\_MENU\_INDEX = 'SET\_MENU\_INDEX'
Enter fullscreen mode Exit fullscreen mode

And the actual actions which can be dispatched.

**import** \* **as** TYPES **from**'./actionTypes'

**export function** toggleMenu (open) {
**return** {type: TYPES.MENU\_TOGGLE, open}
}
**export function** setResponsiveBreakpoint (value) {
**return** {type: TYPES.SET\_RESPONSIVE\_BREAKPOINT, value}
}
**export function** setMenuIndex (index) {
**return** {type: TYPES.SET\_MENU\_INDEX, index}
}
Enter fullscreen mode Exit fullscreen mode

Reducers folder holds obviously the reducers for that module.

**import** \* **as** TYPES **from**'../actions/actionTypes'

**const** defaultState = {
 menu: {
 open: **false** ,
 index: -1
 },
 mobileView: **true** ,
 responsive: {
 mobile: **false** ,
 tablet: **false** ,
 desktop: **true**  
}
}

**function** toggleMenu (state, action) {
**return** {
 ...state,
 menu: {
 ...state.menu,
 open: **typeof** action.open === 'undefined' ? !state.menu.open : action.open
 }
 }
}

**function** setMenuIndex (state, action) {
**return** {
 ...state,
 menu: {
 ...state.menu,
 index: action.index
 }
 }
}

**function** setResponsiveBreakpoint (state, action) {
**return** {
 ...state,
 responsive: {
 mobile: action.value \<= 768,
 tablet: action.value \> 768 && action.value \<= 1200,
 desktop: action.value \> 1200
 }
 }
}

**export default function** (state = defaultState, action) {
**switch** (action.type) {
**case** TYPES.MENU\_TOGGLE:
**return** toggleMenu(state, action)
**case** TYPES.SET\_RESPONSIVE\_BREAKPOINT:
**return** setResponsiveBreakpoint(state, action)
**case** TYPES.SET\_MENU\_INDEX:
**return** setMenuIndex(state, action)
**default** :
**return** state
 }
}
Enter fullscreen mode Exit fullscreen mode

You can also split this into multiple files if you want.

And now comes the important part, the glue! After you defined your folders with its logic and visual components you want to expose it, right? That’s where the index.js of the module folder comes in. It collects all the needed parts of the module and expose it for further use. Something like this:

**import** \* **as** actions **from**'./actions'
**import** reducers **from**'./reducers'
**import** routes **from**'./routes'

**export** {
 actions,
 reducers,
 routes
}
Enter fullscreen mode Exit fullscreen mode

So everytime you need access to this module you have all the important parts right in the root of the module folder.

Make it run

So now comes the fun part. After defining our modules we need to deploy them within our application. Let’s take a look back to our overview:

First let’s combine all the reducers. In “src/reducers/index.js” we bundle all the reducers exposed by our modules.

**export** { reducer as core } **from**'../modules/core'
**export** { reducer as games } **from**'../modules/games'
**export** { reducer as anotherModule } **from**'../modules/anotherModule'
Enter fullscreen mode Exit fullscreen mode

This can be later used to combine our store just with:

**import** \* **as** reducers **from**'../reducers'
Enter fullscreen mode Exit fullscreen mode

And all our reducers from all defined modules are ready to go. Neat!

Same goes for the routes. Take a look inside the AppRoutes.js content:

**import** React **from**'react'
**import** { Switch } **from**'react-router-dom'

**import** Application **from**'./modules/core/containers/Application'
**import** { routes **as** home } **from**'./modules/home'
**import** { routes **as** games } **from**'./modules/games'
**import** { routes **as** team } **from**'./modules/team'
**import** { routes **as** contact } **from**'./modules/contact'
**import** { routes **as** impressum } **from**'./modules/impressum'

**export default** (store) =\> {
**return** (
 \<Switch\>
 \<Application\>
 {home(store)}
 {games(store)}
 {team(store)}
 {contact(store)}
 {impressum(store)}
 \</Application\>
 \</Switch\>
 )
}
Enter fullscreen mode Exit fullscreen mode

This file just exposed all routes defined in our modules. They are embedded in an shared Application component defined in the core module. Application is the wrapper with header and footer and takes children as prop.

Now just import those routes in your Root.js and everything is ready to deploy.

**import** React **from**'react'
**import** { Provider } **from**'react-redux'
**import** \* **as** OfflinePluginRuntime **from**'offline-plugin/runtime'

**import** configureStore **from**'./configs/configureStore'
**import** createRoutes **from**'./AppRoutes'

**import** createHistory **from**'history/createBrowserHistory'
**import** { ConnectedRouter } **from**'react-router-redux'

**import** MuiThemeProvider **from**'material-ui/styles/MuiThemeProvider'
**import** getMuiTheme **from**'material-ui/styles/getMuiTheme'
**import** theme **from**'./theme'

**import**'./main.less'

**const** history = createHistory()

**const** store = configureStore({}, history)

OfflinePluginRuntime.install()

**export default** () =\> (
 \<Provider store={store}\>
 \<MuiThemeProvider muiTheme={getMuiTheme(theme)}\>
 \<ConnectedRouter history={history}\>
 {createRoutes(store)}
 \</ConnectedRouter\>
 \</MuiThemeProvider\>
 \</Provider\>
)
Enter fullscreen mode Exit fullscreen mode

Conclusion

We now have several modules that are isolated from each other. That makes them easy to swap out for others. Also import statements are now extremely short. Further changes inside a module make distraction while coding from another module obsolete. Maintainability is far better than searching for files associated with a specific concern. It scales well due to just a few glue points needed and less clutter inside the modules folder.

Supplement

I wrote a npm package to hide most of the preparations needed to use this structure and do most of this behind the curtain.

TeamWertarbyte/module-loader

It’s still in an early stage an every PR and comment is welcome. But I already use this in production so I’m pretty confident that this will work as intended.


Top comments (0)