├── README.md
├── package.json
├── public
└── index.html
├── redux.png
└── src
├── containers
└── sample-module
│ ├── View.jsx
│ ├── components
│ └── SubComponents.jsx
│ ├── helpers
│ ├── index.js
│ └── staticProps.js
│ └── index.js
├── helpers
└── index.js
├── index.js
├── redux
├── actions
│ ├── api
│ │ ├── epics.js
│ │ ├── helpers.js
│ │ ├── index.js
│ │ └── types.js
│ ├── error
│ │ ├── index.js
│ │ └── types.js
│ ├── index.js
│ ├── sample-module
│ │ ├── index.js
│ │ └── types.js
│ └── ui
│ │ ├── index.js
│ │ └── types.js
├── middleware
│ ├── error
│ │ └── index.js
│ ├── multi-dispatch
│ │ └── index.js
│ └── sample-module
│ │ └── index.js
├── reducers
│ ├── index.js
│ ├── sample-module
│ │ ├── index.js
│ │ └── model.js
│ └── ui
│ │ ├── index.js
│ │ └── model.js
└── store.js
└── styles.css
/README.md:
--------------------------------------------------------------------------------
1 | # advanced-redux-pattern
2 |
3 | Advanced redux pattern for handling multiple sub dispatch, reusable middleware and scalable redux for large application.
4 |
5 | Live Demo
6 |
7 | [](https://codesandbox.io/s/kmwo4vr4x3)
8 |
9 | Inspiration [Advanced Redux Patterns - Nir Kaufman @ ReactNYC](https://youtu.be/JUuic7mEs-s)
10 |
11 | 
12 |
13 | ## Features:
14 | - State management using [redux](https://redux.js.org/)
15 | - Reusable Ajax Request as dispatch using [rxjs](https://github.com/ReactiveX/rxjs) & [redux-observables](https://redux-observable.js.org/) for side effects
16 | - Cancellable Request, in `ComponentWillUnmount` or manual cancel request
17 | - Single Source of Data Flow as Middleware for sub dispatch and multiple dispatch
18 | - Action Creators using [redux-actions](https://github.com/redux-utilities/redux-actions)
19 | - UI Library using [ant-design](https://ant.design/)
20 | - Readable Action Types in redux-dev-tools
21 | - Implement Folder Structure for Containers & Components
22 | - Api Error handling in single pipeline
23 | - Tutorial @medium [article](https://medium.com/@nenjotsu/scalable-redux-advanced-pattern-with-reactjs-c56ea97245f)
24 |
25 | ## Why do you need a sub dipatch
26 | For instance you will need to maintain the flow of dispatch in separate folder, to adhere the separation of concerns and debugging purposes, the component will remain clean and not cluttered of multiple or series of this.props.getEpicOne(), this.props.getEpicTwo(), here are reasons why:
27 | - clean and not cluttered dispatch inside a component
28 | - easy to debug the side effects in multiple requests
29 | - manageable flow dispatch to handle multiple dispatch and ajax request
30 | - dispatch a regular actions alongside with the actions with side effects
31 | - debuggable flow where/when you start the spinner (loading) and where to stop it
32 | - reusable dispatch for ajax request, ui, or master data
33 | - scalable redux and open for extension once you want to use other middleware like `redux-saga`, co-existing or even while you are implementing a new technology for a specific module/usecase.
34 |
35 | ## TODOs
36 | - Implement [reselect](https://github.com/reduxjs/reselect) for memoizing redux state
37 |
38 | ## For more question follow me
39 | - twitter [@nenjotsu](https://twitter.com/nenjotsu)
40 | - github [@nenjotsu](https://github.com/nenjotsu)
41 | - medium [@nenjotsu](https://medium.com/@nenjotsu)
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "advanced-redux-pattern",
3 | "version": "1.0.0",
4 | "description": "Advanced redux pattern for handling multiple sub dispatch, reusable middleware and scalable redux for large application.",
5 | "keywords": [
6 | "redux",
7 | "redux-observables",
8 | "advanced-pattern",
9 | "middleware"
10 | ],
11 | "main": "src/index.js",
12 | "dependencies": {
13 | "antd": "3.10.1",
14 | "query-string": "6.2.0",
15 | "react": "16.5.2",
16 | "react-dom": "16.5.2",
17 | "react-redux": "5.0.7",
18 | "react-scripts": "2.0.3",
19 | "redux": "3.6.0",
20 | "redux-actions": "2.6.1",
21 | "redux-observable": "0.17.0",
22 | "rxjs": "5.5.6"
23 | },
24 | "devDependencies": {},
25 | "scripts": {
26 | "start": "react-scripts start",
27 | "build": "react-scripts build",
28 | "test": "react-scripts test --env=jsdom",
29 | "eject": "react-scripts eject"
30 | },
31 | "browserslist": [
32 | ">0.2%",
33 | "not dead",
34 | "not ie <= 11",
35 | "not op_mini all"
36 | ]
37 | }
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
23 | React App
24 |
25 |
26 |
27 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/redux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nenjotsu/advanced-redux-pattern/dcdb78e9dabdf0b3f760ea6db756aaf2d3f4b5c7/redux.png
--------------------------------------------------------------------------------
/src/containers/sample-module/View.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Button from "antd/lib/button";
3 | import List from "antd/lib/list";
4 | import Spin from "antd/lib/spin";
5 | import Icon from "antd/lib/icon";
6 | import message from "antd/lib/message";
7 | import { defaultProps, propTypes, contextTypes } from "./helpers";
8 |
9 | class SampleModule extends Component {
10 | componentDidMount() {
11 | this.init();
12 | }
13 |
14 | componentWillUnmount() {
15 | this.props.reduxAction.getSampleModuleCancel();
16 | }
17 |
18 | init = () => {
19 | console.log("Component Mounted");
20 | };
21 |
22 | getSampleModuleData = () => {
23 | this.props.reduxAction.getSampleModuleEpic();
24 | };
25 |
26 | getSampleModuleDataCancel = () => {
27 | message.warning("request has been cancelled");
28 | this.props.reduxAction.getSampleModuleCancel();
29 | };
30 |
31 | clearData = () => {
32 | this.props.reduxAction.clearData();
33 | };
34 |
35 | render() {
36 | const { sampleModuleList, isLoading } = this.props;
37 | console.log(sampleModuleList, isLoading);
38 | return (
39 |
40 | Sample Module
41 |
45 |
48 |
51 | {isLoading ? (
52 |
53 |
54 |
55 | ) : (
56 | {item.title}}
62 | />
63 | )}
64 |
65 | );
66 | }
67 | }
68 |
69 | SampleModule.propTypes = propTypes;
70 | SampleModule.defaultProps = defaultProps;
71 | SampleModule.contextTypes = contextTypes;
72 |
73 | export default SampleModule;
74 |
--------------------------------------------------------------------------------
/src/containers/sample-module/components/SubComponents.jsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nenjotsu/advanced-redux-pattern/dcdb78e9dabdf0b3f760ea6db756aaf2d3f4b5c7/src/containers/sample-module/components/SubComponents.jsx
--------------------------------------------------------------------------------
/src/containers/sample-module/helpers/index.js:
--------------------------------------------------------------------------------
1 | import { defaultProps, propTypes, contextTypes } from "./staticProps";
2 |
3 | export { defaultProps, propTypes, contextTypes };
4 |
--------------------------------------------------------------------------------
/src/containers/sample-module/helpers/staticProps.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 |
3 | export const contextTypes = {
4 | router: PropTypes.shape({
5 | history: PropTypes.object.isRequired
6 | })
7 | };
8 |
9 | export const propTypes = {
10 | reduxAction: PropTypes.object.isRequired,
11 | sampleModuleList: PropTypes.array,
12 | isLoading: PropTypes.bool
13 | };
14 |
15 | export const defaultProps = {
16 | sampleModuleList: [],
17 | isLoading: []
18 | };
19 |
--------------------------------------------------------------------------------
/src/containers/sample-module/index.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from "redux";
2 | import { connect } from "react-redux";
3 | import View from "./View";
4 | import * as actions from "../../redux/actions/sample-module";
5 |
6 | function mapStateToProps(state) {
7 | return {
8 | isLoading: state.ui.isLoading,
9 | sampleModuleList: state.sampleModule.sampleModuleList
10 | };
11 | }
12 |
13 | function mapDispatchToProps(dispatch) {
14 | return {
15 | reduxAction: bindActionCreators({ ...actions }, dispatch),
16 | dispatch
17 | };
18 | }
19 |
20 | export default connect(
21 | mapStateToProps,
22 | mapDispatchToProps
23 | )(View);
24 |
--------------------------------------------------------------------------------
/src/helpers/index.js:
--------------------------------------------------------------------------------
1 | export const loadState = () => {
2 | try {
3 | const serializedState = sessionStorage.getItem("state");
4 | if (serializedState === null) {
5 | return undefined;
6 | }
7 | return JSON.parse(serializedState);
8 | } catch (err) {
9 | return undefined;
10 | }
11 | };
12 |
13 | const domain = "https://jsonplaceholder.typicode.com";
14 |
15 | export const url = {
16 | posts: `${domain}/posts`,
17 | comments: `${domain}/comments`
18 | };
19 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { Provider } from "react-redux";
4 | import List from "antd/lib/list";
5 | import Icon from "antd/lib/icon";
6 | import store from "./redux/store";
7 | import SampleModule from "./containers/sample-module";
8 |
9 | import "antd/dist/antd.css";
10 | import "./styles.css";
11 |
12 | const data = ["Scalable", "Reusable", "Maintainable", "Multiple dispatch"];
13 |
14 | function App() {
15 | return (
16 |
17 |
Advanced Redux Pattern
18 |
19 | Using redux, redux-observables, rxjs, react-actions, middleware,
20 | ant-design
21 |
22 |
(
27 |
28 |
29 | {item}
30 |
31 | )}
32 | />
33 |
34 |
35 |
36 |
37 | );
38 | }
39 |
40 | const rootElement = document.getElementById("root");
41 | ReactDOM.render(
42 |
43 |
44 | ,
45 | rootElement
46 | );
47 |
--------------------------------------------------------------------------------
/src/redux/actions/api/epics.js:
--------------------------------------------------------------------------------
1 | import qs from "query-string";
2 | import { ajax } from "rxjs/observable/dom/ajax";
3 | import * as TYPE from "./types";
4 | import { showSpinner, hideSpinner } from "../ui";
5 | import { onErrorApi } from "../error";
6 | import { retryStrategy, headersJson } from "./helpers";
7 | import "rxjs";
8 |
9 | export const getAjaxRequestEpic = action$ =>
10 | action$.ofType(TYPE.GET_AJAX_REQUEST_EPIC).mergeMap(action => {
11 | const { url, onSuccess, onCancel, body } = action.payload;
12 | const inlineQuery = body === null ? "" : `?${qs.stringify(body)}`;
13 | return ajax
14 | .get(`${url}${inlineQuery}`, headersJson)
15 | .retryWhen(retryStrategy)
16 | .mergeMap(result => [onSuccess(result.response)])
17 | .catch(err => [onErrorApi(err), hideSpinner()])
18 | .startWith(showSpinner())
19 | .takeUntil(action$.ofType(onCancel));
20 | });
21 |
22 | export const patchAjaxRequestEpic = action$ =>
23 | action$.ofType(TYPE.PATCH_AJAX_REQUEST_EPIC).mergeMap(action => {
24 | const { url, onSuccess, onCancel, body } = action.payload;
25 | return ajax
26 | .patch(url, body, headersJson)
27 | .retryWhen(retryStrategy)
28 | .mergeMap(result => [onSuccess(result.response)])
29 | .catch(err => [onErrorApi(err), hideSpinner()])
30 | .startWith(showSpinner())
31 | .takeUntil(action$.ofType(onCancel));
32 | });
33 |
34 | export const postAjaxRequestEpic = action$ =>
35 | action$.ofType(TYPE.POST_AJAX_REQUEST_EPIC).mergeMap(action => {
36 | const { url, onSuccess, onCancel, body } = action.payload;
37 | return ajax
38 | .post(url, body, headersJson)
39 | .retryWhen(retryStrategy)
40 | .mergeMap(result => [onSuccess(result.response)])
41 | .catch(err => [onErrorApi(err), hideSpinner()])
42 | .startWith(showSpinner())
43 | .takeUntil(action$.ofType(onCancel));
44 | });
45 |
46 | export const putAjaxRequestEpic = action$ =>
47 | action$.ofType(TYPE.PUT_AJAX_REQUEST_EPIC).mergeMap(action => {
48 | const { url, onSuccess, onCancel, body } = action.payload;
49 | return ajax
50 | .post(url, body, headersJson)
51 | .retryWhen(retryStrategy)
52 | .mergeMap(result => [onSuccess(result.response)])
53 | .catch(err => [onErrorApi(err), hideSpinner()])
54 | .startWith(showSpinner())
55 | .takeUntil(action$.ofType(onCancel));
56 | });
57 | export const deleteAjaxRequestEpic = action$ =>
58 | action$.ofType(TYPE.PUT_AJAX_REQUEST_EPIC).mergeMap(action => {
59 | const { url, onSuccess, onCancel, body } = action.payload;
60 | return ajax
61 | .delete(`${url}?${qs.stringify(body)}`, headers)
62 | .retryWhen(retryStrategy)
63 | .mergeMap(result => [onSuccess(result.response)])
64 | .catch(err => [onErrorApi(err), hideSpinner()])
65 | .startWith(showSpinner())
66 | .takeUntil(action$.ofType(onCancel));
67 | });
68 |
69 | export default [
70 | getAjaxRequestEpic,
71 | patchAjaxRequestEpic,
72 | postAjaxRequestEpic,
73 | putAjaxRequestEpic,
74 | deleteAjaxRequestEpic
75 | ];
76 |
--------------------------------------------------------------------------------
/src/redux/actions/api/helpers.js:
--------------------------------------------------------------------------------
1 | export const retryStrategy = errors =>
2 | errors
3 | .scan((_, value, index) => {
4 | if (index < 2) {
5 | return index;
6 | }
7 | throw value;
8 | })
9 | .delay(5000);
10 |
11 | export const headersJson = {
12 | "Content-Type": "application/json"
13 | // Authorization: `Bearer ${TOKEN}`,
14 | };
15 |
--------------------------------------------------------------------------------
/src/redux/actions/api/index.js:
--------------------------------------------------------------------------------
1 | import { createAction } from "redux-actions";
2 | import * as TYPE from "./types";
3 |
4 | export const getAjaxRequestEpic = createAction(TYPE.GET_AJAX_REQUEST_EPIC);
5 |
--------------------------------------------------------------------------------
/src/redux/actions/api/types.js:
--------------------------------------------------------------------------------
1 | export const GET_AJAX_REQUEST_EPIC = "[app] API Get Ajax Request";
2 | export const PATCH_AJAX_REQUEST_EPIC = "[app] API Patch Ajax Request";
3 | export const POST_AJAX_REQUEST_EPIC = "[app] API Post Ajax Request";
4 | export const PUT_AJAX_REQUEST_EPIC = "[app] API Put Ajax Request";
5 | export const DELETE_AJAX_REQUEST_EPIC = "[app] API Delete Ajax Request";
6 |
--------------------------------------------------------------------------------
/src/redux/actions/error/index.js:
--------------------------------------------------------------------------------
1 | import { createAction } from "redux-actions";
2 | import * as TYPE from "./types";
3 |
4 | export const onErrorApi = createAction(TYPE.ON_ERROR_API);
5 |
--------------------------------------------------------------------------------
/src/redux/actions/error/types.js:
--------------------------------------------------------------------------------
1 | export const ON_ERROR_API = "[error] api";
2 |
--------------------------------------------------------------------------------
/src/redux/actions/index.js:
--------------------------------------------------------------------------------
1 | import { combineEpics } from "redux-observable";
2 |
3 | import API from "./api/epics";
4 |
5 | const rootEpic = (action$, store) =>
6 | combineEpics(...API)(action$, store).catch((error, stream) => {
7 | console.error(error);
8 | return stream;
9 | });
10 |
11 | export default rootEpic;
12 |
--------------------------------------------------------------------------------
/src/redux/actions/sample-module/index.js:
--------------------------------------------------------------------------------
1 | import { createAction } from "redux-actions";
2 | import * as TYPE from "./types";
3 |
4 | export const getSampleModuleEpic = createAction(TYPE.GET_SAMPLE_MODULE_EPIC);
5 | export const getSampleModuleSuccess = createAction(
6 | TYPE.GET_SAMPLE_MODULE_SUCCESS
7 | );
8 | export const getSampleModuleCancel = createAction(
9 | TYPE.GET_SAMPLE_MODULE_CANCEL
10 | );
11 | export const getSampleModule = createAction(TYPE.GET_SAMPLE_MODULE);
12 |
13 | export const clearData = createAction(TYPE.CLEAR_DATA);
14 |
--------------------------------------------------------------------------------
/src/redux/actions/sample-module/types.js:
--------------------------------------------------------------------------------
1 | export const GET_SAMPLE_MODULE_EPIC = "[sample_module_name] Get Epic"; // to middleware
2 | export const GET_SAMPLE_MODULE_SUCCESS = "[sample_module_name] Get Success";
3 | export const GET_SAMPLE_MODULE_CANCEL = "[sample_module_name] Get Cancel";
4 | export const GET_SAMPLE_MODULE = "[sample_module_name] Get";
5 |
6 | export const CLEAR_DATA = "[sample_module_name] Clear Data";
7 |
--------------------------------------------------------------------------------
/src/redux/actions/ui/index.js:
--------------------------------------------------------------------------------
1 | import { createAction } from "redux-actions";
2 | import * as TYPE from "./types";
3 |
4 | export const showSpinner = createAction(TYPE.SHOW_SPINNER);
5 | export const hideSpinner = createAction(TYPE.HIDE_SPINNER);
6 |
--------------------------------------------------------------------------------
/src/redux/actions/ui/types.js:
--------------------------------------------------------------------------------
1 | export const SHOW_SPINNER = "[ui] Show spinner";
2 | export const HIDE_SPINNER = "[ui] Hide spinner";
3 |
--------------------------------------------------------------------------------
/src/redux/middleware/error/index.js:
--------------------------------------------------------------------------------
1 | import * as TYPE from "../../actions/error/types";
2 |
3 | const onErrorApi = ({ dispatch }) => next => action => {
4 | next(action);
5 |
6 | if (action.type === TYPE.ON_ERROR_API) {
7 | console.error("error", action.payload, dispatch);
8 | }
9 | };
10 |
11 | export default [onErrorApi];
12 |
--------------------------------------------------------------------------------
/src/redux/middleware/multi-dispatch/index.js:
--------------------------------------------------------------------------------
1 | const multi = ({ dispatch }) => next => action => {
2 | if (Array.isArray(action)) {
3 | return action.filter(Boolean).map(dispatch);
4 | }
5 | return next(action);
6 | };
7 |
8 | export default multi;
9 |
--------------------------------------------------------------------------------
/src/redux/middleware/sample-module/index.js:
--------------------------------------------------------------------------------
1 | import { getAjaxRequestEpic } from "../../actions/api";
2 | import {
3 | getSampleModule,
4 | getSampleModuleSuccess
5 | } from "../../actions/sample-module";
6 |
7 | import * as TYPE from "../../actions/sample-module/types";
8 | import { url } from "../../../helpers";
9 | import { showSpinner, hideSpinner } from "../../actions/ui";
10 |
11 | const getSampleModuleFlow = ({ dispatch }) => next => action => {
12 | next(action);
13 |
14 | if (action.type === TYPE.GET_SAMPLE_MODULE_EPIC) {
15 | dispatch([
16 | showSpinner(),
17 | getAjaxRequestEpic({
18 | url: url.posts,
19 | body: null,
20 | onCancel: TYPE.GET_SAMPLE_MODULE_CANCEL,
21 | onSuccess: getSampleModuleSuccess
22 | })
23 | ]);
24 | }
25 | };
26 |
27 | const getSampleModuleSuccessFlow = ({ dispatch }) => next => action => {
28 | next(action);
29 |
30 | if (action.type === TYPE.GET_SAMPLE_MODULE_SUCCESS) {
31 | dispatch([getSampleModule(action.payload), hideSpinner()]);
32 | }
33 | };
34 |
35 | const getSampleModuleSuccessCancelFlow = ({ dispatch }) => next => action => {
36 | next(action);
37 |
38 | if (action.type === TYPE.GET_SAMPLE_MODULE_CANCEL) {
39 | dispatch(hideSpinner());
40 | }
41 | };
42 |
43 | export default [
44 | getSampleModuleFlow,
45 | getSampleModuleSuccessFlow,
46 | getSampleModuleSuccessCancelFlow
47 | ];
48 |
--------------------------------------------------------------------------------
/src/redux/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import sampleModule from "./sample-module";
3 | import ui from "./ui";
4 |
5 | export default combineReducers({
6 | ui,
7 | sampleModule
8 | });
9 |
--------------------------------------------------------------------------------
/src/redux/reducers/sample-module/index.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from "redux-actions";
2 | import * as ACTION from "../../actions/sample-module";
3 | import model from "./model";
4 |
5 | export default handleActions(
6 | {
7 | [ACTION.getSampleModule]: (state, action) => {
8 | console.log("action.payload", action.payload);
9 | return {
10 | ...state,
11 | sampleModuleList: action.payload
12 | };
13 | },
14 | [ACTION.clearData]: (state, action) => ({
15 | ...state,
16 | sampleModuleList: []
17 | })
18 | },
19 | model
20 | );
21 |
--------------------------------------------------------------------------------
/src/redux/reducers/sample-module/model.js:
--------------------------------------------------------------------------------
1 | export default {
2 | candidatesCenterList: {
3 | pageSize: 10,
4 | pageCurrent: 0
5 | },
6 | candidateStatus: [],
7 | auctionIdsAssignedToMe: [],
8 | candidateNamesByClient: [],
9 | keyword: ""
10 | };
11 |
--------------------------------------------------------------------------------
/src/redux/reducers/ui/index.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from "redux-actions";
2 | import * as ACTION from "../../actions/ui";
3 | import model from "./model";
4 |
5 | export default handleActions(
6 | {
7 | [ACTION.showSpinner]: state => {
8 | console.log("show spinner count");
9 | return {
10 | ...state,
11 | isLoading: true
12 | };
13 | },
14 | [ACTION.hideSpinner]: state => {
15 | console.log("hide spinner count");
16 | return {
17 | ...state,
18 | isLoading: false
19 | };
20 | }
21 | },
22 | model
23 | );
24 |
--------------------------------------------------------------------------------
/src/redux/reducers/ui/model.js:
--------------------------------------------------------------------------------
1 | export default {
2 | isLoading: false
3 | };
4 |
--------------------------------------------------------------------------------
/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, createStore, compose } from "redux";
2 | import { createEpicMiddleware } from "redux-observable";
3 | import combinedReducers from "./reducers";
4 | import combinedEpics from "./actions";
5 | import { loadState } from "../helpers";
6 | import multiDispatch from "./middleware/multi-dispatch";
7 | import sampleModule from "./middleware/sample-module";
8 | import error from "./middleware/error";
9 |
10 | const composeEnhancers =
11 | process.env.NODE_ENV === "development"
12 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
13 | : null || compose;
14 |
15 | const stateFromLocalStorage = loadState();
16 |
17 | const epicMiddleware = createEpicMiddleware(combinedEpics);
18 |
19 | const store = createStore(
20 | combinedReducers,
21 | stateFromLocalStorage,
22 | composeEnhancers(
23 | applyMiddleware(epicMiddleware, multiDispatch, ...sampleModule, ...error)
24 | )
25 | );
26 |
27 | export default store;
28 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | .App {
2 | font-family: sans-serif;
3 | text-align: center;
4 | padding: 20px;
5 | }
6 |
7 | .icon {
8 | margin-right: 10px;
9 | margin-top: 4px;
10 | color: green;
11 | }
12 |
13 | .button-list {
14 | margin-top: 10px;
15 | }
16 |
17 | .btn {
18 | margin-right: 5px;
19 | }
20 |
21 | .data-list {
22 | margin-top: 10px;
23 | }
24 |
25 | .spin-container {
26 | text-align: center;
27 | background: rgba(0, 0, 0, 0.05);
28 | border-radius: 4px;
29 | margin-bottom: 20px;
30 | padding: 30px 50px;
31 | margin: 20px 0;
32 | }
33 |
--------------------------------------------------------------------------------