Recoil: Middleware equivalent?

Created on 2 Jun 2020  Â·  9Comments  Â·  Source: facebookexperimental/Recoil

Is there a way to have a functional equivalent to something like Redux's middleware? Something that lets me get between the request and the update?

question

All 9 comments

Have you tried using a read/write selector? It would essentially "wrap" the atom and you would then not call the atom directly where you want the middleware functionality.

I'm hoping for something I can do universally for all atoms, rather than on a per-atom basis. I guess I could have one generic wrapper that I used on everything, but I'd rather some way to attach to the RecoilRoot or similar.

For example, the classic crash reporter middleware from Redux:

const crashReporter = store => next => action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
    Raven.captureException(err, {
      extra: {
        action,
        state: store.getState()
      }
    })
    throw err
  }
}

useRecoilCallback can work with multiple atoms
https://recoiljs.org/docs/api-reference/core/useRecoilCallback

Yeah, I'm not sure how you would implement that. The only thing I think that would come close is something like making a selector that reads a recoil value that contains an object of recoil values. Then you would have selectors that each fetch a specific part of that object. Then the middleware would plugin at that core selector.

@adrianbw - For the crash reporter example, perhaps what you are looking for is something like the observer hook which lets you to globally subscribe to all state updates? That's currently unstable as we are working on the final form, but would that functionality meet your requirements?

I'm interested in this as well, and thought adding my use case may spur some new thinking on middleware or its equivalent.

I have an Atom which contains MyItem, an object with nested values. There are times when I need to modify the values in MyItem in an immutable way, for which I'm using a Recoil Selector, which all works fine. However, I run into a problem when for performance reasons I want to only make changes to MyItem when the resulting object would return false for a deep equals comparison. This is something I can accomplish inside the selector set function, but I need to do this in all my atoms, not just MyItem, so writing individual set functions for this violates DRY. I could import a separate function for this to minimize the inefficiency, but I still have to add boilerplate to every single selector.

First question: are the new Atom Effects capable of handling this?

Second: what other mechanisms are capable of acting as a middleware pattern? Has anything else been added or is in development now to handle this case?

You could do something like this:

const onBeforeGet: any[] = [];
const onAfterGet: any[] = [];
const onBeforeSet: any[] = [];
const onAfterSet: any[] = [];

export function atomWithMiddleware<T>(opts: AtomOptions<T>) {
  const _atom = atom<T>(opts);

  const wrapper = selector<T>({
    key: opts.key + '_middleware',
    get: ({ get }) => {
      for (const hook of onBeforeGet) {
        hook(opts.key);
      }

      const value = get(_atom);

      for (const hook of onAfterGet) {
        hook(opts.key);
      }

      return value;
    },
    set: ({ set }, newValue) => {
      for (const hook of onBeforeSet) {
        // allow hooks to override newValue
        newValue = hook(opts.key, newValue);
      }

      set(_atom, newValue);

      for (const hook of onAfterSet) {
        hook(opts.key, newValue);
      }
    }
  })

  return wrapper;
}

And of course add proper typings to the hooks, and maybe create event types for each.
Then you could expose an api for adding/removing the hooks.
Some of this could also be handled by effects, but to the best of my knowledge effects can intercept and change the value you're setting, they can only set a new value afterwards. I'm not sure if that is batched, but if it is, it's probably more efficient as effects.

EDIT:
For your use case of not setting the value if not deep-equal, you could create an onBeforeSetEvent which contains a stopSet function or variable that could be called/set which would then cause the wrapper.set to return early, prior to setting the value of the atom.

@BenjaBobs That's an interesting solution, and something which seems like a natural addition to the library. Let me play with this a little and I'll post back here what I come up with.

If anyone knows of any plans for this sort of thing in the backog, please let me know!

Just to add my 2c (slightly orthogonal to the OP's use-case): in Redux, middleware can be used to store long-lived, stateful instances of things like database connection managers. When the Redux store is bootstrapped, an instance of the database connection middleware is created and stateful logic such as establishing remote endpoint connections is executed. This middleware can then intercept local actions and interact with the remote database or receive changes from the remote database and dispatch actions to the store.

For similar behaviour in Recoil, you’d either have to define a database manager as a singleton and import it everywhere you need it, or initialize a database connection manager instance in a React useEffect then inject it as a parameter of atomFamily so that it can then be used by the atoms’ side effects. Neither of these choices seem ideal as singletons come with a whole host of problems and ideally React, as a view layer, should not concern itself with things like initializing database connections.

In the state synchronization example for instance, “myRemoteStorage” would need to be either a global singleton import or injected as an atomFamily parameter.

Unless I’m missing something, there seems to be no natural place in a Recoil app to put long-lived stateful instances of things that atoms depend on. Perhaps that's as designed since unlike Redux, Recoil is tightly coupled to React?

410 related

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ibnumusyaffa picture ibnumusyaffa  Â·  4Comments

julienJean99 picture julienJean99  Â·  3Comments

yuantongkang picture yuantongkang  Â·  3Comments

robsoncezario picture robsoncezario  Â·  3Comments

Sawtaytoes picture Sawtaytoes  Â·  4Comments