Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example app #14

Merged
merged 2 commits into from
Dec 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules/
.idea
yarn.lock
example/
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ If I would store the submitting and errors in the Redux store why would I use Fo
The second possible solution is to pass the Formik helpers (i.e. setSubmitting or setErrors) as an action props.
Or... You can use this library instead. 🙂

## 💈 Example

Please check out `example` directory.

## 🧰 Usage

### Include promise middleware
Expand Down
12 changes: 12 additions & 0 deletions example/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "example",
"version": "1.0.0",
"description": "Example app using redux-saga-promise-actions",
"main": "src/index.ts",
"author": "Tomek Kleszcz <tomekkleszcz@icloud.com>",
"license": "MIT",
"private": true,
"dependencies": {
"axios": "^0.21.0"
}
}
26 changes: 26 additions & 0 deletions example/src/actions/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//Action creators
import {createPromiseAction} from '../../../dist'; //Replace with redux-saga-promise-actions

//Types
import {Credentials, TokenPair} from '../types/auth';

/**
* signIn promise action declaration
* If you want payload to be empty in any stage declare it as undefined.
* @type {PromiseActionSet<'SIGN_IN_REQUEST', 'SIGN_IN_SUCCESS', 'SIGN_IN_FAILURE', Credentials, TokenPair, undefined>}
*/
export const signIn = createPromiseAction(
'SIGN_IN_REQUEST',
'SIGN_IN_SUCCESS',
'SIGN_IN_FAILURE'
)<Credentials, TokenPair, undefined>();

/**
* signOut promise action declaration
* @type {PromiseActionSet<'SIGN_OUT_REQUEST', 'SIGN_OUT_SUCCESS', 'SIGN_OUT_FAILURE', undefined, undefined, undefined>}
*/
export const signOut = createPromiseAction(
'SIGN_OUT_REQUEST',
'SIGN_OUT_SUCCESS',
'SIGN_OUT_FAILURE'
)<undefined, undefined, undefined>();
28 changes: 28 additions & 0 deletions example/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//Redux
import {combineReducers, createStore, applyMiddleware} from 'redux';

//Middlewares
import createSagaMiddleware from 'redux-saga';
import {promiseMiddleware} from '../../'; //Replace with redux-saga-promise-actions

//Reducers
import authReducer from './reducers/auth';

//Sagas
import {saga} from './sagas';

const reducer = combineReducers({
auth: authReducer
});

const sagaMiddleware = createSagaMiddleware();

export const store = createStore(
reducer,
{},
applyMiddleware(promiseMiddleware, sagaMiddleware) //Add promiseMiddleware before sagaMiddleware in the chain
);

sagaMiddleware.run(saga);

export type RootState = ReturnType<typeof reducer>;
31 changes: 31 additions & 0 deletions example/src/reducers/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//Reducer
import {action, createReducer} from "typesafe-actions";

//Actions
import * as actions from "../actions/auth";

//Types
import {ActionType} from "typesafe-actions";
import {TokenPair, Nullable} from "../types/auth";

export type AuthAction = ActionType<typeof actions>;

export type State = Readonly<Nullable<TokenPair>>;

export const initialState: State = {
accessToken: null,
tokenType: null,
refreshToken: null,
};

export default createReducer<State, AuthAction>(initialState)
.handleAction(actions.signIn.success, (state, action) => ({
...state,
...action.payload,
}))
.handleAction(actions.signOut.success, (state) => ({
...state,
accessToken: null,
tokenType: null,
refreshToken: null,
}));
54 changes: 54 additions & 0 deletions example/src/sagas/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//Effect creators
import {takeLeading, select, put} from 'redux-saga/effects';
import {takeEveryPromiseAction} from '../../../effects'; //Replace with redux-saga-promise-actions/effects

//Actions
import * as actions from '../actions/auth';

//Promise action utils
import {resolvePromiseAction} from '../../../'; //Replace with redux-saga-promise-actions

//Services
import axios from 'axios';

//Types
import {RootState} from '../';
import {TokenPair} from '../types/auth';

/**
* Saga worker which takes every signIn promise action and automatically resolves it with the request response.
* @param {PromiseAction<'SIGN_IN_REQUEST', Credentials, TokenTyoe>} action signIn request action
*/
function* signIn({payload}: ReturnType<typeof actions.signIn.request>) {
return yield axios.post<TokenPair>('/auth/sign-in', payload, {
headers: {
'Content-Type': 'application/json'
}
});
}

/**
* Saga worker which takes leading signOut promise action, makes a request, and resolve despite if the request has succeeded.
* @param {PromiseAction<'SIGN_OUT_REQUEST', undefined, undefined>} action signOut request action
*/
function* signOut(action: ReturnType<typeof actions.signOut.request>) {
try {
const tokenPair = yield select<(state: RootState) => TokenPair>(state => state.auth);

yield axios.post('/auth/sign-out', null, {
headers: {
Authorization: `${tokenPair.tokenType} ${tokenPair.accessToken}`
}
});
} catch(err) {
console.error(err);
}

yield put(actions.signOut.request());
yield resolvePromiseAction(action);
}

export const authSaga = [
takeEveryPromiseAction(actions.signIn, signIn),
takeLeading(actions.signOut.request, signOut)
]
11 changes: 11 additions & 0 deletions example/src/sagas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//Redux saga
import {all} from 'redux-saga/effects';

//Sagas
import {authSaga} from './auth';

export function* saga() {
yield all([
...authSaga
]);
}
14 changes: 14 additions & 0 deletions example/src/types/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type Credentials = {
email: string;
password: string;
}

export type TokenPair = {
accessToken: string;
tokenType: string;
refreshToken: string;
}

export type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
15 changes: 15 additions & 0 deletions example/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


