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 3 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
78 changes: 63 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,16 @@ Requirements
Features
--------

* [React](https://github.com/facebook/react) (`^0.14.0`)
* [React](https://github.com/facebook/react) (`^0.15.0-rc2`)
* [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 @@ -55,8 +57,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 @@ -75,6 +75,18 @@ $ npm install # Install Node modules listed in ./package.json
$ npm start # Compile and launch
```

### Redux DevTools

Redux DevTools components have been removed from this project in favor of the [Redux DevTools Chrome Extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd), which runs on a separate thread and provides much better performance and functionality. It provides access to the most popular monitors, is easy to configure to filter actions, and doesn’t require installing any packages.

**We strongly recommend using the chrome extension.** 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 All @@ -97,6 +109,7 @@ Great, you now have a fresh project! There are a few titles you'll probably want

* `~/package.json` - package name
* `~/src/index.html` - template title tag
* `~/src/main.js` - Helmet document title

Usage
-----
Expand Down Expand Up @@ -172,7 +185,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. The current fractal hierarchy was inspired by [an old angular RFC](https://docs.google.com/document/u/1/d/1XXMvReO8-Awi1EZXAXS4PzDzdNvV6pGcuaF4Q9821Es/pub) and contributed by [Justin Greenberg](https://github.com/justingreenberg).

```
.
Expand All @@ -185,25 +198,56 @@ 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)
│ ├── components # App-wide Presentational React Components
│ ├── store # Redux-specific pieces
│ │   ├── createStore.js # Create and instrument redux store
│ │   └── reducers.js # Reducer registry and injection
│ ├── 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
│ ├── routes # Main route definitions and async split points
│ │   ├── index.js # Bootstrap main application routes with store
│ │   ├── Home * # Fractal (All Route-specific, as needed)
│ │   │   ├── index.js * # Route definitions and async split points
│ │   │   ├── assets # Assets required to render components
│ │   │   ├── components # Presentational React Components
│ │   │   ├── containers # Connect components to actions and store
│ │   │   ├── modules # Collections of reducers/constants/actions
│ │   │   └── routes # Sub-route definitions and async split points
│ │   └── NotFound # Capture unknown routes in component
│ ├── 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
└── tests # Unit tests
```

### Components vs. Views vs. Layouts
### Fractal Structure (or, Recursive Route Hierarchy)

**TL;DR:** They're all components.
Small applications can be built using a flat directory structure, with folders for `components`, `containers`, etc. However, this does not scale and can seriously affect production velocity as your project grows. By starting with a fractal structure, you are forced to think about your architecture more strategically from day one.

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

This provides many benefits which may not immediately be 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.

#### Layouts
- Regular stateless components that dictate major page structure
- Useful for populating structure with named routes

#### Components
- Components should be stateless and purely presentational
- Prefer functional components ie. `const Cool = ({ who }) => <div>${who} is cool</div>`
- The top-level `components` directory should be thought of as a global common palette

#### Containers
- Containers **only** `connect` presentational components to actions/state
- One or many container components can be composed in a stateless function component
- Rule of thumb: **no JSX in containers**!

#### Routes
- A route directory
- *Must* contain an `index.js` that returns route definition
- *May* contain assets, components, containers, modules, and additional child Routes as needed
- Child routes follow the same structure recursively

Webpack
-------
Expand Down Expand Up @@ -297,6 +341,10 @@ Have more questions? Feel free to submit an issue or join the Gitter chat!
Troubleshooting
---------------

### Redux DevTools

We recommend using
Copy link
Owner

Choose a reason for hiding this comment

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

Should just link to the section you added above?


### `npm run dev:nw` produces `cannot read location of undefined.`

This is most likely because the new window has been blocked by your popup blocker, so make sure it's disabled before trying again.
Expand Down
16 changes: 1 addition & 15 deletions build/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,20 +125,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 @@ -280,7 +266,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
1 change: 0 additions & 1 deletion 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 Down
25 changes: 18 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@
"DEBUG": "app:*"
}
},
"deploy:dev": {
"command": "npm run deploy",
"env": {
"NODE_ENV": "development",
"DEBUG": "app:*"
}
},
"deploy:prod": {
"command": "npm run deploy",
"env": {
"NODE_ENV": "production",
"DEBUG": "app:*"
}
},
"start": {
"command": "babel-node bin/server",
"env": {
Expand Down Expand Up @@ -109,13 +123,15 @@
"koa-static": "^2.0.0",
"node-sass": "^3.3.3",
"postcss-loader": "^0.8.0",
"react": "^0.14.0",
"react-dom": "^0.14.0",
"react": "^15.0.0-rc.2",
"react-dom": "^15.0.0-rc.2",
"react-helmet": "^3.0.0",
"react-redux": "^4.0.0",
"react-router": "^2.0.0",
"react-router-redux": "^4.0.0",
"redux": "^3.0.0",
"redux-thunk": "^2.0.0",
"reselect": "^2.2.1",
"rimraf": "^2.5.1",
"sass-loader": "^3.0.0",
"style-loader": "^0.13.0",
Expand Down Expand Up @@ -153,12 +169,7 @@
"phantomjs-polyfill": "0.0.2",
"phantomjs-prebuilt": "^2.1.3",
"react-addons-test-utils": "^0.14.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
37 changes: 37 additions & 0 deletions src/components/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;
}
18 changes: 18 additions & 0 deletions src/components/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.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.activeRoute {
font-weight: bold;
text-decoration: underline;
}
62 changes: 62 additions & 0 deletions src/components/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
## Components

This is where we keep stateless presentational components that will be used throughout
our application.

### Super Simple Example

```js
import React from 'react'

const today = new Date();
const year = today.getFullYear();

export default const FullYear = () => <span>{year}</span>
```

### Counter Example (using FlowType)

- component is using FlowType annotations to type check props
- we can import

```js
/* @flow */
import React, { PropTypes } from 'react'
import classes from './Counter.scss'

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

// We also recommend using react PropTypes
const counterPropTypes = {
counter: PropTypes.number.isRequired,
doubleAsync: PropTypes.func.isRequired,
increment: PropTypes.func.isRequired
}

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 = counterPropTypes

export default Counter
```
12 changes: 0 additions & 12 deletions src/containers/DevTools.js

This file was deleted.

7 changes: 0 additions & 7 deletions src/containers/DevToolsWindow.js

This file was deleted.

Loading