Skip to content

Instantly share code, notes, and snippets.

@praveen001
Last active July 15, 2021 23:57
Show Gist options
  • Save praveen001/9beba17006dde3b03af31e25af9e811b to your computer and use it in GitHub Desktop.
Save praveen001/9beba17006dde3b03af31e25af9e811b to your computer and use it in GitHub Desktop.
React Clean Architecture
{
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true
}
import React from 'react';
import { createStyles, Theme, withStyles, WithStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import Divider from '@material-ui/core/Divider';
import TextField from '@material-ui/core/TextField';
import { ITodoItem, ApiStatus } from '../models';
import Paper from '@material-ui/core/Paper';
import { CircularProgress, Typography } from '@material-ui/core';
const styles = (theme: Theme) => createStyles({
wrap: {
display: 'flex',
justifyContent: 'center'
},
content: {
width: 500
},
addButton: {
marginTop: theme.spacing.unit
},
divider: {
marginTop: theme.spacing.unit * 2,
marginBottom: theme.spacing.unit * 2
}
});
class App extends React.Component<AppProps> {
constructor(props) {
super(props);
this.state = {
desc: ''
}
}
componentDidMount() {
// Load todos on mount
this.props.loadTodos();
}
addTodo = () => {
this.props.addTodo(this.state.desc);
}
onDescChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
desc: e.target.value
});
}
render() {
const { classes, todos, loadingStatus } = this.props;
return (
<div className={classes.wrap}>
<div className={classes.content}>
<div>
<TextField multiline placeholder="Enter todo message" rows="5" variant="outlined" onChange={this.onDescChange} value={this.state.desc} fullWidth />
<Button className={classes.addButton} color="primary" variant="contained" onClick={this.addTodo} fullWidth>Add Todo</Button>
</div>
<Divider className={classes.divider} />
<div>
{loadingStatus === ApiStatus.LOADING && <CircularProgress />}
{loadingStatus === ApiStatus.FAILED && <Typography color="error">Failed to load todos</Typography>}
{loadingStatus === ApiStatus.LOADED && todos.map(todo => (
<Paper key={todo.id}>
{todo.description}
</Paper>
))}
</div>
</div>
</div>
);
}
}
export default withStyles(styles)(App);
// Define props coming from redux store
export interface IAppStateProps {
loadingStatus: ApiStatus;
todos: ITodoItem[];
}
// Define props that are action creators
export interface IAppDispatchProps {
loadTodos: () => void;
addTodo: (todo: ITodoItem) => void;
}
type AppProps = IAppStateProps & IAppDispatchProps & WithStyles<typeof styles>
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles, Theme } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import Divider from '@material-ui/core/Divider';
import TextField from '@material-ui/core/TextField';
import { ITodoItem, ApiStatus } from '../models';
import { loadTodos } from '../actions/todosActions';
import Paper from '@material-ui/core/Paper';
import { CircularProgress, Typography } from '@material-ui/core';
const useStyles = makeStyles((theme: Theme) => ({
wrap: {
display: 'flex',
justifyContent: 'center'
},
content: {
width: 500
},
addButton: {
marginTop: theme.spacing.unit
},
divider: {
marginTop: theme.spacing.unit * 2,
marginBottom: theme.spacing.unit * 2
}
}));
const App: React.FC<AppProps> = (props) => {
const [desc, setDesc] = useState('');
const todos = useSelector(state => state.todos.todos);
const loadingStatus = useSelector(state => state.todos.loadingStatus);
const classes = useStyles();
const dispatch = useDispatch();
useEffect(() => {
dispatch(loadTodos());
}, []);
const addNewTodo = () => {
dispatch(addTodo(desc));
}
const onDescChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setDesc(e.target.value);
}
return (
<div className={classes.wrap}>
<div className={classes.content}>
<div>
<TextField multiline placeholder="Enter todo message" rows="5" variant="outlined" onChange={onDescChange} value={desc} fullWidth />
<Button className={classes.addButton} color="primary" variant="contained" onClick={addNewTodo} fullWidth>Add Todo</Button>
</div>
<Divider className={classes.divider} />
<div>
{loadingStatus === ApiStatus.LOADING && <CircularProgress />}
{loadingStatus === ApiStatus.FAILED && <Typography color="error">Failed to load todos</Typography>}
{loadingStatus === ApiStatus.LOADED && todos.map(todo => (
<Paper key={todo.id}>
{todo.description}
</Paper>
))}
</div>
</div>
</div>
);
}
export default App;
type AppProps = WithStyles<typeof styles>
import App, { IAppStateProps, IAppDispatchProps } from '../components/App';
import { IState } from '../reducers';
import { addTodo, loadTodos } from '../actions/todosActions';
import { connect } from 'react-redux';
function mapStateToDispatch(state: IState): IAppStateProps {
return {
todos: state.todos.todos,
loadingStatus: state.todos.loadingStatus
}
}
const mapDispatchToProps: IAppDispatchProps = {
addTodo,
loadTodos
}
export default connect(mapStateToDispatch, mapDispatchToProps)(App);
import { combineReducers } from 'redux';
import todosReducer, { ITodoState, initialTodoState } from './todosReducer';
export interface IState {
todos: ITodoState;
}
export const initialState: IState = {
todos: initialTodoState
};
export default combineReducers({
todos: todosReducer
});
import { combineEpics, createEpicMiddleware } from 'redux-observable';
import todoEpics from './todoEpics';
export const rootEpic = combineEpics(todoEpics);
export default createEpicMiddleware();
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import AppContainer from './containers/AppContainer';
ReactDOM.render(
<Provider store={store}>
<AppContainer />
</Provider>,
document.getElementById("app")
);
export interface ITodoItem {
id: number;
description: string;
}
export enum ApiStatus {
LOADING = 'loading',
LOADED = 'loaded',
FAILED = 'failed'
}
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{json}": [
"prettier --write"
],
"*.{ts,tsx}": [
"prettier --write",
"tslint --fix",
"git add"
]
}
import { applyMiddleware, createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import epicMiddleware, { rootEpic } from './epics';
import rootReducer, { initialState } from './reducers';
const composeEnhancer = composeWithDevTools({
name: 'React Clean Architecture'
});
const store = createStore(
rootReducer,
initialState,
composeEnhancer(applyMiddleware(epicMiddleware))
);
epicMiddleware.run(rootEpic);
export default store;
export enum TodosActionTypes {
LOAD_TODOS = 'todos/load',
LOADING_TODOS = 'todos/loading',
LOADED_TODOS = 'todos/loaded',
LOADING_TODOS_FAILED = 'todos/loading_failed',
ADD_TODO = 'todos/add',
ADDING_TODO = 'todos/adding',
ADDED_TODOS = 'todos/added',
ADDING_TODOS_FAILED = 'todos/adding_failed'
}
export interface ILoadTodosAction {
type: TodosActionTypes.LOAD_TODOS;
}
export interface ILoadingTodosAction {
type: TodosActionTypes.LOADING_TODOS;
}
import { ITodoItem } from "../models";
export function loadTodos(): ILoadTodosAction {
return {
type: TodosActionTypes.LOAD_TODOS
}
}
export function loadingTodos(): ILoadingTodosAction {
return {
type: TodosActionTypes.LOADING_TODOS
}
}
export function loadedTodos(todos: ITodoItem[]): ILoadedTodosAction {
return {
type: TodosActionTypes.LOADED_TODOS,
payload: {
todos
}
}
}
export function loadingTodosFailed(): ILoadingTodosFailedAction {
return {
type: TodosActionTypes.LOADING_TODOS_FAILED
}
}
export function addTodo(description: string): IAddTodoAction {
return {
type: TodosActionTypes.ADD_TODO,
payload: {
description
}
}
}
export function addingTodo(): IAddingTodoAction {
return {
type: TodosActionTypes.ADDING_TODO
}
}
export function addedTodo(todo: ITodoItem): IAddedTodoAction {
return {
type: TodosActionTypes.ADDED_TODOS,
payload: {
todo
}
}
}
export function addingTodoFailed(): IAddingTodoFailedAction {
return {
type: TodosActionTypes.ADDING_TODOS_FAILED
}
}
import { combineEpics, Epic } from "redux-observable";
import { switchMap, map, startWith, catchError, filter, mergeMap } from "rxjs/operators";
import axios from "axios";
import {
TodosAction,
TodosActionTypes,
loadedTodos,
loadingTodos,
loadingTodosFailed,
addedTodo,
addingTodo,
addingTodoFailed
} from "../actions/todosActions";
import { IState } from "../reducers";
import { from, of } from "rxjs";
import { isOfType } from "typesafe-actions";
const loadTodosEpic: Epic<TodosAction, TodosAction, IState> = (
action$,
state$
) =>
action$.pipe(
filter(isOfType(TodosActionTypes.LOAD_TODOS)),
switchMap(action =>
from(axios.get("http://localhost:5000/todos")).pipe(
map(response => loadedTodos(response.data.data)),
startWith(loadingTodos()),
catchError(() => of(loadingTodosFailed()))
)
)
);
const addTodoEpic: Epic<TodosAction, TodosAction, IState> = (
action$,
state$
) => action$.pipe(
filter(isOfType(TodosActionTypes.ADD_TODO)),
mergeMap(action =>
from(axios.post("http://localhost:5000/todos", action.payload)).pipe(
map(response => addedTodo(response.data.data)),
startWith(addingTodo()),
catchError(() => of(addingTodoFailed()))
)
)
)
export default combineEpics(loadTodosEpic, addTodoEpic);
export const initialTodoState: ITodoState = {
loadingStatus: ApiStatus.LOADING,
addingStatus: ApiStatus.LOADED,
todos: []
}
export interface ITodoState {
loadingStatus: ApiStatus;
addingStatus: ApiStatus;
todos: ITodoItem[];
}
export default function todosReducer(state: ITodoState = initialTodoState, action: TodosAction) {
return produce(state, draft => {
switch (action.type) {
case TodosActionTypes.LOAD_TODOS:
case TodosActionTypes.LOADING_TODOS:
draft.loadingStatus = ApiStatus.LOADING;
break;
case TodosActionTypes.LOADING_TODOS_FAILED:
draft.loadingStatus = ApiStatus.FAILED;
break;
case TodosActionTypes.LOADED_TODOS:
draft.loadingStatus = ApiStatus.LOADED;
draft.todos = action.payload.todos;
break;
case TodosActionTypes.ADD_TODO:
case TodosActionTypes.ADDING_TODO:
draft.addingStatus = ApiStatus.LOADING;
break;
case TodosActionTypes.ADDING_TODOS_FAILED:
draft.addingStatus = ApiStatus.FAILED;
break;
case TodosActionTypes.ADDED_TODOS:
draft.todos.push(action.payload.todo);
break;
}
});
}
{
"compilerOptions": {
"target": "es5",
"outDir": "./dist",
"jsx": "react",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"lib": ["es2015", "dom", "es2017"],
"strict": true
},
"exclude": ["node_modules"]
}
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended",
"tslint-config-standard",
"tslint-react",
"tslint-config-prettier"
],
"jsRules": {},
"rules": {
"ordered-imports": true,
"object-literal-sort-keys": false,
"member-ordering": false,
"jsx-no-lambda": false,
"jsx-boolean-value": false,
"member-access": false,
"max-line-length": [true, 150],
"no-var-requires": false
},
"rulesDirectory": []
}
const path = require('path'),
HtmlWebpackPlugin = require('html-webpack-plugin'),
webpack = require('webpack'),
ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'),
HappyPack = require('happypack');
module.exports = {
mode: 'development',
entry: './src/index.tsx',
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx']
},
devtool: 'inline-source-map',
devServer: {
historyApiFallback: {
index: '/index.html'
}
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'happypack/loader?id=ts'
}
]
},
plugins: [
new HappyPack({
id: 'ts',
threads: 2,
loaders: [
{
path: 'ts-loader',
query: { happyPackMode: true }
}
]
}),
new ForkTsCheckerWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin()
]
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment