Skip to content
Open
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
104 changes: 89 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,73 @@ 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed indentation

}
}

```

Your service will receive an object with the nextPage prop.

Example of using:

```js
//reducer.js

const stateDescription = {
tickets: null
};

const initialState = completeState(stateDescription);

const reducerDescription = {
paginationActions: [actions.GET_TICKETS]
};

//actions.js

const formatPagination = response => ({
list: response.data?.data,
meta: {
totalPages: response.data?.meta?.total_pages,
currentPage: response.data?.meta?.current_page,
totalItems: response.data?.meta?.total
}
});

export const actionCreators = {
getTickets: (newPagination = true) => ({
type: actions.GET_TICKETS,
target: ticketsTarget,
paginationAction: true,
refresh: newPagination,
reducerName: "tickets",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having to specify the reducer you are in doesn't look good to me.

service: TicketService.getTickets,
successSelector: response => formatPagination(response)
})
};

//service.js
const getTickets = ({ nextPage }) => api.get(`/tickets?page=${nextPage}`);
```

`IMPORTANT`: If you want to send more info to your service, your `payload` will have to return an object, not a single value. (Only if you are using paginationActions)

## 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 +245,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
15 changes: 14 additions & 1 deletion src/completers/completeReducer/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import onLoading from '../../effects/onLoading';
import onSuccess from '../../effects/onSuccess';
import onSuccessPagination from '../../effects/onSuccessPagination';
import onFailure from '../../effects/onFailure';

import onSubscribe from '../../effects/onSubscribe';
Expand All @@ -12,7 +13,8 @@ function completeReducer(reducerDescription) {
if (
!reducerDescription ||
((!reducerDescription.primaryActions || !reducerDescription.primaryActions.length) &&
(!reducerDescription.modalActions || !reducerDescription.modalActions.length))
(!reducerDescription.modalActions || !reducerDescription.modalActions.length) &&
(!reducerDescription.paginationActions || !reducerDescription.paginationActions.length))
) {
throw new Error('Reducer description is incomplete, should contain at least an actions field to complete');
}
Expand All @@ -30,6 +32,17 @@ 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');
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
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}`]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[`${action.target}`]:
[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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to modify completeState as well, in order to add all these new targets

[`${action.target}NextPage`]: Number(selector(action).meta.currentPage) + 1,
...(selector(action).meta.totalItems && {
[`${action.target}TotalItems`]: Number(selector(action).meta.totalItems)
})
})
: mergeState(state, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can call onSuccess directly here

[`${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, refresh },
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
22 changes: 19 additions & 3 deletions src/injections/composeInjections/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import mergeInjections from '../mergeInjections';

const checkPaginationNotHasFinished = (state, pageSelector) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name is a little bit confusing. Would you like to name it using the positive case ?

pageSelector.refresh ||
(!state[pageSelector.reducerName][`${pageSelector.target}Loading`] &&
state[pageSelector.reducerName][`${pageSelector.target}NextPage`] <=
state[pageSelector.reducerName][`${pageSelector.target}TotalPages`]);

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

Expand All @@ -12,14 +18,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)) ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm I don't understand this part

(paginationAction && !paginationNotHasFinished) ||
determination(response)
) {
const shouldContinue = success(dispatch, response, getState());
if (shouldContinue) postSuccess(dispatch, response, getState());
} else {
Expand Down
Loading