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.





Top comments (0)