agraboso / redux-api-middleware

Redux middleware for calling an API.
MIT License
1.49k stars 195 forks source link

RFC: `configureCallRSAA` (Callable-RSAA utilities) #216

Open darthrellimnad opened 5 years ago

darthrellimnad commented 5 years ago

I've been using redux-api-middleware for a bit, and ended up writing a few utilities that I've reused a few times across projects. Lately however, I was thinking about how I could remove the redux-api-middleware peerDependency in my utility package to focus it's concerns a bit, but figured I'd first see if any of these seem like a good addition here, before I try to find another way to refactor my projects anyhow. If there's any interest, I'll dig through my bag and see if there's anything else I can port over that that might be helpful or worth discussion :)

Background:

The first util I'm thinking about porting over is a configureCallRSAA util, that's used to create fetchRSAA and fromRSAA methods that will immediately invoke the fetch for any valid RSAA (using apiMiddleware implementation internally) and either return a [Promise, FSA] array tuple (fetchRSAA) or an Observable that emits FSAs (fromRSAA). See JSDoc below fold for more info:

I originally had a use-case for this when using redux-api-middleware alongside redux-observable. Most often when using redux-api-middleware, I'll dispatch RSAAs normally to the redux store, allow apiMiddleware to process it, and respond to the REQUEST, SUCCESS, and FAILURE actions in reducers or epics.

However, because of redux-observable issue #314, We can't really "dispatch" sequenced RSAAs from a single Epic anymore, only emit them in the result, which meant I usually needed multiple epics or observables to dispatch an API request, forward it to the store to be processed by apiMiddleware, and then respond to the result FSA in the epic... which felt like a bit too much indirection for something that I really just needed fetch and from for if I avoided my preexisting RSAA action creators we've made for our API.

I didn't really want to abandon our RSAAs in these situations if I could help it... they are a great way to organize RPC/REST/Legacy api methods in the client, and we still often use them normally by dispatching them directly to the redux store. Doing so would also mean that I would need to rework any middleware we use that operate on RSAAs or response FSAs (like our "authMiddleware" that applies auth token to headers by reading token from store).

After I tried out various iterations of configureCallRSAA to solve the problem, and it became more useful, I found this also allowed me to clean up some "noise" in the action log and reducer code for a few API-request-heavy features and avoid store updates until the entire sequence (or parts of the sequence) was complete. As I tried to generalize the solution, I also realized this would allow you to use redux-api-middleware outside of a redux-store context entirely, if desired, and still offer the pre/post-fetch hooks w/ middleware for interceptor-like functionality (similar to angular, axios, etc).

This util might be a good fit for redux-api-middleware lib, since it would offer users a stable, built-in utility that allows projects using redux & redux-api-middleware to reuse the existing RSAA spec (and existing action creators or middlewares that leverage it) for situations where direct fetch may be desired.

Proposal

Here is the current JSDoc I have for this util that describes this in more detail:

/**
 * Configure methods for directly fetching via RSAA, without relying on a redux store's
 * configured apiMiddleware.
 *
 * Generally used for custom use-cases, where you'd like more control over async
 * action handling for one or more API requests within a context _other_ than the
 * redux-api-middleware instance configured with the redux store (e.g. redux-observable,
 * or redux-saga). Can be used outside of a redux context entirely if necessary.
 *
 * ```
 * type StoreInterface = { getState: Function, dispatch?: Function }
 * type CallRsaaApi = {|
 *   fetchRSAA: (rsaa: RSAA, store?: StoreInterface) => [Promise, FSA],
 *   fromRSAA: (rsaa: RSAA, store?: StoreInterface) => Observable
 * |}
 * ```
 *
 * Example Configuration:
 * ```
 * // use a tc39 compliant Observable implementation (until natively supported) like RxJS
 * import { Observable } from 'rxjs'
 *
 * // Some app middlewares we'd like to use for RSAAs.
 * // These would likely be ones that target the RSAA action type (i.e. isRSAA()).
 * const rsaaMiddleware = [
 *   authMiddleware,
 *   rsaaMetaMiddleware,
 * ]
 *
 * // Middleware that will target the FSAs produced by redux-api-middleware
 * const fsaMiddleware = [
 *   apiRetryMiddleware
 * ]
 *
 * // configure your store... use the same middleware arrays as above if you'd like :)
 * const store = configureStore( ... )
 *
 * // Then create your callRSAA methods using your desired middleware
 * export const { fromRSAA, fetchRSAA } = configureCallRSAA({
 *   Observable,
 *   rsaaMiddleware,
 *   fsaMiddleware,
 *   store
 * })
 * ```
 *
 * Example Use (w/ redux-observable):
 * ```
 * // Returns an array whose first value is a Promise for the `fetch` request, and
 * // whose second value is the "request" FSA.  Promise will resolve the async result
 * // FSA.  If you'd like to dispatch the "request" action before handling the
 * // resolved value, you must do so manually.
 * const testFetchRSAA = (action$, state$) =>
 *   action$.pipe(
 *     ofType('TEST_FETCH_RSAA'),
 *     switchMap(() => {
 *       const rsaa = rsaaCreator({ foo: 'bar' })
 *       const [ promise, request ] = fetchRSAA(rsaa)
 *       return from(promise).pipe(startWith(request))
 *     })
 *   )
 *
 * // Returns an Observable which will emit the "request" and "success|failure" FSAs to
 * // any subscriptions.  Useful for utilizing rxjs operators that leverage higher order
 * // observables, like switchMap.
 * const testFromRSAA = (action$, state$) =>
 *   action$.pipe(
 *     ofType('TEST_FETCH_RSAA'),
 *     switchMap(() => {
 *       const rsaa = rsaaCreator({ foo: 'bar' })
 *       return fromRSAA(rsaa)
 *     })
 *   )
 * ```
 *
 * @alias module:api
 * @param {Observable} Observable tc39 compliant Observable class to use for `fromRSAA`
 * @param {function} [apiMiddleware] override the redux-api-middleware with different implementation (useful for mocks/tests, generally not in production!)
 * @param {function[]} [fsaMiddleware] list of "redux" middleware functions to use for the RSAA's resulting FSAs
 * @param {function} [fsaTransform] custom "transform" to apply to resulting FSAs from called RSAA
 * @param {function[]} [rsaaMiddleware] list of "redux" middleware functions process incoming RSAA
 * @param {function} [rsaaTransform] custom "transform" to apply to incoming RSAA
 * @param {{}} [store] a redux store interface. leave `dispatch` method undefined if you wish to avoid dispatching action side-effects to store from configured middleware (recommended).
 * @return {CALL_RSAA_API} the 2 "Call RSAA" API methods
 */

Caveats

There were a couple of design decisions made to simplify the implementation and focus it's utility on most common/likely use case:

darthrellimnad commented 5 years ago

thought a little more on a few of the above points:

darthrellimnad commented 5 years ago

Lately I've also been considering removing the fetchRSAA requirement from this util, and only making a configureFromRSAA util that returns an Observable. This has the benefit of allowing me to be less restrictive about the types of middlewares I can configure for it, since the Observable can emit more than 1 action, unlike fetchRSAA, which was designed to return Promises.

This would simplify things quite a bit actually, and slim down the code... but wouldn't offer the "Promise" based utility if anyone thinks that would still be helpful (but we could later add a configureFetchRSAA util if desired).