├── .eslintrc
├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── App.css
├── App.js
├── actions
│ └── index.js
├── constants.js
├── index.js
├── lib
│ └── api.js
├── reducers
│ ├── index.js
│ └── users.js
├── registerServiceWorker.js
├── sagas
│ ├── index.js
│ └── watchers
│ │ └── getUsers.js
├── screens
│ └── Home
│ │ ├── Home.test.js
│ │ ├── __snapshots__
│ │ └── Home.test.js.snap
│ │ ├── index.js
│ │ └── styles.js
└── store.js
└── yarn.lock
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "airbnb",
4 | "globals": {
5 | "__DEV__": true
6 | },
7 | "env": {
8 | "browser": true,
9 | "node": true,
10 | "jest/globals": true
11 | },
12 | "rules": {
13 | "comma-dangle": 0,
14 | "spaced-comment": 0,
15 | "new-cap": [2, { "capIsNewExceptions": ["Map"] }],
16 | "max-len": 0,
17 | "no-restricted-syntax": 0,
18 | "no-console": 0,
19 | "no-loop-func": 0,
20 | "no-plusplus": 0,
21 | "no-bitwise": 0,
22 | "func-names": 0,
23 | "camelcase": 0,
24 | "prefer-template": 0,
25 | "no-param-reassign": ["error", { "props": false }],
26 | "class-methods-use-this": 0,
27 | "react/no-did-mount-set-state": 0,
28 | "react/prefer-stateless-function": 0,
29 | "react/no-unknown-property": [2, { "ignore": ["charset"] }],
30 | "react/prop-types": 0,
31 | "react/jsx-filename-extension": 0,
32 | "react/forbid-prop-types": 0,
33 | "react/no-array-index-key": 0,
34 | "jsx-a11y/anchor-has-content": 0,
35 | "jsx-a11y/no-static-element-interactions": 0,
36 | "jsx-first-prop-new-line": 0,
37 | "vars-on-top": 0,
38 | "no-nested-ternary": 0,
39 | "eqeqeq": 0,
40 | "no-underscore-dangle": 0
41 | },
42 | "plugins": ["react", "jest"]
43 | }
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 | .idea
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 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Mateus Andrade
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-redux-saga-boilerplate
2 |
3 | [](https://greenkeeper.io/)
4 |
5 | My simple starter kit which I'd love to share to the community. The project was generated from `create-react-app` and then I included a few libraries.
6 |
7 | ## Features Out-Of-The-Box
8 |
9 | * React-Router 4
10 | * Semantic Ui
11 | * Redux
12 | * Redux Saga
13 | * ESlint
14 | * Airbnb's ESlint rules
15 |
16 | ## Live Demo
17 |
18 | https://react-redux-saga-boilerplate.herokuapp.com/
19 |
20 | ## Installation
21 |
22 | Clone repo and run:
23 |
24 | ```
25 | yarn && yarn start
26 | ```
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-saga-boilerplate",
3 | "version": "0.1.0",
4 | "private": false,
5 | "license": "MIT",
6 | "dependencies": {
7 | "connected-react-router": "^4.5.0",
8 | "history": "^4.7.2",
9 | "react": "^16.5.2",
10 | "react-dom": "^16.5.2",
11 | "react-helmet": "^5.2.0",
12 | "react-redux": "^5.0.7",
13 | "react-router": "^4.3.1",
14 | "react-router-dom": "^4.3.1",
15 | "react-router-redux": "^5.0.0-alpha.9",
16 | "react-scripts": "2.1.0",
17 | "redux": "^4.0.1",
18 | "redux-devtools-extension": "^2.13.5",
19 | "redux-saga": "^0.16.2",
20 | "semantic-ui-css": "^2.4.1",
21 | "semantic-ui-react": "^0.83.0"
22 | },
23 | "devDependencies": {
24 | "eslint-config-airbnb": "^17.1.0",
25 | "eslint-plugin-import": "^2.14.0",
26 | "eslint-plugin-jest": "^21.18.0",
27 | "eslint-plugin-jsx-a11y": "^6.1.2",
28 | "eslint-plugin-react": "^7.11.0",
29 | "react-test-renderer": "^16.5.2"
30 | },
31 | "scripts": {
32 | "start": "react-scripts start",
33 | "build": "react-scripts build",
34 | "test": "react-scripts test --env=jsdom",
35 | "eject": "react-scripts eject"
36 | },
37 | "browserslist": [
38 | ">0.2%",
39 | "not dead",
40 | "not ie <= 11",
41 | "not op_mini all"
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mCodex/react-redux-saga-boilerplate/748e5a4d758b3b75917f9d7f5e46779eae68ac8f/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React Redux Saga Boilerplate
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/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 | body,div {
2 | display: flex;
3 | flex-direction: column;
4 | flex: 1;
5 | }
6 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import { Provider } from 'react-redux';
4 | import { ConnectedRouter } from 'connected-react-router';
5 | import { Route } from 'react-router-dom';
6 |
7 | import store, { history } from './store';
8 |
9 | import Home from './screens/Home';
10 |
11 | import 'semantic-ui-css/semantic.min.css';
12 | import './App.css';
13 |
14 | export default class App extends Component {
15 | render() {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import { GET_USERS_SAGA, SET_USERS } from '../constants';
2 |
3 | export function setUsers(users) {
4 | return {
5 | type: SET_USERS,
6 | users
7 | };
8 | }
9 |
10 | //Sagas
11 | export function getUsersSaga() {
12 | return {
13 | type: GET_USERS_SAGA
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | const GET_USERS_SAGA = 'GET_USERS_SAGA';
2 | const SET_USERS = 'SET_USERS';
3 |
4 | export { //eslint-disable-line
5 | GET_USERS_SAGA,
6 | SET_USERS
7 | };
8 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 |
4 | import registerServiceWorker from './registerServiceWorker';
5 |
6 | import App from './App';
7 |
8 | const rootElement = document.getElementById('root');
9 |
10 | render(, rootElement);
11 |
12 | registerServiceWorker();
13 |
--------------------------------------------------------------------------------
/src/lib/api.js:
--------------------------------------------------------------------------------
1 |
2 | export async function getUsers() {
3 | const response = await fetch('https://jsonplaceholder.typicode.com/users');
4 | return response.json();
5 | }
6 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import usersReducer from './users';
4 |
5 | export default combineReducers({
6 | usersReducer
7 | });
8 |
--------------------------------------------------------------------------------
/src/reducers/users.js:
--------------------------------------------------------------------------------
1 | import { SET_USERS } from '../constants';
2 |
3 | const initialState = { users: [] };
4 |
5 | export default function setBrowserInfo(state = initialState, action) {
6 | switch (action.type) {
7 | case SET_USERS:
8 | return {
9 | ...state,
10 | users: action.users
11 | };
12 | default:
13 | return state;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/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/sagas/index.js:
--------------------------------------------------------------------------------
1 | import { all, fork } from 'redux-saga/effects';
2 |
3 | import watchGetUsersSaga from './watchers/getUsers';
4 |
5 | export default function* root() {
6 | yield all([
7 | fork(watchGetUsersSaga),
8 | ]);
9 | }
10 |
--------------------------------------------------------------------------------
/src/sagas/watchers/getUsers.js:
--------------------------------------------------------------------------------
1 | import { put, takeLatest, call } from 'redux-saga/effects';
2 |
3 | import { GET_USERS_SAGA } from '../../constants';
4 | import { setUsers } from '../../actions';
5 | import { getUsers } from '../../lib/api';
6 |
7 | function* workerGetUsersSaga() {
8 | const users = yield call(getUsers);
9 | yield put(setUsers(users));
10 | }
11 |
12 | export default function* watchGetUsersSaga() {
13 | yield takeLatest(GET_USERS_SAGA, workerGetUsersSaga);
14 | }
15 |
--------------------------------------------------------------------------------
/src/screens/Home/Home.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import HomeComponent from './index';
4 |
5 | import store from '../../store';
6 |
7 | it('Testing Home Component', () => {
8 | const tree = renderer
9 | .create()
10 | .toJSON();
11 | expect(tree).toMatchSnapshot();
12 | });
13 |
--------------------------------------------------------------------------------
/src/screens/Home/__snapshots__/Home.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Testing Home Component 1`] = `
4 |
14 |
21 |
22 | `;
23 |
--------------------------------------------------------------------------------
/src/screens/Home/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Button, Table } from 'semantic-ui-react';
4 |
5 | import { getUsersSaga } from '../../actions';
6 |
7 | import styles from './styles';
8 |
9 | class Home extends Component {
10 | constructor() {
11 | super();
12 | this.handleBtnOnClick = this.handleBtnOnClick.bind(this);
13 | }
14 |
15 | handleBtnOnClick() {
16 | this.props.getUsersSaga();
17 | }
18 |
19 | render() {
20 | const { users } = this.props;
21 | return (
22 |
23 | {users.length > 0
24 | && (
25 |
28 |
29 |
30 | Id
31 | Name
32 | Username
33 | E-mail
34 | Phone
35 | Website
36 |
37 |
38 |
39 | {users.map(({
40 | id,
41 | name,
42 | email,
43 | phone,
44 | username,
45 | website
46 | }, i) => (
47 |
48 | {id}
49 | {name}
50 | {username}
51 | {email}
52 | {phone}
53 | {website}
54 | ))}
55 |
56 |
57 | )
58 | }
59 |
65 |
66 | );
67 | }
68 | }
69 |
70 | const mapStateToProps = state => ({
71 | users: state.usersReducer.users
72 | });
73 |
74 | const mapDispatchToProps = dispatch => ({
75 | getUsersSaga: () => dispatch(getUsersSaga())
76 | });
77 |
78 | export default connect(mapStateToProps, mapDispatchToProps)(Home);
79 |
--------------------------------------------------------------------------------
/src/screens/Home/styles.js:
--------------------------------------------------------------------------------
1 | const styles = {
2 | container: {
3 | marginLeft: 40,
4 | marginRight: 40,
5 | alignItems: 'center',
6 | justifyContent: 'center'
7 | }
8 | };
9 |
10 | export default styles;
11 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import { connectRouter, routerMiddleware } from 'connected-react-router';
3 | import { createBrowserHistory } from 'history';
4 | import { composeWithDevTools } from 'redux-devtools-extension';
5 | import createSagaMiddleware from 'redux-saga';
6 |
7 | import rootReducer from './reducers';
8 | import sagas from './sagas';
9 |
10 | export const history = createBrowserHistory();
11 |
12 | const sagaMiddleware = createSagaMiddleware();
13 |
14 | const initialState = {};
15 | const enhancers = [];
16 | const middleware = [
17 | routerMiddleware(history),
18 | sagaMiddleware
19 | ];
20 | /*
21 | if (process.env.NODE_ENV === 'development') {
22 | const devToolsExtension = window.__REDUX_DEVTOOLS_EXTENSION__;
23 | if (typeof devToolsExtension === 'function') {
24 | enhancers.push(devToolsExtension());
25 | }
26 | }*/
27 |
28 | const composedEnhancers = composeWithDevTools(
29 | applyMiddleware(...middleware),
30 | ...enhancers
31 | );
32 |
33 | const store = createStore(
34 | connectRouter(history)(rootReducer),
35 | initialState,
36 | composedEnhancers
37 | );
38 |
39 | sagaMiddleware.run(sagas);
40 |
41 | export default store;
42 |
--------------------------------------------------------------------------------