Structure your react apps the mantra way

Sascha Becker
Wertarbyte
Published in
7 min readSep 18, 2017

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>
)
}
}

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)

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}/>
)
}

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'

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}
}

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
}
}

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
}

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'

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

import * as reducers from '../reducers'

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>
)
}

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>
)

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.

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.

--

--