Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Alternative to direct calling of store.dispatch()

Since in the latest redux-observable (0.17) is direct calling of store.dispatch() deprecated, I wonder what is an alternative if I need to dispatch actions from the outside of my redux app.

Example:

Let's say I have this function which initialize native module and set up native handler.

const configure = (dispatch) => {
    const printingModule = NativeModules.PrintingManager
    const eventEmitter = new NativeEventEmitter(printingModule)

    eventEmitter.addListener(
        "PrintingManagerNewPrinterConnected",
        (payload) => dispatch({
            type: PRINTER_MANAGER_NEW_PRINTER_CONNECTED,
            payload: {
                macAddress: payload[2],
                connectionType: payload[3],
            },
        }))

    printingModule.initialize()
}

What I typically do is that I will call this function from observable after something like APP_STARTUP_FINISHED:

const appStatePrepared = (action$: Object, { dispatch }) =>
    action$.ofType(APP_STATE_PREPARED)
        .switchMap(() => {
            configurePrinters(dispatch)
        })

What is the correct solution for this?

Thanks!

like image 403
trubi Avatar asked Dec 06 '17 01:12

trubi


Video Answer


1 Answers

When using RxJS the ideal is to compose streams. So in this case we need to some how create a stream of "PrintingManagerNewPrinterConnected" events that we can then map each to their own PRINTER_MANAGER_NEW_PRINTER_CONNECTED action.a

Let's first learn how to do this completely custom.

Custom Observables

Creating your own custom Observables is very similar to creating a Promise. So say you had the most simple Promise in the world that just immediately resolves to the number 1

const items = new Promise(resolve => {
  resolve(1);
});

The equivalent Observable looks super similar

const items = new Observable(observer => {
  observer.next(1);
  observer.complete();
});

Visually, the main differences are that instead of being passed (resolve, reject) callbacks we're given an Observer because there is next, error, and complete.

Semantically, Observables can represent more than one value by calling observer.next as many times as they'd like until they call observer.complete() to signal the end of the stream; this is in contrast to Promises, which only represent a single value.

Observables are also lazy and synchronous by default, whereas Promises are always eager and async.

Now that we have that understanding we want to take that and wrap your NativeEventEmitter API which uses addEventListener.

const configurePrinters = () => {
  return new Observable(observer => {
    const printingModule = NativeModules.PrintingManager;
    const eventEmitter = new NativeEventEmitter(printingModule);

    eventEmitter.addListener(
      'PrintingManagerNewPrinterConnected',
      (payload) => observer.next(payload)
    );

    printingModule.initialize();
  });
};

configurePrinters()
  .subscribe(payload => console.log(payload));

This works and is super simple, but there's one problem with it: we should call removeListener when they unsubscribe so that we clean up after ourselves and don't leak memory.

To do that, we need to return a subscription inside our custom Observable. A subscription in this context is an object that has an unsubscribe() method on it, which will be called automatically when the subscriber unsubscribes, an error is triggered, or your observable completes. This is your chance to clean up.

const items = new Observable(observer => {
  let i = 0;
  const timer = setInterval(() => {
    observer.next(i++);
  }, 1000);

  // return a subscription that has our timer cleanup logic
  return {
    unsubscribe: () => {
      clearInterval(timer);
    }
  };
});

Because returning an object is a bit verbose RxJS supports a shorthand where you just return a function which itself will be treated as the unsubscribe method.

const items = new Observable(observer => {
  let i = 0;
  const timer = setInterval(() => {
    observer.next(i++);
  }, 1000);

  // return an "unsubscribe" function that has our timer cleanup logic
  return () => {
    clearInterval(timer);
  };
});

Now we can apply this to our example, where we want to remove our listener when our unsubscribe teardown function is called.

const configurePrinters = () => {
  return new Observable(observer => {
    const printingModule = NativeModules.PrintingManager;
    const eventEmitter = new NativeEventEmitter(printingModule);

    const listener = (payload) => observer.next(payload);

    eventEmitter.addListener(
      'PrintingManagerNewPrinterConnected',
      listener
    );

    printingModule.initialize();

    return () => eventEmitter.removeListener(
      'PrintingManagerNewPrinterConnected',
      listener
    );
  });
};

Now let's turn this into a reusable utility function

const fromPrinterEvent = (eventName) => {
  return new Observable(observer => {
    const printingModule = NativeModules.PrintingManager;
    const eventEmitter = new NativeEventEmitter(printingModule);

    const listener = (payload) => observer.next(payload);
    eventEmitter.addListener(eventName, listener);
    printingModule.initialize();

    return () => eventEmitter.removeListener(eventName, listener);
  });
};

fromPrinterEvent('PrintingManagerNewPrinterConnected')
  .subscribe(payload => console.log(payload));

Observable.fromEvent

While NativeEventEmitter is a react-native thing, it follows the node-style EventEmitter interface and RxJS already comes with a utility helper to create an Observable from them to save you the effort. It's called fromEvent, found at Observable.fromEvent or import { fromEvent } from 'rxjs/observables/fromEvent'.

const fromPrinterEvent = (eventName) => {
  return Observable.defer(() => {
    const printingModule = NativeModules.PrintingManager;
    const eventEmitter = new NativeEventEmitter(printingModule);
    printingModule.initialize();

    return Observable.fromEvent(eventEmitter, eventName);
  });
};

Here I also wrapped it in Observable.defer so that we don't create the NativeEventEmitter or printingModule.initialize() until someone actually subscribes (maintain laziness). This may or may not be neccesary for you, I don't know what PrintingManager does or how it behaves. e.g. it might be desirable to only create a single emitter and to initialize the module upfront.

const printingModule = NativeModules.PrintingManager;
const printerEmitter = new NativeEventEmitter(printingModule);
printingModule.initialize();

const fromPrinterEvent = (eventName) =>
  Observable.fromEvent(printerEmitter, eventName);

So keep in mind that I'm just showing patterns without knowing what PrintingManager, etc does.

Use it within redux-observable

To use this within redux-observable and your epic is now the same as you'd use any other Observable. So we'll want to map the values from it to actions and mergeMap, switchMap, concatMap, or exhaustMap that into our top-level stream.

Something like this:

const appStatePrepared = action$ =>
  action$.ofType(APP_STATE_PREPARED)
      .switchMap(() =>
        fromPrinterEvent('PrintingManagerNewPrinterConnected')
          .map(payload => ({
            type: PRINTER_MANAGER_NEW_PRINTER_CONNECTED,
            payload: {
              macAddress: payload[2],
              connectionType: payload[3],
            }
          }))
      );

Remember that many streams, including our custom fromPrinterEvent('PrintingManagerNewPrinterConnected'), go forever until you unsubscribe from them. So if you only want one you'd use .take(1). If you want to unsubscribe when you receive another action you'd use .takeUntil(action$.ofType(WHATEVER)), etc. Normal RxJS patterns.

like image 94
jayphelps Avatar answered Nov 14 '22 17:11

jayphelps