-
Notifications
You must be signed in to change notification settings - Fork 16
Handle pagination in recompose #76
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
base: master
Are you sure you want to change the base?
Changes from all commits
1ac405e
44c2342
c2954ad
ec69f16
ff8c1d6
f10a85d
7e770c9
9bee581
28761ed
a62f128
a1595ab
9867f37
e58bf62
9748daa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,10 @@ | ||
|
|
||
|  | ||
|  | ||
| [](https://codecov.io/gh/Wolox/redux-recompose) | ||
| [](https://www.wolox.com.ar/) | ||
| # Redux-recompose | ||
|
|
||
| # Redux-recompose | ||
|
|
||
|  | ||
|
|
||
| ## Why another Redux library ? | ||
|
|
@@ -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; | ||
|
|
@@ -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); | ||
|
|
@@ -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: '' }); | ||
| ``` | ||
|
|
||
|
|
@@ -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. | ||
| } | ||
| } | ||
|
|
||
| ``` | ||
|
|
||
| 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", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
@@ -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 | ||
|
|
||
| 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}`]: | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| Number(selector(action).meta.currentPage) === 1 | ||||||
| ? selector(action).list | ||||||
| : state[action.target].concat(selector(action).list), | ||||||
| [`${action.target}TotalPages`]: Number(selector(action).meta.totalPages), | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you need to modify |
||||||
| [`${action.target}NextPage`]: Number(selector(action).meta.currentPage) + 1, | ||||||
| ...(selector(action).meta.totalItems && { | ||||||
| [`${action.target}TotalItems`]: Number(selector(action).meta.totalItems) | ||||||
| }) | ||||||
| }) | ||||||
| : mergeState(state, { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you can call |
||||||
| [`${action.target}Loading`]: false, | ||||||
| [`${action.target}Error`]: null | ||||||
| })); | ||||||
| } | ||||||
|
|
||||||
| export default onSuccessPagination; | ||||||
| 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 | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,11 @@ | ||
| import mergeInjections from '../mergeInjections'; | ||
|
|
||
| const checkPaginationNotHasFinished = (state, pageSelector) => | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
|
||
|
|
@@ -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)) || | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needed indentation