Skip to content

RFC: configureCallRSAA (Callable-RSAA utilities) #216

@darthrellimnad

Description

@darthrellimnad

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:

  • while configureCallRSAA can accept rsaaMiddleware and fsaMiddleware configurations, these middlewares are not treated identically as a redux middleware configured with the redux Store. Since we're operating outside of the store's middleware context, and only interested in the request/response behavior of the individual fetch request handled by apiMiddleware, I limit the # of possible middlewares to those that are synchronous, and only emit a single action to the next middleware (i.e. next called only once). Trying to mimic a redux store's middleware chain point-for-point would be a bit more involved since any potential side-effect or async processing would be allowed, meaning this util would no longer be the fetch wrapper I needed. And if you really needed the identical behavior of a redux store here, I'd recommend using the actual redux store and not using configureCallRSAA :). That said, I probably still need to improve documentation and error handling around this. May also be worth more discussion, or potentially expanding the # of allowed middlewares with future updates (e.g. allow next to be called once async as well?).
  • I think (if merged into redux-api-middleware) that it might be worth a disclaimer that this isn't intended be an app-wide replacement for redux-api-middleware apps that use something like redux-observable... using a redux store's configured apiMiddleware and dispatching the RSAAs individually is still my preferred way to do many things with a REST api and client cache :). So probably some documentation around intended use might be good, and a point about other techniques you could use before reaching for fetchRSAA or fromRSAA.
  • when using configureCallRSAA, I usually provide a store value that only provides an interface to the redux store's getState method, and leave dispatch undefined (generally making any rsaaMiddleware and fsaMiddleware read-only by avoiding property mutations). However, I couldn't really decide if there might be situations where you want a fetchRSAA or fromRSAA method to invoke a dispatch to the real redux store from a configured middleware... 🤔 . So this is allowed, but in practice I usually avoid it unless I'm refactoring an older middleware function that may have used dispatch directly.
  • any new utils may add to package size of redux-api-middleware lib, which is not a huge concern for many apps built using unused code removal and tree shaking, but could be an issue for some.
  • i don't think I ever really figured out a behavior to use when bailout is true w/ the callRSAA methods 🤔. In practice, I don't ever use it with RSAAs invoked by fromRSAA or fetchRSAA but might be worth discussion?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions