Scalable project structure for React + Redux apps

Philip // The Bakery - @bakeryhq

# 🙋 ### Let's keep this interactive
## Scalable how? - know where things are - minimize change surface area - collocation for related things
## React + Redux apps - ✨components/containers - ⚡ actions - 🔧reducers - 🍴state access helpers (e.g. selectors) - tools for side effects (e.g. sagas)
## The "official" way [redux/examples/real-world](https://github.com/reactjs/redux/tree/master/examples/real-world)
## Project structure ``` ├──⚡actions⚡ │   └── index.js ├──✨components✨ │   ├── Explore.js │   └── User.js ├──✨containers✨ │   ├── App.js │   └── UserPage.js ├── middleware │   └── api.js ├── 🔧reducers🔧 │   ├── index.js │   └── paginate.js ├── routes.js └── store └── configureStore.js ```

Actions

src/actions/index.js

            
export const RESET_ERROR_MESSAGE = 'RESET_ERROR_MESSAGE'

// Resets the currently visible error message.
export const resetErrorMessage = () => ({
    type: RESET_ERROR_MESSAGE
})
            
          

Reducers

src/reducers/index.js

            
⚡ import * as ActionTypes from '../actions'
import paginate from './paginate'
import { routerReducer as routing } from 'react-router-redux'
import { combineReducers } from 'redux'

🔧const entities = (state = { users: {}, repos: {} }, action) => {
  return state
}

/* more reducers defined here */

const rootReducer = combineReducers({ entities, pagination })

export default rootReducer
            
          

Containers

src/containers/UserPage.js

            
import { ⚡loadUser, ⚡loadStarred } from '../actions'

class UserPage extends Component { /* stuff here */ }

const mapStateToProps = (state, ownProps) => {
  const {
    pagination: { starredByUser },
    entities: { users, repos }
  } = state

  const starredRepos = starredPagination.ids.map(id => repos[id])
  return { starredRepos }
}

export default connect(mapStateToProps, { ⚡loadUser, ⚡loadStarred })(UserPage)
            
          
## React boilerplate [mxstbr/react-boilerplate](https://github.com/mxstbr/react-boilerplate)
### Project structure ``` ├── components │   └── A │      └── ✨index.js ├── containers │   └── App │      ├── ⚡actions.js │      ├── constants.js │      ├── ✨index.js │      ├── 🔧reducer.js │      ├── 🍴selectors.js │      └── tests │      ├── ⚡actions.test.js │      ├── index.test.js │      ├── 🔧reducer.test.js │      └── 🍴selectors.test.js ├── reducers.js └── store.js ```

Actions

app/containers/App/actions.js

            
import {
  LOAD_REPOS,
  LOAD_REPOS_SUCCESS,
  LOAD_REPOS_ERROR,
} from './constants';

export function ⚡loadRepos() {
  return {
    type: LOAD_REPOS,
  };
}
            
          

Reducers

app/containers/App/reducer.js

            
import {
  LOAD_REPOS_SUCCESS,
  LOAD_REPOS,
  LOAD_REPOS_ERROR,
} from './constants';

function 🔧appReducer(state = initialState, action) {
  return state;
}

export default appReducer;
            
          

Selectors

app/containers/App/selector.js

            
import { createSelector } from 'reselect';

const selectGlobal = () => (state) => state.get('global');

const 🍴selectCurrentUser = () => createSelector(
  selectGlobal(),
  (globalState) => globalState.get('currentUser')
);

export {
  🍴selectGlobal,
  🍴selectCurrentUser
};
            
          

Containers

app/containers/HomePage/index.js

            
import { 🍴selectRepos } from 'containers/App/selectors';

export class Page extends React.PureComponent {}

export function mapDispatchToProps(dispatch) {
  return {
    onChange: (evt) => dispatch(⚡changeUsername(evt.target.value))
  };
};

const mapStateToProps = createStructuredSelector({
  repos: 🍴selectRepos() 
});

export default connect(mapStateToProps, mapDispatchToProps)(Page);
            
          
## Wordpress Calypso [Automattic/wp-calypso](https://github.com/Automattic/wp-calypso)
### Project structure ``` ├── components │ ├── ✨index.jsx │ └── style.scss └── state ├── action-types.js └── posts ├── ⚡actions.js ├── constants.js ├── 🔧reducer.js ├── schema.js ├── 🍴selectors.js └── test    ├── actions.js    ├── reducer.js    └── selectors.js ```

Actions

client/state/posts/actions.js

            
import {
  POSTS_RECEIVE,
  POSTS_REQUEST,
  POSTS_REQUEST_SUCCESS,
  POSTS_REQUEST_FAILURE
} from 'state/action-types';

export function ⚡receivePosts( posts ) {
  return {
    type: POSTS_RECEIVE,
    posts
  };
}
            
          

Reducers

client/state/posts/reducer.js

            
import {
  POSTS_REQUEST,
  POSTS_REQUEST_SUCCESS,
  POSTS_REQUEST_FAILURE
} from 'state/action-types';

export function 🔧siteRequests( state = {}, action ) {
  return state;
}
            
          

Selectors

client/state/posts/selectors.js

            
import createSelector from 'lib/create-selector';

export const 🍴getNormalizedPost = createSelector(
  (state, globalId) => normalizePost(getPost(state, globalId)),
  (state) => [state.posts.items, state.posts.queries]
);
            
          

Containers

client/components/data/query-geo/index.jsx

            
import { 🍴isRequestingGeo } from 'state/geo/selectors';
import { ⚡requestGeo } from 'state/geo/actions';

class QueryGeo extends Component {}

export default connect(
  ( state ) => ( { requesting: 🍴isRequestingGeo( state ) } ),
  { ⚡requestGeo }
)( QueryGeo );
            
          
*Bonus* ## Apollo Stack [apollostack/GitHunt-React](https://github.com/apollostack/GitHunt-React)

GraphQL

source: graphql.org

### Project structure ``` ├── package.json ├── ui │   ├── client.js │   ├── components │   │   └── Feed.js │   ├── routes │   │   └── FeedPage.js │   ├── server.js │   └── style │   └── index.css └── webpack.config.js ```

Containers

ui/routes/FeedPage.js

            
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';

class FeedPage extends React.Component { }

const FEED_QUERY = gql`
  query Feed($type: FeedType!, $offset: Int, $limit: Int) {
    feed(type: $type, offset: $offset, limit: $limit) {
      ...FeedEntry
    }
  }
`;

const withData = graphql(FEED_QUERY, { /* ... */ });
export default withData(FeedPage);
            
          
### Thank you 🙇