Skip to content

Instantly share code, notes, and snippets.

@kotarella1110
Last active December 20, 2022 02:02
Show Gist options
  • Save kotarella1110/e31fda608ab7182fec33c5e5737105be to your computer and use it in GitHub Desktop.
Save kotarella1110/e31fda608ab7182fec33c5e5737105be to your computer and use it in GitHub Desktop.
Counter app example - TypeScript + React + Redux + redux-saga
import * as React from 'react';
import { Store, createStore, combineReducers, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { reducer as counter, rootSaga } from './counter';
import Counter from './Counter';
const sagaMiddleware = createSagaMiddleware();
const store: Store = createStore(
combineReducers({
counter,
}),
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(rootSaga);
interface IProps {
compiler: string;
framework: string;
}
const App: React.SFC<IProps> = () => (
<Provider store={store}>
<Counter title="Counter" />
</Provider>
);
ReactDOM.render(
<App compiler="TypeScript" framework="React" />,
document.getElementById('app') as HTMLElement
);
import { actions, asyncActions, reducer } from './counter';
describe('counter actions', () => {
it('increment should create counter/INCREMENT action', () => {
expect(actions.increment()).toEqual({
type: 'counter/INCREMENT',
});
});
it('decrement should create counter/DECREMENT action', () => {
expect(actions.decrement()).toEqual({
type: 'counter/DECREMENT',
});
});
});
describe('counter async actions', () => {
it('asyncActions.incrementStarted should create counter/INCREMENT_ASYNC_STARTED action', () => {
expect(asyncActions.incrementStarted()).toEqual({
type: 'counter/INCREMENT_ASYNC_STARTED',
});
});
it('asyncActions.incrementDone should create counter/INCREMENT_ASYNC_DONE action', () => {
expect(asyncActions.incrementDone()).toEqual({
type: 'counter/INCREMENT_ASYNC_DONE',
});
});
it('asyncActions.incrementFailed should create counter/INCREMENT_ASYNC_FAILED action', () => {
expect(asyncActions.incrementFailed()).toEqual({
type: 'counter/INCREMENT_ASYNC_FAILED',
});
});
it('asyncActions.decrementStarted should create counter/DECREMENT_ASYNC_STARTED action', () => {
expect(asyncActions.decrementStarted()).toEqual({
type: 'counter/DECREMENT_ASYNC_STARTED',
});
});
it('asyncActions.decrementDone should create counter/DECREMENT_ASYNC_DONE action', () => {
expect(asyncActions.decrementDone()).toEqual({
type: 'counter/DECREMENT_ASYNC_DONE',
});
});
it('asyncActions.decrementFailed should create counter/DECREMENT_ASYNC_FAILED action', () => {
expect(asyncActions.decrementFailed()).toEqual({
type: 'counter/DECREMENT_ASYNC_FAILED',
});
});
});
describe('counter reducer', () => {
it('should handle counter/INCREMENT_ASYNC_STARTED', () => {
expect(
reducer(
{
isLoading: false,
errorMessage: 'Request failed',
count: 0,
},
asyncActions.incrementStarted()
)
).toEqual({
isLoading: true,
errorMessage: '',
count: 0,
});
});
it('should handle counter/INCREMENT_ASYNC_DONE', () => {
expect(
reducer(
{
isLoading: true,
errorMessage: '',
count: 0,
},
asyncActions.incrementDone()
)
).toEqual({
isLoading: false,
errorMessage: '',
count: 1,
});
});
it('should handle counter/INCREMENT_ASYNC_FAILED', () => {
expect(
reducer(
{
isLoading: true,
errorMessage: '',
count: 1,
},
asyncActions.incrementFailed()
)
).toEqual({
isLoading: false,
errorMessage: 'Request failed',
count: 1,
});
});
it('should handle counter/DECREMENT_ASYNC_STARTED', () => {
expect(
reducer(
{
isLoading: false,
errorMessage: 'Request failed',
count: 1,
},
asyncActions.decrementStarted()
)
).toEqual({
isLoading: true,
errorMessage: '',
count: 1,
});
});
it('should handle counter/DECREMENT_ASYNC_DONE', () => {
expect(
reducer(
{
isLoading: true,
errorMessage: '',
count: 1,
},
asyncActions.decrementDone()
)
).toEqual({
isLoading: false,
errorMessage: '',
count: 0,
});
});
it('should handle counter/DECREMENT_ASYNC_FAILED', () => {
expect(
reducer(
{
isLoading: true,
errorMessage: '',
count: 0,
},
asyncActions.decrementFailed()
)
).toEqual({
isLoading: false,
errorMessage: 'Request failed',
count: 0,
});
});
});
import { Action as AnyAction, Reducer } from 'redux';
import { SagaIterator, delay } from 'redux-saga';
import { takeEvery, call, put, cancelled } from 'redux-saga/effects';
export type Meta = null | { [key: string]: any };
export interface FSA<Type extends string, Payload = null> extends AnyAction {
type: Type;
payload?: Payload;
error?: boolean;
meta?: Meta;
}
enum ActionType {
increment = 'counter/INCREMENT',
decrement = 'counter/DECREMENT',
}
enum AsyncActionType {
incrementStarted = 'counter/INCREMENT_ASYNC_STARTED',
incrementDone = 'counter/INCREMENT_ASYNC_DONE',
incrementFailed = 'counter/INCREMENT_ASYNC_FAILED',
decrementStarted = 'counter/DECREMENT_ASYNC_STARTED',
decrementDone = 'counter/DECREMENT_ASYNC_DONE',
decrementFailed = 'counter/DECREMENT_ASYNC_FAILED',
}
export type Action = FSA<ActionType.increment> | FSA<ActionType.decrement>;
export type AsyncAction =
| FSA<AsyncActionType.incrementStarted>
| FSA<AsyncActionType.incrementDone>
| FSA<AsyncActionType.incrementFailed>
| FSA<AsyncActionType.decrementStarted>
| FSA<AsyncActionType.decrementDone>
| FSA<AsyncActionType.decrementFailed>;
const increment = (): Action => {
return { type: ActionType.increment };
};
const decrement = (): Action => {
return { type: ActionType.decrement };
};
const incrementStarted = (): AsyncAction => {
return { type: AsyncActionType.incrementStarted };
};
const incrementDone = (): AsyncAction => {
return { type: AsyncActionType.incrementDone };
};
const incrementFailed = (): AsyncAction => {
return { type: AsyncActionType.incrementFailed };
};
const decrementStarted = (): AsyncAction => {
return { type: AsyncActionType.decrementStarted };
};
const decrementDone = (): AsyncAction => {
return { type: AsyncActionType.decrementDone };
};
const decrementFailed = (): AsyncAction => {
return { type: AsyncActionType.decrementFailed };
};
export const actions = { increment, decrement };
export const asyncActions = {
incrementStarted,
incrementDone,
incrementFailed,
decrementStarted,
decrementDone,
decrementFailed,
};
export function* incrementAsyncWorker(): SagaIterator {
yield put(asyncActions.incrementStarted());
try {
yield call(delay, 1000);
if (Math.random() > 0.8) {
throw new Error();
}
yield put(asyncActions.incrementDone());
} catch {
yield put(asyncActions.incrementFailed());
} finally {
if (yield cancelled()) {
yield put(asyncActions.incrementFailed());
}
}
}
export function* decrementAsyncWorker(): SagaIterator {
yield put(asyncActions.decrementStarted());
try {
yield call(delay, 1000);
if (Math.random() > 0.8) {
throw new Error();
}
yield put(asyncActions.decrementDone());
} catch {
yield put(asyncActions.decrementFailed());
} finally {
if (yield cancelled()) {
yield put(asyncActions.decrementFailed());
}
}
}
export function* rootSaga(): SagaIterator {
yield takeEvery(ActionType.increment, incrementAsyncWorker);
yield takeEvery(ActionType.decrement, decrementAsyncWorker);
}
export interface CounterState {
readonly isLoading: boolean;
readonly errorMessage: string;
readonly count: number;
}
export interface State {
readonly counter: CounterState;
}
const initialState: CounterState = {
isLoading: false,
errorMessage: '',
count: 0,
};
export const reducer: Reducer<State['counter'], Action | AsyncAction> = (
state = initialState,
action
) => {
switch (action.type) {
case AsyncActionType.incrementStarted:
case AsyncActionType.decrementStarted:
return {
...state,
isLoading: true,
errorMessage: '',
};
case AsyncActionType.incrementFailed:
case AsyncActionType.decrementFailed:
return {
...state,
isLoading: false,
errorMessage: 'Request failed',
};
case AsyncActionType.incrementDone:
return {
...state,
isLoading: false,
count: state.count + 1,
};
case AsyncActionType.decrementDone:
return {
...state,
isLoading: false,
count: state.count - 1,
};
default:
return state;
}
};
import * as React from 'react';
import { Dispatch, bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { Action, State, CounterState, actions } from './counter';
export interface IProps {
readonly title: string;
}
export interface IStateProps {
counter: CounterState;
}
export interface IDispatchProps extends ReturnType<typeof mapDispatchToProps> {}
const Counter: React.SFC<IProps & IStateProps & IDispatchProps> = ({
title,
counter,
increment,
decrement,
}): JSX.Element => {
const handleIncrement = (e: React.MouseEvent<HTMLButtonElement>) =>
increment();
const handleDecrement = (e: React.MouseEvent<HTMLButtonElement>) =>
decrement();
return (
<div>
<h1>{title}</h1>
<p>Clicked: {counter.count} times</p>
<button disabled={counter.isLoading} onClick={handleIncrement}>
+
</button>
<button disabled={counter.isLoading} onClick={handleDecrement}>
-
</button>
{counter.errorMessage && <p>{counter.errorMessage}</p>}
</div>
);
};
const mapStateToProps = (state: State): IStateProps => ({
counter: state.counter,
});
const mapDispatchToProps = (dispatch: Dispatch<Action>) =>
bindActionCreators(actions, dispatch);
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter);
@sunilbenur45
Copy link

Can you make combining all things in one app add it in simple repo it will be easy to understand thing easily

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment