UNPKG

redux-boost

Version:
385 lines (283 loc) 14.4 kB
# Redux Boost `redux-boost` works with [React Redux](https://github.com/reactjs/react-redux) to enable smooth HTTP request handling in [React](https://github.com/facebook/react) using [Redux](https://github.com/reactjs/redux) to store all the request updates. ## TODO: - [x] Add [interactive example app](https://codesandbox.io/s/3rqv2q3695) - [x] Add Getting Started section - [ ] Add API reference (WIP) ## Installation `npm install --save redux-boost` or `yarn add redux-boost` ## Documentation * [**Getting Started**](#getting-started) - [Overview](#overview) - [Data flow](#data-flow) - [Basic Usage Guide](#basic-usage-guide) * [**API**](#api) - [createRequestMiddleware()](#createrequestmiddlewareconfig) - [createActions()](#createactionsactiontypes--prefix) - [createReducer()](#createreducerhandlers--defaultstate) - [restQuery()](#) - [restMutation()](#) - [createSaga()](#) - [selectors](#) * [**Examples**](#examples) + [Store configuration](#store-configuration) + [React integration](#react-integration) ## Getting Started The basic implementation of `redux-boost` is simple. However, to make the most of it, it's recommended to have basic knowledge on: - [Redux](http://redux.js.org/) state container, - [React](https://facebook.github.io/react/) and [Higher-Order Components (HOCs)](https://facebook.github.io/react/docs/higher-order-components.html). ### Overview To connect your React form components to your Redux store you'll need the following pieces from the `redux-boost` package: - Redux middleware `createRequestMiddleware()`. - Redux Reducers: `requestsReducer` and `dataReducer`, - React HOC `restQuery()` for `GET` requests. - React HOC `restMutation()` for other types of requests. It's important to understand their responsibilities: | | type | responsibility | | ------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `requestsReducer` `dataReducer` | _reducer_ | function that tells how to update the Redux store based on changes coming from the application; those changes are described by Redux actions | | `restQuery()` `restMutation()` | _HOC_ | function that takes configuration object and returns a new function; use it to wrap your React component and bind user interaction to dispatch of Redux actions | | `requestMiddleware` | _middleware_ | function that provides a third-party extension point between dispatching an action, and the moment it reaches the reducer | ### Data flow The diagram below represents the simplified data flow. Note that in most cases you don't need to worry about the [action creators](http://redux-form.com/7.4.2/docs/api/ActionCreators.md/) for yourself, as they're already bound to dispatch for certain actions. Let's go through a simple example. We have a React component wrapped with `restQuery()`. There is a list inside of it that need data to be fetched from the server to be rendered correctly. The data flows like this: 1. Component gets rendered for the first time, 2. `restQuery` dispatches `fetchStart` action with `query` description passed into it, 3. `requestMiddleware` catches this action and initiates HTTP request, 4. `requestsReducer` catches this action too and updatse Redux state with request state, 5. `restQuery` updates wrapped component passing request state into it, 6. On request resolution `requestMiddleware` dispatches a relevant action, either `fetchSuccess` or `fetchFail`, 7. `requestsReducer` updates request state with the result or an error 8. `restQuery` updates wrapped component with the final request state, With `redux-boost` comes a lot more: hooks for error handling and props to refetch the data, data selectors and action creators. This guide describes the basic usage – feel free to dig deeper. ### Basic Usage Guide #### Step 1 of 3: Request middleware The store should know how to handle actions with quert descriptions to intiate HTTP requests. To enable this, we need to pass the `createRequestMiddleware` to your store. ```js import axios from 'axios' import { applyMiddleware } from 'redux' import { createRequestMiddleware } from 'redux-boost' const middlewares = [ createRequestMiddleware({ // Executor is function that will initiate HTTP requests executor: axios }), // ...otherMiddlewares, ] export const enhancer = applyMiddleware(...middlewares) ``` Now your store knows how to initiate HTTP requests received from the certain actions. #### Step 2 of 3: Request and data reducers The store should know how to handle actions coming from the form components. To enable this, we need to pass the `requestsReducer` and `dataReducer` to your store. It serves for **all of your form components**, so you only have to pass it once. ```js import { createStore, combineReducers } from 'redux' import { dataReducer, requestsReducer } from 'redux-boost' const rootReducer = combineReducers({ // ...your other reducers here // you have to pass requestsReducer under 'requests' key, // and dataReducer under 'data` key. data: dataReducer, requests: requestsReducer, }) const store = createStore(rootReducer) ``` Now your store knows how to handle actions coming from the form components. **NOTE:** The keys used to pass the `redux-boost` reducer should be named **`requests`** and **`data`**. #### Step 3 of 3: High-order components To make your React component communicate with the store, we need to wrap it with `restQuery()` to receive data from the server or `restMutation()` to mutate data on the server. It will provide the props about the current request state and function to refetch data. ```js import React from 'react' import { restQuery } from 'redux-boost' const FriendList = props => { // getFriends prop updates on each request stage: [START, SUCCESS, FAIL] const { getFriends: { result, loading, error } } = props if (loading) return <Spinner>...loading</Spinner> if (error) return <Error>{error.message}</Error> return result.map(friend => <div key={friend.id}>{friend.name}</div>) } export default restQuery({ // a unique name the request, will be used as a prop name and redux state key name: 'getFriends' // options allows to use ownProps of the component // to create dynamic parts of the query options: ({ userId }) => ({ // payload will be passed to the executor function, it's axios in our example payload: 'http://awesome.api.com/friends?userId=${userId}' }), // we don't want to fetch anything if userId is not defined skip: ({ userId }) => !userId, })(FriendList) ``` **NOTE**: If the `()()` syntax seems confusing, you can always break it down into two steps: ```js // ... // create new, "configured" function createRestQuery = restQuery(configuration) // evaluate it for FriendList component const FriendListContainer = createRestQuery(FriendList) export default FriendListContainer ``` ## API #### `createRequestMiddleware(config)` Creates a middleware that will handle request actions generated by the library to make network requests. Configuration keys: | Key | Default value | Description | | ----------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | method | `'GET'` | Default HTTP method to use if no method passed in the request action. | | | executor | `fetch()` | A function that will execute HTTP request `executor(...requestAction.payload)` | | prepareExecutor | | A function that is called before the request to alter executor in some way using request description. `prepareExecutor(executor, requestAction)` | serialize | | A function that is called with the body of the response, allowing you to transform it. `serialize(response)` | | onSuccess | | A function that is only called on network request successful completion. `onSuccess({ name, result })` | | onError | | A function that is only called on network request error. `onError({ name, error })` | #### `createActions(actionTypes [, prefix])` Returns an object with [action creators](#action-creator). If `prefix` is passed, it will be used to prefix type in each action creator. ```js const actions = createActions([ 'set', 'delete', 'update', ], 'users') // actions.set(profile) -> { type: 'users/set', payload: profile } // actions.delete({ id: 77 }) -> { type: 'users/delete', payload: { id: 77 } } // actions.set.type === 'users/set' // String(actions.set) === 'users/set' // actions.set.isReduxAction === true ``` Apart from `payload`, the second argument in action creator creates `meta` property, which can be useful in middlewares. ```js // actions.update({ name }, callback) -> // { // type: 'users/update', // payload: { name }, // meta: callback // } ``` #### `createReducer(handlers [, defaultState])` Returns a new reducer. - **handlers**: `object`, that provides mapping between action types and action handlers. - **defaultState** (anything, optional): the initial state of the reducer. Must not be empty if you plan to use this reducer inside a `combineReducers` ```javascript import { actions } from './actions' const DECREMENT_ACTION_TYPE = 'DECREMENT' const counterReducer = createReducer({ [actions.increment]: (state) => state + 1, [actions.add]: (state, payload) => state + payload, [DECREMENT_ACTION_TYPE]: state => state - 1, }, 0) ``` ## Examples ### Store configuration ```js /* eslint-disable */ import { combineReducers } from 'redux' import { boostStore, createStore, dataReducer, requestsReducer, } from 'redux-boost' const myReducer = combineReducers({ data: dataReducer, requests: requestsReducer, }) const { store, persistor } = createStore({ initialState: {}, reducer: myReducer, reduxLogger: 'fallback', devtoolExtension: true, // --- Redux persist config reduxPersist: { key: 'root', whitelist: ['auth'], // storage, }, // --- Middlewares config middlewares: { saga: true, thunk: true, eventFilter: true, }, // middlewares: [ // ...customMiddlewaresArray, // ], }) // If store is not created by redux-boost // `boostStore` call is required to enable binded `fetchStart` action boostStore(store) ``` ### React integration `restQuery` — HOC for `GET` requests. `restMutation` – HOC for other types of requests. API is similar to [React-Apollo](https://github.com/apollographql/react-apollo) library. First wrap React component into HOC to receive desired functionality. ```js import { compose } from 'redux' import { connect } from 'react-redux' import { getResponse, restMutation, restQuery } from 'redux-boost' import exampleSaga from 'modules/sagas/exampleSaga' import ExampleComponent from './ExampleComponent' const mapStateToProps = state => ({ latestQuote: getResponse(state, { operation: 'operationName' }), }) export default compose( connect(mapStateToProps), // On componentDidMount will execute declared action which should // return promise to track request status and receive result later in props restQuery({ name: 'requiredData', action: fetchRequiredData, options: props => ({ userId: props.userId, }), }), // Provides props to make request, track it's status and receive result restMutation({ // { // name: 'exampleSaga', // action: exampleSagaAction, // } ...exampleSaga, // Will be passed to the executor as a request params options: ({ foo, bar }) => ({ foo: foo.toUpperCase(), bar, constant: 1, }), }) )(ExampleComponent) ``` Then use provided props to send mutation request and display queried data. ```js // In React component use mutation name to access passed properties import React, { Component } from 'react' class ExampleComponent extends Component { executeMutation = async () => { // Mutation result promise which resolves with result of the mutation // But anyway mutation result will be passed as a prop to the component too const result = await this.props.exampleSaga.mutate({ additionalProp: 'value', // Will be merged into the params defined in container }) console.log(result) } render() { const { requiredData, exampleSaga: { loading, mutate, result, error }, } = this.props if (requiredData.loading) { return <div>...loading</div> } return ( <div> <Data {...requiredData.result} /> <button onClick={this.executeMutation}>Mutate</button> {loading && '...mutation result is loading'} {result && <ResultComponent {...result} />} </div> ) } } ```