axios@^0.21.0:
version "0.21.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.0.tgz#26df088803a2350dff2c27f96fef99fe49442aca"
integrity sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==
dependencies:
follow-redirects "^1.10.0"

follow-redirects@^1.10.0:
version "1.13.1"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7"
integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==
13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
"repository": "git@github.com:tomekkleszcz/redux-saga-promise-actions.git",
"author": "Tomek Kleszcz <tomekkleszcz@icloud.com>",
"license": "MIT",
"keywords": [
"redux",
"redux-saga",
"promise-action",
"promise",
"action",
"promise-middleware",
"middleware",
"redux-store",
"store"
],
"scripts": {
"prepublish": "tsc"
},
Expand All @@ -17,6 +28,6 @@
"typesafe-actions": "^5.1.0"
},
"devDependencies": {
"typescript": "^4.1.2"
"typescript": "^4.1.3"
}
}
99 changes: 83 additions & 16 deletions src/effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,39 @@ import {
} from "redux-saga/effects";

//Types
import {PromiseActionSet, rejectPromiseAction, resolvePromiseAction} from "./";
import {TypeConstant} from "typesafe-actions";
import {
PromiseActionSet,
PromiseAction,
rejectPromiseAction,
resolvePromiseAction,
} from "./";

type EffectCreator = typeof takeEvery | typeof takeLeading | typeof takeLatest;

type Worker<A extends PromiseActionSet<any, any, any, any, any, any>> = (action: ReturnType<A["request"]>) => any;
type Worker<RequestType extends TypeConstant, X, Y> = (
action: PromiseAction<RequestType, X, Y>
) => any;

function* promiseActionWrapper<
A extends PromiseActionSet<any, any, any, any, any, any>
>(promiseAction: A, action: ReturnType<A['request']>, worker: Worker<A>) {
RequestType extends TypeConstant,
SuccessType extends TypeConstant,
FailureType extends TypeConstant,
X,
Y,
Z
>(
promiseAction: PromiseActionSet<
RequestType,
SuccessType,
FailureType,
X,
Y,
Z
>,
action: PromiseAction<RequestType, X, Y>,
worker: Worker<RequestType, X, Y>
) {
try {
const payload = yield call(worker, action);
yield resolvePromiseAction(action, payload);
Expand All @@ -28,41 +52,84 @@ function* promiseActionWrapper<

function effectCreatorFactory<
E extends EffectCreator,
A extends PromiseActionSet<any, any, any, any, any, any>
>(effectCreator: E, promiseAction: A, worker: Worker<A>) {
return effectCreator(promiseAction.request, (action: ReturnType<A['request']>) => promiseActionWrapper(promiseAction, action, worker)
RequestType extends TypeConstant,
SuccessType extends TypeConstant,
FailureType extends TypeConstant,
X,
Y,
Z
>(
effectCreator: E,
promiseAction: PromiseActionSet<
RequestType,
SuccessType,
FailureType,
X,
Y,
Z
>,
worker: Worker<RequestType, X, Y>
) {
return effectCreator(
promiseAction.request,
(action: PromiseAction<RequestType, X, Y>) =>
promiseActionWrapper(promiseAction, action, worker)
);
}

/**
* Spawns a saga on each particular promise action dispatched to the store. If saga succeeds action is resolved with return value as payload. Otherwise it gets rejected.
* @param {A} action Promise action to watch
* @param {PromiseActionSet<RequestType, SuccessType, FailureType, X, Y, Z>} action Promise action to watch
* @param worker A generator function
*/
export function takeEveryPromiseAction<
A extends PromiseActionSet<any, any, any, any, any, any>
>(action: A, worker: Worker<A>) {
RequestType extends TypeConstant,
SuccessType extends TypeConstant,
FailureType extends TypeConstant,
X,
Y,
Z
>(
action: PromiseActionSet<RequestType, SuccessType, FailureType, X, Y, Z>,
worker: Worker<RequestType, X, Y>
) {
return effectCreatorFactory(takeEvery, action, worker);
}

/**
* Spawns a saga on each particular promise action dispatched to the store. Automatically cancels any previous sagas started previously if it's still running. If saga succeeds action is resolved with return value as payload. Otherwise it gets rejected.
* @param {A} action Promise action to watch
* @param {PromiseActionSet<RequestType, SuccessType, FailureType, X, Y, Z>} action Promise action to watch
* @param worker A generator function
*/
export function takeLeadingPromiseAction<
A extends PromiseActionSet<any, any, any, any, any, any>
>(action: A, worker: Worker<A>) {
RequestType extends TypeConstant,
SuccessType extends TypeConstant,
FailureType extends TypeConstant,
X,
Y,
Z
>(
action: PromiseActionSet<RequestType, SuccessType, FailureType, X, Y, Z>,
worker: Worker<RequestType, X, Y>
) {
return effectCreatorFactory(takeLeading, action, worker);
}

/**
* Spawns a saga on each particular promise action dispatched to the store. After spawning a task once, it blocks until spawned saga completes and then starts to listen for an action again. If saga succeeds action is resolved with return value as payload. Otherwise it gets rejected.
* @param {A} action Promise action to watch
* @param {PromiseActionSet<RequestType, SuccessType, FailureType, X, Y, Z>} action Promise action to watch
* @param worker A generator function
*/
export function takeLatestPromiseAction<
A extends PromiseActionSet<any, any, any, any, any, any>
>(action: A, worker: Worker<A>) {
RequestType extends TypeConstant,
SuccessType extends TypeConstant,
FailureType extends TypeConstant,
X,
Y,
Z
>(
action: PromiseActionSet<RequestType, SuccessType, FailureType, X, Y, Z>,
worker: Worker<RequestType, X, Y>
) {
return effectCreatorFactory(takeLatest, action, worker);
}
Loading