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