├── .gitignore ├── src ├── flow-types.js ├── worker.js ├── Todo │ ├── services │ │ ├── ErrorService.js │ │ ├── BaseService.js │ │ ├── __test__ │ │ │ └── ErrorService.test.js │ │ ├── index.js │ │ ├── ModalService.js │ │ ├── ValidationService.js │ │ └── TodoService.js │ ├── components │ │ ├── index.js │ │ ├── TodoListComponent.js │ │ ├── TodoComponent.js │ │ └── TodoFormComponent.js │ ├── containers │ │ ├── index.js │ │ ├── TodoFormContainer.js │ │ ├── TodoListContainer.js │ │ └── TodoContainer.js │ ├── reducers │ │ ├── index.js │ │ ├── usersReducer.js │ │ ├── todoListsReducer.js │ │ ├── __test__ │ │ │ ├── usersReducer.test.js │ │ │ └── todosReducer.test.js │ │ └── todosReducer.js │ ├── actions │ │ ├── index.js │ │ ├── userActions.js │ │ ├── todoListActions.js │ │ └── todoActions.js │ ├── index.js │ ├── models │ │ ├── UserModel.js │ │ ├── index.js │ │ ├── UserCollection.js │ │ ├── TodoListModel.js │ │ ├── TodoModel.js │ │ ├── TodoListCollection.js │ │ └── TodoCollection.js │ ├── usecases.js │ ├── __test__ │ │ ├── actions.test.js │ │ └── usecases.test.js │ ├── selectors.js │ └── sdk.js ├── clean-architecture-utils │ └── index.js ├── template.html ├── style.css ├── usecases.js ├── services.js ├── index.js ├── immutable-perf.babel.js ├── ReverseLookup.js ├── app.js └── store.js ├── .flowconfig ├── test ├── example.test.js └── ReverseLookup.test.js ├── flow-typed └── npm │ ├── flow-bin_v0.x.x.js │ ├── shortid_v2.2.x.js │ ├── uuid_v3.x.x.js │ ├── font-awesome_vx.x.x.js │ ├── url-loader_vx.x.x.js │ ├── file-loader_vx.x.x.js │ ├── babel-preset-react_vx.x.x.js │ ├── babel-preset-es2015_vx.x.x.js │ ├── babel-preset-es2017_vx.x.x.js │ ├── react-addons-perf_vx.x.x.js │ ├── babel-preset-stage-1_vx.x.x.js │ ├── babel-preset-power-assert_vx.x.x.js │ ├── eslint-config-airbnb-flow_vx.x.x.js │ ├── babel-plugin-syntax-dynamic-import_vx.x.x.js │ ├── babel-plugin-transform-regenerator_vx.x.x.js │ ├── babel-plugin-syntax-async-functions_vx.x.x.js │ ├── babel-plugin-transform-async-to-generator_vx.x.x.js │ ├── uglifyjs-webpack-plugin_vx.x.x.js │ ├── babel-plugin-transform-runtime_vx.x.x.js │ ├── re-reselect_vx.x.x.js │ ├── eslint-plugin-flow-vars_vx.x.x.js │ ├── eslint-plugin-class-property_vx.x.x.js │ ├── style-loader_vx.x.x.js │ ├── redux-thunk_vx.x.x.js │ ├── html-webpack-plugin_vx.x.x.js │ ├── babel-loader_vx.x.x.js │ ├── babel-polyfill_vx.x.x.js │ ├── sass-loader_vx.x.x.js │ ├── redux_v3.x.x.js │ ├── eslint-config-airbnb_vx.x.x.js │ ├── babel-preset-env_vx.x.x.js │ ├── css-loader_vx.x.x.js │ ├── babel-eslint_vx.x.x.js │ ├── react-redux_v5.x.x.js │ ├── babel-plugin-espower_vx.x.x.js │ ├── redux-logger_vx.x.x.js │ ├── babel-cli_vx.x.x.js │ ├── uglify-js_vx.x.x.js │ ├── offline-plugin_vx.x.x.js │ ├── react-hot-loader_vx.x.x.js │ ├── webpack-dev-server_vx.x.x.js │ ├── worker-loader_vx.x.x.js │ ├── flow-typed_vx.x.x.js │ ├── eslint-plugin-ava_vx.x.x.js │ ├── babel-core_vx.x.x.js │ ├── node-sass_vx.x.x.js │ ├── eslint-plugin-import_vx.x.x.js │ ├── sinon_vx.x.x.js │ └── eslint-plugin-flow_vx.x.x.js ├── README.md ├── clean-architecture-demo.sublime-project ├── .babelrc ├── wallaby.js ├── .eslintrc ├── webpack.config.babel.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /src/flow-types.js: -------------------------------------------------------------------------------- 1 | declare type ReduxAction
= { 2 | type: string, 3 | payload: P, 4 | }; 5 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | src/flow-types.js 7 | 8 | [options] 9 | -------------------------------------------------------------------------------- /test/example.test.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | 3 | 4 | test("example test", t => { 5 | t.truthy(true); 6 | }); 7 | -------------------------------------------------------------------------------- /src/worker.js: -------------------------------------------------------------------------------- 1 | // silly little experiment 2 | 3 | import "babel-polyfill"; 4 | import * as usecases from "Todo/usecases"; 5 | 6 | onmessage = e => { 7 | console.log(e); 8 | postMessage("Hello World"); 9 | }; 10 | -------------------------------------------------------------------------------- /src/Todo/services/ErrorService.js: -------------------------------------------------------------------------------- 1 | import BaseService from "./BaseService"; 2 | 3 | export default class ErrorService extends BaseService { 4 | async reportError(err: Error) { 5 | console.error(err.message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /flow-typed/npm/flow-bin_v0.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 6a5610678d4b01e13bbfbbc62bdaf583 2 | // flow-typed version: 3817bc6980/flow-bin_v0.x.x/flow_>=v0.25.x 3 | 4 | declare module "flow-bin" { 5 | declare module.exports: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/Todo/services/BaseService.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from "redux"; 2 | 3 | export default class BaseService { 4 | constructor(dispatch, getState) { 5 | this.dispatch = dispatch; 6 | this.getState = getState; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Todo/components/index.js: -------------------------------------------------------------------------------- 1 | import TodoComponent from "./TodoComponent"; 2 | import TodoListComponent from "./TodoListComponent"; 3 | import TodoFormComponent from "./TodoFormComponent"; 4 | 5 | export { TodoComponent, TodoListComponent, TodoFormComponent }; -------------------------------------------------------------------------------- /src/Todo/services/__test__/ErrorService.test.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import ErrorService from "../ErrorService"; 3 | 4 | test(t => { 5 | t.truthy(ErrorService); 6 | const errorService = new ErrorService(); 7 | t.truthy(errorService.reportError); 8 | }); 9 | -------------------------------------------------------------------------------- /src/Todo/containers/index.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import TodoContainer from "./TodoContainer"; 4 | import TodoListContainer from "./TodoListContainer"; 5 | import TodoFormContainer from "./TodoFormContainer"; 6 | 7 | export { TodoContainer, TodoListContainer, TodoFormContainer }; -------------------------------------------------------------------------------- /src/Todo/reducers/index.js: -------------------------------------------------------------------------------- 1 | import todos from "./todosReducer"; 2 | import todoLists from "./todoListsReducer"; 3 | import users from "./usersReducer"; 4 | 5 | const reducers = { 6 | todos, 7 | todoLists, 8 | users, 9 | }; 10 | 11 | export default reducers; 12 | -------------------------------------------------------------------------------- /src/Todo/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as todo from "./todoActions"; 2 | import * as todoList from "./todoListActions"; 3 | import * as users from "./userActions"; 4 | 5 | const actions = { 6 | todo, 7 | todoList, 8 | users, 9 | }; 10 | 11 | export default actions; 12 | -------------------------------------------------------------------------------- /src/Todo/services/index.js: -------------------------------------------------------------------------------- 1 | // import ModalService from "./ModalService"; 2 | import ErrorService from "./ErrorService"; 3 | import TodoService from "./TodoService"; 4 | import ValidationService from "./ValidationService"; 5 | 6 | 7 | export { 8 | // ModalService, 9 | ErrorService, 10 | TodoService, 11 | ValidationService, 12 | }; 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clean Architecture Demo 2 | 3 | This project is to illustrate the structure of a React + Redux web application using Clean Architecture. 4 | 5 | # Setup 6 | 7 | ``` 8 | yarn install 9 | ``` 10 | 11 | or, if you like npm just install using that method. 12 | 13 | 14 | # Running 15 | 16 | ``` 17 | NODE_ENV=test webpack-dev-server 18 | ``` 19 | -------------------------------------------------------------------------------- /src/clean-architecture-utils/index.js: -------------------------------------------------------------------------------- 1 | export const UsecaseInteractor = usecase => services => (...args) => usecase(services)(...args); 2 | 3 | export const ServiceInitializer = (dispatch, getState) => (services) => { 4 | let _services = {}; 5 | for (let p in services) { 6 | _services[p] = new services[p](dispatch, getState); 7 | } 8 | return _services; 9 | } 10 | -------------------------------------------------------------------------------- /src/Todo/index.js: -------------------------------------------------------------------------------- 1 | import actions from "./actions"; 2 | import reducers from "./reducers"; 3 | import containers from "./containers"; 4 | import models from "./models"; 5 | import * as services from "./services"; 6 | import usecases from "./usecases"; 7 | 8 | export { 9 | actions, 10 | containers, 11 | models, 12 | reducers, 13 | services, 14 | usecases, 15 | }; 16 | -------------------------------------------------------------------------------- /src/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Todo/models/UserModel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Record } from "immutable"; 4 | 5 | const _shape = { 6 | id: undefined, 7 | name: "", 8 | }; 9 | 10 | export class UserModel extends Record(_shape) {} 11 | 12 | export const create = (payload) => new UserModel(payload); 13 | 14 | export const getID = (userModel) => userModel.id; 15 | 16 | export const getName = (userModel) => userModel.name; 17 | -------------------------------------------------------------------------------- /clean-architecture-demo.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "config_file": "/home/meyerrd/Documents/source/clean-architecture-demo/wallaby.js", 3 | "folders": 4 | [ 5 | { 6 | "folder_exclude_patterns": 7 | [ 8 | "node_modules", 9 | "dist" 10 | ], 11 | "name": "Clean Architecture Demo", 12 | "path": "." 13 | } 14 | ], 15 | "settings": 16 | { 17 | "tab_size": 2 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Todo/services/ModalService.js: -------------------------------------------------------------------------------- 1 | import BaseService from "./BaseService"; 2 | import { open } from "../actions/modalActions"; 3 | 4 | 5 | export default class ModalService extends BaseService { 6 | async openModal(modalID: string) { 7 | this.dispatch(open(modalID)); 8 | // this needs to return a promise that resolves to the submitted form value 9 | return Promise.resolve("debug"); 10 | } 11 | 12 | closeModal() { 13 | this.dispatch(actions.modals.close()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | @import "~@blueprintjs/core/dist/blueprint.css"; 2 | @import "~@blueprintjs/datetime/dist/blueprint-datetime.css"; 3 | 4 | body { 5 | background-color: #efefef; 6 | margin: 15px; 7 | padding-top: 50px; 8 | } 9 | 10 | .TodoList { 11 | margin-top: 15px; 12 | } 13 | 14 | .TodoForm__label { 15 | display: inline-block; 16 | padding-right: 20px; 17 | width: 80px; 18 | } 19 | 20 | .TodoForm__date-group .pt-popover-target { 21 | width: 100%; 22 | } 23 | 24 | .TodoNavbar__link { 25 | color: #106ba3; 26 | } 27 | -------------------------------------------------------------------------------- /src/Todo/models/index.js: -------------------------------------------------------------------------------- 1 | import * as TodoModel from "./TodoModel"; 2 | import * as TodoCollection from "./TodoCollection"; 3 | import * as TodoListModel from "./TodoListModel"; 4 | import * as TodoListCollection from "./TodoListCollection"; 5 | import * as UserModel from "./UserModel"; 6 | import * as UserCollection from "./UserCollection"; 7 | 8 | const models = { 9 | TodoModel, 10 | TodoCollection, 11 | TodoListModel, 12 | TodoListCollection, 13 | UserModel, 14 | UserCollection, 15 | }; 16 | 17 | export default models; 18 | -------------------------------------------------------------------------------- /flow-typed/npm/shortid_v2.2.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: b156a2435634461a06e78c26c06b3e55 2 | // flow-typed version: 4037d522d2/shortid_v2.2.x/flow_>=v0.27.x 3 | 4 | type ShortIdModule = { 5 | (): string, 6 | generate(): string, 7 | seed(seed: number): ShortIdModule, 8 | worker(workerId: number): ShortIdModule, 9 | characters(characters: string): string, 10 | decode(id: string): { version: number, worker: number }, 11 | isValid(id: mixed): boolean, 12 | }; 13 | 14 | declare module 'shortid' { 15 | declare module.exports: ShortIdModule; 16 | }; 17 | -------------------------------------------------------------------------------- /flow-typed/npm/uuid_v3.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: c07f382c8238bb78e545b60dd4f097a6 2 | // flow-typed version: 27f92307d3/uuid_v3.x.x/flow_>=v0.33.x 3 | 4 | declare module 'uuid' { 5 | declare function v1(options?: {| 6 | node?: number[], 7 | clockseq?: number, 8 | msecs?: number | Date, 9 | nsecs?: number, 10 | |}, buffer?: number[] | Buffer, offset?: number): string; 11 | declare function v4(options?: {| 12 | random?: number[], 13 | rng?: () => number[] | Buffer, 14 | |}, buffer?: number[] | Buffer, offset?: number): string; 15 | } 16 | -------------------------------------------------------------------------------- /src/Todo/containers/TodoFormContainer.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import { connect } from "react-redux"; 4 | import TodoFormComponent from "../components/TodoFormComponent"; 5 | import { addTodoUsecase } from "usecases"; 6 | 7 | const mapDispatchToProps = (dispatch, ownProps) => { 8 | return { 9 | addTodo: (name: string, description: string, date: string) => addTodoUsecase(ownProps.userID, name, description, date), 10 | }; 11 | }; 12 | 13 | const TodoFormContainer = connect(undefined, mapDispatchToProps)(TodoFormComponent); 14 | 15 | export default TodoFormContainer; 16 | -------------------------------------------------------------------------------- /flow-typed/npm/font-awesome_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: f8be86a6060f971e78b251ec2163941b 2 | // flow-typed version: < = {
9 | type: string,
10 | payload: P,
11 | };
12 |
13 | type t_AddPayload = {
14 | id: string,
15 | name: string,
16 | };
17 |
18 | type t_RemovePayload = {
19 | id: string,
20 | };
21 |
22 | export const add = (id: string, name: string): t_Action = {
7 | type: string,
8 | payload: P,
9 | };
10 |
11 | type AddPayload = {
12 | id: string,
13 | name: string,
14 | todoIDs: string[],
15 | };
16 |
17 | type UpdatePayload = {
18 | id: string,
19 | name: string,
20 | todoID: string,
21 | };
22 |
23 | export const add = (id: string, name: string, todoIDs: string[]): Action = (props: P, context: Context) => ?React$Element ): ConnectedComponentClass{name} - {userName} - {date}
17 | = {
16 | dispatch: Dispatch;
17 | getState(): S;
18 | };
19 |
20 | declare type Store = {
21 | // rewrite MiddlewareAPI members in order to get nicer error messages (intersections produce long messages)
22 | dispatch: Dispatch;
23 | getState(): S;
24 | subscribe(listener: () => void): () => void;
25 | replaceReducer(nextReducer: Reducer): void
26 | };
27 |
28 | declare type Reducer = (state: S, action: A) => S;
29 |
30 | declare type CombinedReducer = (state: $Shape & {} | void, action: A) => S;
31 |
32 | declare type Middleware =
33 | (api: MiddlewareAPI) =>
34 | (next: Dispatch) => Dispatch;
35 |
36 | declare type StoreCreator = {
37 | (reducer: Reducer, enhancer?: StoreEnhancer): Store;
38 | (reducer: Reducer, preloadedState: S, enhancer?: StoreEnhancer): Store;
39 | };
40 |
41 | declare type StoreEnhancer = (next: StoreCreator) => StoreCreator;
42 |
43 | declare function createStore(reducer: Reducer, enhancer?: StoreEnhancer): Store;
44 | declare function createStore(reducer: Reducer, preloadedState: S, enhancer?: StoreEnhancer): Store;
45 |
46 | declare function applyMiddleware(...middlewares: Array;
47 |
48 | declare type ActionCreator = (...args: Array) => A;
49 | declare type ActionCreators) => S>, A>;
55 |
56 | declare function compose(...fns: Array = (state: S, ownProps: OP) => SP | MapStateToProps;
19 |
20 | declare type MapDispatchToProps = ((dispatch: Dispatch, ownProps: OP) => DP) | DP;
21 |
22 | declare type MergeProps extends React$Component(
64 | mapStateToProps: MapStateToProps,
65 | mapDispatchToProps: Null,
66 | mergeProps: Null,
67 | options?: ConnectOptions
68 | ): Connector(
78 | mapStateToProps: MapStateToProps,
79 | mapDispatchToProps: MapDispatchToProps,
80 | mergeProps: Null,
81 | options?: ConnectOptions
82 | ): Connector(
85 | mapStateToProps: MapStateToProps,
86 | mapDispatchToProps: MapDispatchToProps,
87 | mergeProps: MergeProps