Skip to content
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

RFC: configureCallRSAA (Callable-RSAA utilities) #216

Open
darthrellimnad opened this issue Nov 15, 2018 · 2 comments
Open

RFC: configureCallRSAA (Callable-RSAA utilities) #216

darthrellimnad opened this issue Nov 15, 2018 · 2 comments

Comments

@darthrellimnad
Copy link
Contributor

darthrellimnad commented Nov 15, 2018

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?
@darthrellimnad
Copy link
Contributor Author

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

  • if we wanted to support async REQUEST actions for any configured middleware, i think the signature of fetchRSAA may need to change to RSAA => [Promise<FSA>, Promise<FSA>], instead of synchronously returning the value of the REQUEST FSA... I haven't had a use case for this personally, but might keep things a bit more flexible. However, requirement that configured middleware for configureCallRSAA only forward a single action to the next middleware (i.e. call next once and ignore additional calls) would still be necessary either way.
  • we could also split configureCallRSAA into 2 methods, configureFetchRSAA & configureFromRSAA if we think we'd rather avoid creating any utility function that we don't intend to use. with redux-observable, I usually only use fromRSAA, so maybe it would be best to allow separate configuration?
  • for the bailout option, either we could disallow this option when using the callRSAA methods (e.g. throw an error if bailout is/returns true). Otherwise, we could resolve/emit some type of "bailout" message, for the returned Promise/Observable... although this would add another difference to middleware spec for callRSAA methods, which I think I'd like to avoid if possible. Another option would be to do nothing, and allow the result Promise/Observable to never resolve/emit, and document that users should be careful when using bailout with callRSAA methods.

@darthrellimnad
Copy link
Contributor Author

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant