Skip to content
This repository has been archived by the owner on Sep 11, 2018. It is now read-only.

Fractal Project Structure #684

Merged
merged 20 commits into from
Apr 21, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 83 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ Features
* [React](https://github.com/facebook/react) (`^15.0.0`)
* [Redux](https://github.com/rackt/redux) (`^3.0.0`)
* react-redux (`^4.0.0`)
* redux-devtools
* redux-thunk middleware
* [react-router](https://github.com/rackt/react-router) (`^2.0.0`)
* Asynchronous routes configured with dependencies and reducers
* [react-router-redux](https://github.com/rackt/react-router-redux) (`^4.0.0`)
* [Webpack](https://github.com/webpack/webpack)
* Vanilla HMR using `module.hot` and `webpack-dev-middleware`
* Code-splitting using `react-router` route configuration
* Bundle splitting and CSS extraction
* Sass w/ CSS modules, autoprefixer, and minification
* [Koa](https://github.com/koajs/koa) (`^2.0.0-alpha`)
Expand All @@ -56,8 +58,6 @@ Features
* Code coverage reports/instrumentation with [isparta](https://github.com/deepsweet/isparta-loader)
* [Flow](http://flowtype.org/) (`^0.22.0`)
* [Babel](https://github.com/babel/babel) (`^6.3.0`)
* [react-transform-hmr](https://github.com/gaearon/react-transform-hmr) hot reloading for React components
* [redbox-react](https://github.com/KeywordBrain/redbox-react) visible error reporting for React components
* [babel-plugin-transform-runtime](https://www.npmjs.com/package/babel-plugin-transform-runtime) so transforms aren't inlined
* [babel-plugin-transform-react-constant-elements](https://babeljs.io/docs/plugins/transform-react-constant-elements/) save some memory allocation
* [babel-plugin-transform-react-remove-prop-types](https://github.com/oliviertassinari/babel-plugin-transform-react-remove-prop-types) remove `PropTypes`
Expand All @@ -76,6 +76,20 @@ $ npm install # Install Node modules listed in ./package.json
$ npm start # Compile and launch
```

### Redux DevTools

#### We recommend using the [Redux DevTools Chrome Extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd).

Using the chrome extension allows your monitors to run on a separate thread and affords better performance and functionality. It comes with several of the most popular monitors, is easy to configure, filters actions, and doesn’t require installing any packages.

However, adding the DevTools components to your project is simple, first grab the packages from npm:

```
npm i --D redux-devtools redux-devtools-log-monitor redux-devtools-dock-monitor
```

Then follow the [manual integration walkthrough](https://github.com/gaearon/redux-devtools/blob/master/docs/Walkthrough.md).

### Starting a New Project

First, I highly suggest checking out a new project by
Expand Down Expand Up @@ -173,7 +187,7 @@ make sure to copy over the `blueprints` folder in this project for starter-kit s
Structure
---------

The folder structure provided is only meant to serve as a guide, it is by no means prescriptive. It is something that has worked very well for me and my team, but use only what makes sense to you.
The folder structure provided is only meant to serve as a guide, it is by no means prescriptive.

```
.
Expand All @@ -186,25 +200,78 @@ The folder structure provided is only meant to serve as a guide, it is by no mea
├── server # Koa application (uses webpack middleware)
│ └── main.js # Server application entry point
├── src # Application source code
│ ├── components # Generic React Components (generally Dumb components)
│ ├── containers # Components that provide context (e.g. Redux Provider)
│ ├── main.js # Application bootstrap and rendering
│ ├── components # Reusable Presentational Components
│ ├── containers # Reusable Container Components
│ ├── layouts # Components that dictate major page structure
│ ├── redux # Redux-specific pieces
│ │ ├── modules # Collections of reducers/constants/actions
│ │ └── utils # Redux-specific helpers
│ ├── routes # Application route definitions
│ ├── static # Static assets (not imported anywhere in source code)
│ ├── styles # Application-wide styles (generally settings)
│ ├── views # Components that live at a route
│ └── main.js # Application bootstrap and rendering
│ ├── store # Redux-specific pieces
│ │   ├── createStore.js # Create and instrument redux store
│ │   └── reducers.js # Reducer registry and injection
│ └── routes # Main route definitions and async split points
│    ├── index.js # Bootstrap main application routes with store
│    ├── Root.js # Wrapper component for context-aware providers
│    ├── Home # Fractal route
│    │   ├── index.js # Route definitions and async split points
│    │   ├── assets # Assets required to render components
│    │   ├── components # Presentational React Components
│    │   ├── container # Connect components to actions and store
│    │   ├── modules # Collections of reducers/constants/actions
│    │   └── routes ** # Fractal sub-routes (** optional)
│    └── NotFound # Capture unknown routes in component
└── tests # Unit tests
```

### Components vs. Views vs. Layouts
#### Fractal App 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 it's own architecture from day one.

We use `react-router` [route definitions](https://github.com/reactjs/react-router/blob/master/docs/API.md#plainroute) (`<route>/index.js`) to define units of logic within our application. *Additional child routes can be nested in a fractal hierarchy.*

This provides many benefits that may not be immediately obvious:
- Routes can be be bundled into "chunks" using webpack's [code splitting](https://webpack.github.io/docs/code-splitting.html) 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](https://github.com/webpack/docs/wiki/list-of-plugins#dllplugin) 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 :evergreen_tree:). 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).

##### Layouts
- Stateless components that dictate major page structure
- Useful for composing `react-router` [named components](https://github.com/reactjs/react-router/blob/master/docs/API.md#components-1) into views

##### Components
- Prefer [stateless function components](https://facebook.github.io/react/docs/reusable-components.html#stateless-functions)
- eg: `const HelloMessage = ({ name }) => <div>Hello {name}</div>`
- Top-level `components` and `containers` directories contain reusable components

##### Containers
- 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 function component
- Tip: props injected by `react-router` can be accessed using `connect`:
```js
// 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' }
```

**TL;DR:** They're all components.
##### Routes
- A route directory...
- *Must* 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.

This distinction may not be important for you, but as an explanation: A **Layout** is something that describes an entire page structure, such as a fixed navigation, viewport, sidebar, and footer. Most applications will probably only have one layout, but keeping these components separate makes their intent clear. **Views** are components that live at routes, and are generally rendered within a **Layout**. What this ends up meaning is that, with this structure, nearly everything inside of **Components** ends up being a dumb component.
Note: 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**, which is totally cool! Webpack creates split points based on static analysis of `require` during compilation; the recursive hierarchy folder structure is simply for organizational purposes.

Webpack
-------
Expand Down Expand Up @@ -338,4 +405,4 @@ This project wouldn't be possible without help from the community, so I'd like t
* [Spencer Dixin](https://github.com/SpencerCDixon) - For your creation of [redux-cli](https://github.com/SpencerCDixon/redux-cli).
* [Jonas Matser](https://github.com/mtsr) - For your help in triaging issues and unending support in our Gitter channel.

And to everyone else who has contributed, even if you are not listed here your work is appreciated.
And to everyone else who has contributed, even if you are not listed here your work is appreciated.
32 changes: 10 additions & 22 deletions build/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ const webpackConfig = {
target: 'web',
devtool: config.compiler_devtool,
resolve: {
root: paths.base(config.dir_client),
root: paths.client(),
extensions: ['', '.js', '.jsx', '.json']
},
module: {}
}
// ------------------------------------
// Entry Points
// ------------------------------------
const APP_ENTRY_PATH = paths.base(config.dir_client) + '/main.js'
const APP_ENTRY_PATH = paths.client('main.js')

webpackConfig.entry = {
app: __DEV__
Expand All @@ -37,7 +37,7 @@ webpackConfig.entry = {
// ------------------------------------
webpackConfig.output = {
filename: `[name].[${config.compiler_hash_type}].js`,
path: paths.base(config.dir_dist),
path: paths.dist(),
publicPath: config.compiler_public_path
}

Expand Down Expand Up @@ -81,9 +81,11 @@ if (__DEV__) {

// Don't split bundles during testing, since we only want import one bundle
if (!__TEST__) {
webpackConfig.plugins.push(new webpack.optimize.CommonsChunkPlugin({
names: ['vendor']
}))
webpackConfig.plugins.push(
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor']
})
)
}

// ------------------------------------
Expand Down Expand Up @@ -125,20 +127,6 @@ webpackConfig.module.loaders = [{
plugins: ['transform-runtime'],
presets: ['es2015', 'react', 'stage-0'],
env: {
development: {
plugins: [
['react-transform', {
transforms: [{
transform: 'react-transform-hmr',
imports: ['react'],
locals: ['module']
}, {
transform: 'react-transform-catch-errors',
imports: ['react', 'redbox-react']
}]
}]
]
},
production: {
plugins: [
'transform-react-remove-prop-types',
Expand Down Expand Up @@ -169,7 +157,7 @@ const PATHS_TO_TREAT_AS_CSS_MODULES = [
// If config has CSS modules enabled, treat this project's styles as CSS modules.
if (config.compiler_css_modules) {
PATHS_TO_TREAT_AS_CSS_MODULES.push(
paths.base(config.dir_client).replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&')
paths.client().replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&')
)
}

Expand Down Expand Up @@ -280,7 +268,7 @@ if (!__DEV__) {
).forEach((loader) => {
const [first, ...rest] = loader.loaders
loader.loader = ExtractTextPlugin.extract(first, rest.join('!'))
delete loader.loaders
Reflect.deleteProperty(loader, 'loaders')
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woah, never seen this before.

})

webpackConfig.plugins.push(
Expand Down
22 changes: 9 additions & 13 deletions config/_base.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ config.globals = {
'__PROD__' : config.env === 'production',
'__TEST__' : config.env === 'test',
'__DEBUG__' : config.env === 'development' && !argv.no_debug,
'__DEBUG_NEW_WINDOW__' : !!argv.nw,
'__BASENAME__' : JSON.stringify(process.env.BASENAME || '')
}

Expand All @@ -100,17 +99,14 @@ config.compiler_vendor = config.compiler_vendor
// ------------------------------------
// Utilities
// ------------------------------------
config.utils_paths = (() => {
const resolve = path.resolve

const base = (...args) =>
resolve.apply(resolve, [config.path_base, ...args])

return {
base : base,
client : base.bind(null, config.dir_client),
dist : base.bind(null, config.dir_dist)
}
})()
const resolve = path.resolve
const base = (...args) =>
Reflect.apply(resolve, null, [config.path_base, ...args])
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justingreenberg I'm not familiar with using the Reflect api. What is the advantage of this?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just found https://github.com/tvcutsem/harmony-reflect/wiki so maybe that will clear things up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a great resource, pretty much says it all... basically, Reflect provides a nice functional first-class api for certain primitive methods, ie apply and operators ie. delete but it's really a matter of preference, i'm happy to revert this if you prefer legacy apply, delete, etc

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries about reverting, I just learned some new things today is all ;)


config.utils_paths = {
base : base,
client : base.bind(null, config.dir_client),
dist : base.bind(null, config.dir_dist)
}

export default config
7 changes: 1 addition & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
"react": "^15.0.0",
"react-dom": "^15.0.0",
"react-redux": "^4.0.0",
"react-router": "^2.0.0",
"react-router": "^2.2.0",
"react-router-redux": "^4.0.0",
"redux": "^3.0.0",
"redux-thunk": "^2.0.0",
Expand Down Expand Up @@ -153,12 +153,7 @@
"phantomjs-polyfill": "0.0.2",
"phantomjs-prebuilt": "^2.1.3",
"react-addons-test-utils": "^15.0.0",
"react-transform-catch-errors": "^1.0.2",
"react-transform-hmr": "^1.0.2",
"redbox-react": "^1.2.2",
"redux-devtools": "^3.0.0",
"redux-devtools-dock-monitor": "^1.0.1",
"redux-devtools-log-monitor": "^1.0.1",
"sinon": "^1.17.3",
"sinon-chai": "^2.8.0",
"webpack-dev-middleware": "^1.6.1",
Expand Down
2 changes: 1 addition & 1 deletion server/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ if (config.env === 'development') {
// Serving ~/dist by default. Ideally these files should be served by
// the web server and not the app server, but this helps to demo the
// server in production.
app.use(convert(serve(paths.base(config.dir_dist))))
app.use(convert(serve(paths.dist())))
}

export default app
2 changes: 1 addition & 1 deletion server/middleware/webpack-dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function (compiler, publicPath) {

const middleware = WebpackDevMiddleware(compiler, {
publicPath,
contentBase: paths.base(config.dir_client),
contentBase: paths.client(),
hot: true,
quiet: config.compiler_quiet,
noInfo: config.compiler_quiet,
Expand Down
37 changes: 37 additions & 0 deletions src/components/Counter/Counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* @flow */
import React from 'react'
import classes from './Counter.scss'

// FlowType annotations
type Props = {
counter: number,
doubleAsync: Function,
increment: Function
}

export const Counter = (props: Props) => (
<div>
<h2 className={classes.counterContainer}>
Counter:
{' '}
<span className={classes['counter--green']}>
{props.counter}
</span>
</h2>
<button className='btn btn-default' onClick={props.increment}>
Increment
</button>
{' '}
<button className='btn btn-default' onClick={props.doubleAsync}>
Double (Async)
</button>
</div>
)

Counter.propTypes = {
counter: React.PropTypes.number.isRequired,
doubleAsync: React.PropTypes.func.isRequired,
increment: React.PropTypes.func.isRequired
}

export default Counter
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
color: rgb(25,200,25);
}

.duck {
display: block;
width: 100%;
margin-top: 1.5rem;
.counterContainer {
margin: 1em auto;
}
3 changes: 3 additions & 0 deletions src/components/Counter/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Counter from './Counter'

export default Counter
18 changes: 18 additions & 0 deletions src/components/Header/Header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react'
import { IndexLink, Link } from 'react-router'
import classes from './Header.scss'

export const Header = () => (
<div>
<h1>React Redux Starter Kit</h1>
<IndexLink to='/' activeClassName={classes.activeRoute}>
Home
</IndexLink>
{' · '}
<Link to='/counter' activeClassName={classes.activeRoute}>
Counter
</Link>
</div>
)

export default Header
4 changes: 4 additions & 0 deletions src/components/Header/Header.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.activeRoute {
font-weight: bold;
text-decoration: underline;
}
3 changes: 3 additions & 0 deletions src/components/Header/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Header from './Header'

export default Header
Loading