├── .env
├── .eslintrc
├── .gitignore
├── .storybook
├── addons.js
└── config.js
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── App.css
├── App.js
├── App.test.js
├── AppContainer.js
├── components
│ ├── Header
│ │ ├── Header.js
│ │ ├── HeaderContainer.js
│ │ └── index.js
│ └── Hello
│ │ └── index.js
├── index.css
├── index.js
├── reducers
│ └── index.js
├── registerServiceWorker.js
├── routes
│ ├── HomePage.js
│ └── index.js
└── sagas
│ ├── index.js
│ └── testSaga
│ ├── actions.js
│ ├── reducer.js
│ └── saga.js
└── stories
└── index.js
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_VERSION=$npm_package_version
2 | REACT_APP_NAME=$npm_package_name
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "react-app",
3 | "rules": {
4 | "react/prop-types": 2
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/.storybook/addons.js:
--------------------------------------------------------------------------------
1 | import '@storybook/addon-actions/register'
2 | import 'storybook-addon-material-ui'
3 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure } from '@storybook/react'
2 | import '@storybook/addon-console'
3 |
4 | function loadStories() {
5 | require('../stories/index.js')
6 | // You can require as many stories as you need.
7 | }
8 |
9 | import { setConsoleOptions } from '@storybook/addon-console'
10 |
11 | setConsoleOptions({
12 | panelExclude: []
13 | })
14 |
15 | configure(loadStories, module)
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 David Broadhurst
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # create-react-app-redux-router-saga-boilerplate - filling the gaps for non-trival apps
2 |
3 | create-react-app is great but even a basic app needs more than a component renderer. Simple boilerplate to fill the gaps.
4 |
5 | create-react-app +
6 |
7 | - redux - https://redux.js.org/docs/introduction/
8 | - react-router - https://reacttraining.com/react-router/
9 | - saga - https://redux-saga.js.org/docs/introduction/
10 | - material-ui - http://www.material-ui.com/#/
11 | - storybook - https://storybook.js.org/
12 | - .env - https://medium.com/@tacomanator/environments-with-create-react-app-7b645312c09d
13 |
14 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).
15 |
16 | Below you will find some information on how to perform common tasks.
17 | You can find the most recent version of this guide [here](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md).
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "create-react-app-redux-saga-boilerplate",
3 | "description": "Filling the gaps for non-trival apps",
4 | "keywords": [
5 | "react",
6 | "redux",
7 | "saga"
8 | ],
9 | "version": "1.0.0",
10 | "author": "David Broadhurst",
11 | "license": "MIT",
12 | "private": true,
13 | "dependencies": {
14 | "material-ui": "^0.20.0",
15 | "react": "^16.2.0",
16 | "react-dom": "^16.2.0",
17 | "react-redux": "^5.0.7",
18 | "react-router-dom": "^4.2.2",
19 | "react-scripts": "^1.1.1",
20 | "react-tap-event-plugin": "^3.0.2",
21 | "redux": "^3.6.0",
22 | "redux-form": "^7.3.0",
23 | "redux-form-material-ui": "^4.3.2",
24 | "redux-saga": "^0.16.0",
25 | "validator": "^9.4.1"
26 | },
27 | "devDependencies": {
28 | "@storybook/addon-actions": "^3.3.15",
29 | "@storybook/addon-console": "^1.0.0",
30 | "@storybook/react": "^3.3.15",
31 | "storybook-addon-material-ui": "^0.8.2"
32 | },
33 | "scripts": {
34 | "start": "react-scripts start",
35 | "build": "react-scripts build",
36 | "test": "react-scripts test --env=jsdom",
37 | "eject": "react-scripts eject",
38 | "storybook": "start-storybook -p 9001 -c .storybook"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dbroadhurst/create-react-app-redux-saga-boilerplate/8ffdf246169fa7553f40ac3559a7640f9b5ab902/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
23 | %REACT_APP_NAME%-%REACT_APP_VERSION%
24 |
25 |
26 |
27 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import Routes from './routes'
4 |
5 | import './App.css'
6 |
7 | class App extends Component {
8 | static propTypes = {
9 | init: PropTypes.func
10 | }
11 |
12 | render() {
13 | this.props.init()
14 |
15 | return (
16 |
17 |
18 |
19 | )
20 | }
21 | }
22 |
23 | export default App
24 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/src/AppContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 |
3 | import AppComponent from './App'
4 |
5 | import { init } from './sagas/testSaga/reducer'
6 |
7 | const mapStatetoProps = state => {
8 | return {}
9 | }
10 |
11 | const mapDispatchToProps = dispatch => {
12 | return {
13 | init: () => {
14 | dispatch(init())
15 | }
16 | }
17 | }
18 |
19 | export default connect(mapStatetoProps, mapDispatchToProps)(AppComponent)
20 |
--------------------------------------------------------------------------------
/src/components/Header/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar'
5 |
6 | export default class HeaderComponent extends React.Component {
7 | static propTypes = {
8 | testSaga: PropTypes.object
9 | }
10 |
11 | render() {
12 | const { message } = this.props.testSaga
13 | console.log(message)
14 |
15 | return (
16 |
17 |
18 |
19 | {`${process.env.REACT_APP_NAME}-v${process.env.REACT_APP_VERSION}`}
20 |
21 |
22 |
23 | )
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/Header/HeaderContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 |
3 | import HeaderComponent from './Header'
4 |
5 | import { init } from '../../sagas/testSaga/reducer'
6 |
7 | const mapStatetoProps = state => {
8 | return {
9 | testSaga: state.testSaga
10 | }
11 | }
12 |
13 | const mapDispatchToProps = dispatch => {
14 | return {
15 | testInit: () => {
16 | dispatch(init())
17 | }
18 | }
19 | }
20 |
21 | export default connect(mapStatetoProps, mapDispatchToProps)(HeaderComponent)
22 |
--------------------------------------------------------------------------------
/src/components/Header/index.js:
--------------------------------------------------------------------------------
1 | import Header from './HeaderContainer'
2 |
3 | export default Header
4 |
--------------------------------------------------------------------------------
/src/components/Hello/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | class Hello extends Component {
4 | render() {
5 | return Hello
6 | }
7 | }
8 |
9 | export default Hello
10 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './AppContainer'
4 | import './index.css'
5 |
6 | import { Provider } from 'react-redux'
7 | import { createStore, applyMiddleware, compose } from 'redux'
8 |
9 | import reducers from './reducers'
10 | import createSagaMiddleware from 'redux-saga'
11 |
12 | import sagas from './sagas'
13 |
14 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
15 | import injectTapEventPlugin from 'react-tap-event-plugin'
16 | import getMuiTheme from 'material-ui/styles/getMuiTheme'
17 |
18 | import { blue800, amber50 } from 'material-ui/styles/colors'
19 |
20 | const muiTheme = getMuiTheme({
21 | palette: {
22 | accent1Color: amber50
23 | },
24 | tabs: {
25 | backgroundColor: blue800
26 | }
27 | })
28 |
29 | const sagaMiddleware = createSagaMiddleware()
30 |
31 | let composeEnhancers = compose
32 |
33 | if (process.env.NODE_ENV === 'development') {
34 | const composeWithDevToolsExtension =
35 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
36 | if (typeof composeWithDevToolsExtension === 'function') {
37 | composeEnhancers = composeWithDevToolsExtension
38 | }
39 | }
40 |
41 | const store = createStore(
42 | reducers,
43 | composeEnhancers(applyMiddleware(sagaMiddleware))
44 | )
45 |
46 | sagaMiddleware.run(sagas)
47 |
48 | injectTapEventPlugin()
49 |
50 | ReactDOM.render(
51 |
52 |
53 |
54 |
55 | ,
56 | document.getElementById('root')
57 | )
58 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import { reducer as formReducer } from 'redux-form'
3 | import { reducer as testSaga } from '../sagas/testSaga/reducer'
4 |
5 | const reducers = combineReducers({
6 | testSaga,
7 | form: formReducer
8 | })
9 |
10 | export default reducers
11 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/routes/HomePage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Hello from '../components/Hello'
3 |
4 | export default class HomePage extends React.Component {
5 | render() {
6 | return
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/routes/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { HashRouter as Router, Route, Switch } from 'react-router-dom'
3 | import Header from '../components/Header'
4 | import HomePage from './HomePage'
5 |
6 | export default class Routes extends Component {
7 | render() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | )
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/sagas/index.js:
--------------------------------------------------------------------------------
1 | import { all, fork } from 'redux-saga/effects'
2 |
3 | import testSaga from './testSaga/saga'
4 |
5 | export default function* root() {
6 | yield all([fork(testSaga)])
7 | }
8 |
--------------------------------------------------------------------------------
/src/sagas/testSaga/actions.js:
--------------------------------------------------------------------------------
1 | export const INIT = 'INIT'
2 | export const SET_STATE = 'SET_STATE'
3 |
--------------------------------------------------------------------------------
/src/sagas/testSaga/reducer.js:
--------------------------------------------------------------------------------
1 | import * as action from './actions'
2 |
3 | export const init = () => {
4 | return {
5 | type: action.INIT,
6 | payload: {}
7 | }
8 | }
9 |
10 | const ACTION_HANDLERS = {
11 | [action.SET_STATE]: (state, action) => {
12 | return { ...state, ...action.payload }
13 | },
14 | [action.INIT]: (state, action) => {
15 | return { ...state, ...action.payload }
16 | }
17 | }
18 |
19 | let defaultState = {}
20 |
21 | export const reducer = (state = defaultState, action) => {
22 | const handler = ACTION_HANDLERS[action.type]
23 | return handler ? handler(state, action) : state
24 | }
25 |
--------------------------------------------------------------------------------
/src/sagas/testSaga/saga.js:
--------------------------------------------------------------------------------
1 | import { call, put, takeLatest } from 'redux-saga/effects'
2 |
3 | import * as actions from './actions'
4 |
5 | let defaultState = {
6 | message: 'has not run'
7 | }
8 |
9 | function* init() {
10 | yield put({
11 | type: actions.SET_STATE,
12 | payload: {
13 | ...defaultState,
14 | message: 'has been run'
15 | }
16 | })
17 | }
18 |
19 | export default function* sagas() {
20 | yield takeLatest(actions.INIT, init)
21 | }
22 |
--------------------------------------------------------------------------------
/stories/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import Header from '../src/components/Header/Header'
4 | import { muiTheme } from 'storybook-addon-material-ui'
5 |
6 | storiesOf('Header', module)
7 | .addDecorator(muiTheme())
8 | .add('Header', () => )
9 |
--------------------------------------------------------------------------------