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

Pagination #1

Merged
merged 3 commits into from
Jan 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
58 changes: 43 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@

![versión npm](https://img.shields.io/npm/v/redux-recompose.svg?color=68d5f7)
![Download npm](https://img.shields.io/npm/dw/redux-recompose.svg?color=7551bb)
[![codecov](https://codecov.io/gh/Wolox/redux-recompose/branch/master/graph/badge.svg)](https://codecov.io/gh/Wolox/redux-recompose)
[![supported by](https://img.shields.io/badge/supported%20by-Wolox.💗-blue.svg)](https://www.wolox.com.ar/)
# Redux-recompose

# Redux-recompose

![Vertical Logo Redux-recompose](./logo/images/Redux_vertical_small@2x.png)

## Why another Redux library ?
Expand All @@ -18,14 +19,14 @@ Usually, we are used to write:
// actions.js

function increment(anAmount) {
return { type: 'INCREMENT', payload: anAmount };
return { type: "INCREMENT", payload: anAmount };
}

// reducer.js

function reducer(state = initialState, action) {
switch(action.type) {
case 'INCREMENT':
switch (action.type) {
case "INCREMENT":
return { ...state, counter: state.counter + action.payload };
default:
return state;
Expand All @@ -40,18 +41,20 @@ With the new concept of _target_ of an action, we could write something like:

// Define an action. It will place the result on state.counter
function increment(anAmount) {
return { type: 'INCREMENT', target: 'counter', payload: anAmount };
return { type: "INCREMENT", target: "counter", payload: anAmount };
}


// reducer.js
// Create a new effect decoupled from the state structure at all.
const onAdd = (state, action) => ({ ...state, [action.target]: state[action.target] + action.payload });
const onAdd = (state, action) => ({
...state,
[action.target]: state[action.target] + action.payload
});

// Describe your reducer - without the switch
const reducerDescription = {
'INCREMENT': onAdd()
}
INCREMENT: onAdd()
};

// Create it !
const reducer = createReducer(initialState, reducerDescription);
Expand Down Expand Up @@ -140,8 +143,12 @@ completeFromProps: Helps to write a state from propTypes definition
And to introduce completers that support custom patterns:

```js
const initialStateDescription = { msg: '' };
const initialState = completeCustomState(initialStateDescription, ['Info', 'Warn', 'Error']);
const initialStateDescription = { msg: "" };
const initialState = completeCustomState(initialStateDescription, [
"Info",
"Warn",
"Error"
]);
// initialState.toEqual({ msg: '', msgInfo: '', msgWarn: '', msgError: '' });
```

Expand All @@ -158,6 +165,27 @@ There's currently documentation for the following:
- [withStatusHandling](./src/injections/withStatusHandling/)
- [withSuccess](./src/injections/withSuccess/)

## Pagination Actions

You will have to write actions with the following params:

- paginationAction (boolean)
- reducerName (The name of the reducer you are going to handle this) (use only if paginationAction is true)
- refresh (Param that probably you receive in your action, you are going to set it to false when you want to have next pages. By default is setted to true)
- successSelector (This last param have to transform the response and return the following object)

```js
{
list,
meta: {
totalPages,
currentPage,
totalItems // This last item is not necessary but maybe you will need it for something specific.
}
}

```

## Middlewares

Middlewares allow to inject logic between dispatching the action and the actual desired change in the store. Middlewares are particularly helpful when handling asynchronous actions.
Expand All @@ -171,16 +199,16 @@ The following are currently available:
The way `redux-recompose` updates the redux state can be configured. The default configuration is

```js
(state, newContent) => ({ ...state, ...newContent })
(state, newContent) => ({ ...state, ...newContent });
```

You can use `configureMergeState` to override the way `redux-recompose` handles state merging. This is specially useful when you are using immutable libraries.
For example, if you are using `seamless-immutable` to keep your store immutable, you'll want to use it's [`merge`](https://github.com/rtfeldman/seamless-immutable#merge) function. You can do so with the following configuration:

```js
import { configureMergeState } from 'redux-recompose';
import { configureMergeState } from "redux-recompose";

configureMergeState((state, newContent) => state.merge(newContent))
configureMergeState((state, newContent) => state.merge(newContent));
```

## Thanks to
Expand Down
41 changes: 29 additions & 12 deletions src/completers/completeReducer/index.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
import onLoading from '../../effects/onLoading';
import onSuccess from '../../effects/onSuccess';
import onFailure from '../../effects/onFailure';
import onLoading from "../../effects/onLoading";
import onSuccess from "../../effects/onSuccess";
import onFailure from "../../effects/onFailure";

import onSubscribe from '../../effects/onSubscribe';
import onUnsubscribe from '../../effects/onUnsubscribe';
import onSubscribe from "../../effects/onSubscribe";
import onUnsubscribe from "../../effects/onUnsubscribe";

import { isStringArray, isValidObject } from '../../utils/typeUtils';
import { isStringArray, isValidObject } from "../../utils/typeUtils";

// Given a reducer description, it returns a reducerHandler with all success and failure cases
function completeReducer(reducerDescription) {
if (
!reducerDescription ||
((!reducerDescription.primaryActions || !reducerDescription.primaryActions.length) &&
(!reducerDescription.modalActions || !reducerDescription.modalActions.length))
((!reducerDescription.primaryActions ||
!reducerDescription.primaryActions.length) &&
(!reducerDescription.modalActions ||
!reducerDescription.modalActions.length))
) {
throw new Error('Reducer description is incomplete, should contain at least an actions field to complete');
throw new Error(
"Reducer description is incomplete, should contain at least an actions field to complete"
);
}

let reducerHandler = {};

if (reducerDescription.primaryActions) {
if (!isStringArray(reducerDescription.primaryActions)) {
throw new Error('Primary actions must be a string array');
throw new Error("Primary actions must be a string array");
}
reducerDescription.primaryActions.forEach(actionName => {
reducerHandler[actionName] = onLoading();
Expand All @@ -30,9 +34,20 @@ function completeReducer(reducerDescription) {
});
}

if (reducerDescription.paginationActions) {
if (!isStringArray(reducerDescription.paginationActions)) {
throw new Error("Primary actions must be a string array");
}
reducerDescription.paginationActions.forEach(actionName => {
reducerHandler[actionName] = onLoading();
reducerHandler[`${actionName}_SUCCESS`] = onSuccessPagination();
reducerHandler[`${actionName}_FAILURE`] = onFailure();
});
}

if (reducerDescription.modalActions) {
if (!isStringArray(reducerDescription.modalActions)) {
throw new Error('Modal actions must be a string array');
throw new Error("Modal actions must be a string array");
}
reducerDescription.modalActions.forEach(actionName => {
reducerHandler[`${actionName}_OPEN`] = onSubscribe();
Expand All @@ -42,7 +57,9 @@ function completeReducer(reducerDescription) {

if (reducerDescription.override) {
if (!isValidObject(reducerDescription.override)) {
throw new Error('Reducer description containing a override is not an object');
throw new Error(
"Reducer description containing a override is not an object"
);
}
reducerHandler = { ...reducerHandler, ...reducerDescription.override };
}
Expand Down
6 changes: 3 additions & 3 deletions src/completers/completeState/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { isStringArray, isValidObject } from '../../utils/typeUtils';
import { isStringArray, isValidObject } from "../../utils/typeUtils";

// Given a defaultState, it populates that state with ${key}Loading and ${key}Error
function completeState(defaultState, ignoredTargets = []) {
if (!isValidObject(defaultState)) {
throw new Error('Expected an object as a state to complete');
throw new Error("Expected an object as a state to complete");
}
if (!isStringArray(ignoredTargets)) {
throw new Error('Expected an array of strings as ignored targets');
throw new Error("Expected an array of strings as ignored targets");
}

const completedState = { ...defaultState };
Expand Down
28 changes: 28 additions & 0 deletions src/effects/onSuccessPagination/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## onSuccess - Effect

This effect is used when describing the `SUCCESS` case of the `SUCCESS-FAILURE` pattern.

This effect is a multi-target effect - It modifies more than one target at the same time.

It will:
* Put `${action.target}Loading` in `false`
* Put `${action.target}Error` in `null`
* Fill `${action.target}` with your `action.payload` by default, or use a selector provided

Example:
```js
const selector =
(action, state) => action.payload || state[action.target];

const reducerDescription = {
'SUCCESS': onSuccess(),
'SUCCESS_CUSTOM': onSuccess(selector)
};
```

### Custom selectors
`onSuccess` effect receives an optional parameter:
* selector: This function describes how we read the data from the `action`.
`(action, state) => any`
By default, is:
`action => action.payload`
27 changes: 27 additions & 0 deletions src/effects/onSuccessPagination/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { mergeState } from '../../configuration';

// TODO: Add support and validations for multi target actions

function onSuccessPagination(selector = action => action.payload) {
return (state, action) =>
(selector(action).list
? mergeState(state, {
[`${action.target}Loading`]: false,
[`${action.target}Error`]: null,
[`${action.target}`]:
Number(selector(action).meta.currentPage) === 1
? selector(action).list
: state[action.target].concat(selector(action).list),
[`${action.target}TotalPages`]: Number(selector(action).meta.totalPages),
[`${action.target}NextPage`]: Number(selector(action).meta.currentPage) + 1,
...(selector(action).meta.totalItems && {
[`${action.target}TotalItems`]: Number(selector(action).meta.totalItems)
})
})
: mergeState(state, {
[`${action.target}Loading`]: false,
[`${action.target}Error`]: null
}));
}

export default onSuccessPagination;
43 changes: 43 additions & 0 deletions src/effects/onSuccessPagination/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Immutable from 'seamless-immutable';

import createReducer from '../../creators/createReducer';

import onSuccessPagination from '.';

const initialState = {
target: null,
targetLoading: true,
targetError: 'Some error'
};

const setUp = {
state: null
};

beforeEach(() => {
setUp.state = Immutable(initialState);
});

describe('onSuccessPagination', () => {
it('Sets correctly target with error, loading, totalPages, nextPages y TotalItems', () => {
const reducer = createReducer(setUp.state, {
'@@ACTION/TYPE': onSuccessPagination()
});
const newState = reducer(setUp.state, {
type: '@@ACTION/TYPE',
target: 'target',
payload: {
list: ['item 1', 'item 2'],
meta: { totalPages: 1, currentPage: 1, totalItems: 2 }
}
});
expect(newState).toEqual({
target: ['item 1', 'item 2'],
targetLoading: false,
targetError: null,
targetTotalPages: 1,
targetNextPage: 2,
targetTotalItems: 2
});
});
});
27 changes: 24 additions & 3 deletions src/injections/baseThunkAction/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,38 @@ function baseThunkAction({
target,
service,
payload = () => {},
paginationAction = false,
reducerName,
refresh = true,
successSelector = response => response.data,
failureSelector = response => response.problem
}) {
const pageSelector = state =>
paginationAction && {
nextPage: refresh ? 1 : state[reducerName][`${target}NextPage`]
};
const selector = typeof payload === 'function' ? payload : () => payload;

const finalSelector = state =>
(paginationAction ? { ...pageSelector(state), ...selector(state) } : selector(state));
return {
prebehavior: dispatch => dispatch({ type, target }),
apiCall: async getState => service(selector(getState())),
apiCall: async getState => service(finalSelector(getState())),
determination: response => response.ok,
success: (dispatch, response) => dispatch({ type: `${type}_SUCCESS`, target, payload: successSelector(response) }),
failure: (dispatch, response) => dispatch({ type: `${type}_FAILURE`, target, payload: failureSelector(response) })
paginationAction,
pageSelector: { reducerName, target },
success: (dispatch, response) =>
dispatch({
type: `${type}_SUCCESS`,
target,
payload: response && successSelector(response)
}),
failure: (dispatch, response) =>
dispatch({
type: `${type}_FAILURE`,
target,
payload: failureSelector(response)
})
};
}

Expand Down
20 changes: 17 additions & 3 deletions src/injections/composeInjections/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import mergeInjections from '../mergeInjections';

const checkPaginationNotHasFinished = (state, pageSelector) =>
state[pageSelector.reducerName][`${pageSelector.target}NextPage`] <=
state[pageSelector.reducerName][`${pageSelector.target}TotalPages`];

function composeInjections(...injections) {
const injectionsDescription = mergeInjections(injections);

Expand All @@ -12,14 +16,24 @@ function composeInjections(...injections) {
postBehavior = () => {},
postFailure = () => {},
failure = () => {},
statusHandler = () => true
statusHandler = () => true,
pageSelector,
paginationAction
} = injectionsDescription;

return async (dispatch, getState) => {
prebehavior(dispatch);
const response = await apiCall(getState);
const paginationNotHasFinished =
paginationAction && checkPaginationNotHasFinished(getState(), pageSelector);
const response = paginationAction
? paginationNotHasFinished && (await apiCall(getState))
: await apiCall(getState);
postBehavior(dispatch, response);
if (determination(response)) {
if (
(paginationAction && paginationNotHasFinished && determination(response)) ||
(paginationAction && !paginationNotHasFinished) ||
determination(response)
) {
const shouldContinue = success(dispatch, response, getState());
if (shouldContinue) postSuccess(dispatch, response, getState());
} else {
Expand Down
Loading