-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Fractal Project Structure
Also known as: Self-Contained Apps, Recursive Route Hierarchy, Providers, etc
Small applications can be built using a flat directory structure, with folders for components
, containers
, etc. However, this structure does not scale and can seriously affect development velocity as your project grows. Starting with a fractal structure allows your application to organically drive its own architecture from day one.
This structure provides many benefits that may not be immediately obvious:
- Routes can be bundled into "chunks" using webpack's code splitting and merging algorithm. This means that the entire dependency tree for each route can be omitted from the initial bundle and then loaded on demand.
- Since logic is self-contained, routes can easily be broken into separate repositories and referenced with webpack's DLL plugin for flexible, high-performance development and scalability.
Large, mature apps tend to naturally organize themselves in this way—analogous to large, mature trees (as in actual trees 🌲). The trunk is the router, branches are route bundles, and leaves are views composed of common/shared components/containers. Global application and UI state should be placed on or close to the trunk (or perhaps at the base of a huge branch, eg. /app
route).
Note: We recommend keeping your store flat, which is not strictly fractal. However, this structure provides a rock-solid foundation for creating or migrating to truly fractal apps by dropping in frameworks such as redux-elm.
We use react-router
route definitions (<route>/index.js
) to define units of logic within our application.
It's important to understand how webpack integrates with react-router
to implement code splitting, and how everything is tied together with redux. Let's dissect the counter route definition:
/* 1. ReactRouter - Create PlainRoute definition object */
export default (store) => ({
path: 'counter',
/* 2. ReactRouter - Invoked when path match (lazy) */
getComponent (nextState, cb) {
/* 3. Webpack (build) - Create split point
4. Webpack (runtime) - Load async chunk with embedded jsonp client */
require.ensure([], (require) => {
/* 5. Webpack (build) - Require all bundle contents */
const Counter = require('./containers/CounterContainer').default
const reducer = require('./modules/counter').default
/* 6. Redux - Use store and helper to add async reducer */
injectReducer(store, { key: 'counter', reducer })
/* 7. ReactRouter - Return component */
cb(null, Counter)
/* 8. Webpack - Provide a name for bundle */
}, 'counter')
}
})
-
ReactRouter - We export a function that accepts the instantiated store for async reducer/middleware/etc injection and returns a
PlainRoute
object evaluated by react-router during application bootstrap. -
ReactRouter - The
getComponent
callback is registered but it is not invoked until the route is matched, so it is the perfect place to encapsulate and load our bundled logic at runtime. -
Webpack (Build) - Webpack uses the
require.ensure
callback to create a split point and replaces it with a call to it's own internaljsonp
client with relevant module information. - Webpack (Runtime) - Webpack loads your bundle over the network.
- Webpack (Build) - Webpack walks the required dependency tree and runs a chunking algorithm to merge modules into an async bundle, also known as code-splitting.
-
Redux - Use
injectReducer
helper and instantiated store to injectcounter
reducer on key 'counter' -
ReactRouter - Pass resolved component back up to
Router
(using CPS callback signature) -
Webpack (Build) - Create named chunk using
require.ensure
callback
Notes
- Your entire route hierarchy can and should be loaded during application bootstrap, since code-splitting and bundle loading happens lazily in
getComponents
the route definitions should be registered in advance! - Additional child routes can be nested in a fractal hierarchy by adding
childRoutes
- This structure is designed to provide a flexible foundation for module bundling and dynamic loading
- Using a fractal structure is optional, smaller apps might benefit from a flat routes directory
Usage with JSX
We recommend using POJO (Plain Old Javascript Object) route definitions, however you can easily integrate them with JSX routes using React Router's createRoutes
helper. Example of POJO routes using JSX:
// ...
import SubRoutes from './routes/SubRoutes' // JSX Routes
export default {
path: '/component',
component: Component,
children: createRoutes(SubRoutes)
}
- Alternatively, the JSX route definition file can
export default createRoutes(<Route />)
- JSX can easily use POJO routes by passing them as a prop, ie
<Route children={PlainRoute} />
Above all, you should seek to find the best solution for the problem you are trying to solve. This setup will not fit every use case, but it is extremely flexible. There is no "right" or "wrong" way to set up your project. Here are some general recommendations that we have found useful. If you would like to add something, please submit a PR.
- A route directory...
-
Should contain an
index.js
that returns route definition - Optional: assets, components, containers, redux modules, nested child routes
- Additional child routes can be nested within
routes
directory in a fractal hierarchy
-
Should contain an
- Your store should not reflect the hierarchy of your folder structure
- Keep your store as flat and normalized as possible. If you are dealing with deeply nested data structures, we recommend using a tool such as normalizr.
- Note that the
injectReducer
helper can be repurposed to suit your needs.
- Stateless components that dictate major page structure
- Useful for composing
react-router
named components into views
- Prefer stateless functional components
- eg:
const HelloMessage = ({ name }) => <div>Hello {name}</div>
- eg:
- Top-level
components
andcontainers
directories contain reusable components
- Containers only
connect
presentational components to actions/state- Rule of thumb: no JSX in containers!
- One or many container components can be composed in a stateless functional components
- Tip: props injected by
react-router
can be accessed usingconnect
:// CounterWithMusicContainer.js import { connect } from 'react-redux' import Counter from 'components/Counter' export const mapStateToProps = (state, ownProps) => ({ counter: state.counter, music: ownProps.location.query.music // why not }) export default connect(mapStateToProps)(Counter) // Location -> 'localhost:3000/counter?music=reggae' // Counter.props = { counter: 0, music: 'reggae' }