├── .babelrc.js ├── .gitignore ├── README.md ├── client ├── __tests__ │ └── components │ │ ├── Container.js │ │ └── DumbComponent.js └── src │ ├── action │ └── actions.js │ ├── components │ └── DumbComponent.jsx │ ├── constants │ └── actionTypes.js │ ├── containers │ ├── App.scss │ └── Container.jsx │ ├── index.js │ ├── reducers │ ├── combinedReducers.js │ └── firstReducer.js │ └── store.js ├── package.json ├── public ├── bundle.js └── index.html ├── server └── server.js └── webpack.config.js /.babelrc.js: -------------------------------------------------------------------------------- 1 | const isTest = String(process.env.NODE_DEV) === 'test' 2 | 3 | module.exports = { 4 | "presets": ["env", "react", "stage-2"], 5 | "env": { 6 | "test": { 7 | "presets": ["env"] 8 | }, 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xavyr-react-redux-boilerplate 2 | Instant React/Redux Boilerplate 3 | 4 | Setting up React and Redux can take a few moments and be a pain. This repository is the simplest React example application that adheres to the container/component react paradigm. 5 | 6 | Jest and SASS are configured using Babel. 7 | 8 | With regards to Redux I've instantiated a single reducer, very simple store and two action dispatchers; one synchronous action and one asynchronous using Thunk middleware. 9 | 10 | I hope this gets you up to speed with regards to your react/redux applications faster. 11 | -------------------------------------------------------------------------------- /client/__tests__/components/Container.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xavyr/react-redux-boilerplate/548aa2c192c3260df6117fc60f63f08673273c04/client/__tests__/components/Container.js -------------------------------------------------------------------------------- /client/__tests__/components/DumbComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import DumbComponent from '../components/DumbComponent' 4 | import { syncAction } from '../action/actions'; 5 | 6 | function sum(a, b) { 7 | return a + b; 8 | } 9 | 10 | test('adds 1 + 2 to equal 3', () => { 11 | expect(sum(1, 2)).toBe(3); 12 | }); 13 | 14 | test('DumbComponent renders just fine', () => { 15 | const container = document.createElement('div') 16 | ReactDOM.render(, container) 17 | }) -------------------------------------------------------------------------------- /client/src/action/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes.js' 2 | 3 | export const asyncAction = (currValue) => { 4 | return dispatch => { 5 | console.log(String(currValue) + ' => ' + String(!currValue) + ' in 3, 2, 1...'); 6 | return setTimeout(function () { 7 | return dispatch({ type: types.ASYNC_ACTION, payload: !currValue }) 8 | }, 3000); 9 | } 10 | }; 11 | 12 | export const syncAction = (currValue) => { 13 | return { type: types.SYNC_ACTION, payload: !currValue } 14 | } 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /client/src/components/DumbComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const DumbComponent = props => 4 |
5 | 9 | 14 |
15 |

{JSON.stringify(props.syncData)}

16 |

{JSON.stringify(props.asyncData)}

17 |
18 | 19 | 20 | export default DumbComponent; -------------------------------------------------------------------------------- /client/src/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const SYNC_ACTION = "SYNC_ACTION"; 2 | export const ASYNC_ACTION = "ASYNC_ACTION"; 3 | -------------------------------------------------------------------------------- /client/src/containers/App.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | font: 100% Helvetica, sans-serif; 3 | color: red; 4 | text-align: center; 5 | } -------------------------------------------------------------------------------- /client/src/containers/Container.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import DumbComponent from '../components/DumbComponent.jsx'; 3 | import { connect } from 'react-redux'; 4 | import { bindActionCreators } from 'redux'; 5 | import * as actions from '../action/actions' 6 | import './App.scss'; 7 | 8 | 9 | const mapStateToProps = (store) => { 10 | return { 11 | syncData: store.firstReducer.syncData, 12 | asyncData: store.firstReducer.asyncData, 13 | } 14 | } 15 | 16 | const mapDispatchToProps = (dispatch) => { 17 | return bindActionCreators({ 18 | syncAction: actions.syncAction, 19 | asyncAction: actions.asyncAction 20 | }, dispatch) 21 | }; 22 | 23 | 24 | class Container extends Component { 25 | 26 | componentDidMount() { 27 | console.log('exampleContainer did mount'); 28 | } 29 | 30 | render() { 31 | return ( 32 |
33 |

Container

34 | 40 |
41 | ); 42 | } 43 | } 44 | 45 | export default connect(mapStateToProps, mapDispatchToProps)(Container); -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import store from './store.js'; 5 | import App from './../src/containers/Container.jsx' 6 | ReactDOM.render( 7 | 8 | 9 | , document.getElementById('root')); 10 | 11 | module.hot.accept(); -------------------------------------------------------------------------------- /client/src/reducers/combinedReducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import firstReducer from './firstReducer.js' 3 | 4 | 5 | // combine reducers 6 | const reducers = combineReducers({ 7 | // we only have a single reducer, but if we had others we could combine them here, 8 | //by mapping them to a key. 9 | firstReducer: firstReducer 10 | }); 11 | 12 | 13 | export default reducers; -------------------------------------------------------------------------------- /client/src/reducers/firstReducer.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | syncData: false, 3 | asyncData: false, 4 | count: 0 5 | } 6 | 7 | //The action passed into our reducer comes from the dispatch. 8 | const firstReducer = (state = initialState, action) => { 9 | switch (action.type) { 10 | case 'SYNC_ACTION': 11 | return { 12 | ...state, 13 | syncData: action.payload 14 | } 15 | case 'ASYNC_ACTION': 16 | return { 17 | ...state, 18 | asyncData: action.payload 19 | } 20 | default: 21 | return state; 22 | } 23 | }; 24 | 25 | 26 | 27 | 28 | export default firstReducer; 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /client/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import { composeWithDevTools } from 'redux-devtools-extension'; 4 | import reducers from './reducers/combinedReducers.js'; 5 | 6 | 7 | //the store, enhanced with thunk middleware to allow for async action in redux. 8 | const store = createStore( 9 | reducers, 10 | composeWithDevTools( 11 | applyMiddleware(thunk) 12 | ) 13 | ); 14 | 15 | 16 | export default store; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-boilerplate", 3 | "version": "1.0.0", 4 | "description": "Modern React Redux Boilerplate", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon ./server/server.js & webpack-dev-server --config ./webpack.config.js --mode development --hot", 8 | "test": "jest" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Xavyr/react-redux-boilerplate" 13 | }, 14 | "author": "Xavyr", 15 | "license": "MIT", 16 | "homepage": "https://github.com/Xavyr/react-redux-boilerplate", 17 | "dependencies": { 18 | "css-loader": "^1.0.0", 19 | "react": "^16.3.2", 20 | "react-dom": "^16.3.2", 21 | "react-hot-loader": "^4.2.0", 22 | "react-redux": "^5.0.7", 23 | "redux": "^4.0.0", 24 | "redux-devtools-extension": "^2.13.2", 25 | "redux-thunk": "^2.2.0", 26 | "style-loader": "^0.23.0" 27 | }, 28 | "devDependencies": { 29 | "babel-core": "^6.26.3", 30 | "babel-jest": "^23.6.0", 31 | "babel-loader": "^7.1.4", 32 | "babel-preset-env": "^1.7.0", 33 | "babel-preset-react": "^6.24.1", 34 | "babel-preset-stage-2": "^6.24.1", 35 | "jest": "^23.6.0", 36 | "node-sass": "^4.9.3", 37 | "sass-loader": "^7.1.0", 38 | "webpack": "^4.17.2", 39 | "webpack-cli": "^2.1.3", 40 | "webpack-dev-server": "^3.1.4" 41 | }, 42 | "babel": { 43 | "presets": "./.babelrc.js" 44 | }, 45 | "jest": {} 46 | } 47 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React Boilerplate 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | //npm modules 2 | const express = require('express'); 3 | const path = require('path'); 4 | 5 | //the actual express app instance; 6 | const app = express(); 7 | 8 | //parsing body for post routes 9 | var bodyParser = require('body-parser'); 10 | app.use(bodyParser.json()); 11 | app.use(bodyParser.urlencoded({ extended: true })); 12 | 13 | //pathing to specific files 14 | app.use(express.static(path.join(__dirname, './../public'))); 15 | 16 | app.get('/api', (req, res) => { 17 | console.log('hit the home route'); 18 | res.send('from home api route'); 19 | }); 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | app.listen(3000); 31 | 32 | module.exports = app; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './client/src/index.js', 5 | output: { 6 | path: path.join(__dirname, 'public'), 7 | filename: 'bundle.js' 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | loader: 'babel-loader', 13 | test: /\.js$|\.jsx$/, 14 | exclude: /node_modules/ 15 | }, 16 | { 17 | test: /\.scss$/, 18 | use: [{ 19 | loader: "style-loader" 20 | }, { 21 | loader: "css-loader" 22 | }, { 23 | loader: "sass-loader", 24 | options: { 25 | includePaths: ["absolute/path/a", "absolute/path/b"] 26 | } 27 | }] 28 | }, 29 | { 30 | test: /\.(png|svg|jpg|jpeg|gif)$/, 31 | use: [ 32 | 'url-loader' 33 | ] 34 | } 35 | ] 36 | }, 37 | 38 | // Dev tools are provided by webpack 39 | // Source maps help map errors to original react code 40 | devtool: 'cheap-module-eval-source-map', 41 | 42 | // Configuration for webpack-dev-server 43 | devServer: { 44 | contentBase: path.join(__dirname, 'public'), 45 | proxy: { 46 | "/api*": "http://localhost:3000" 47 | } 48 | }, 49 | }; --------------------------------------------------------------------------------