简体   繁体   中英

Unsubscribe from Redux store when condition is true?

I'm employing the suggestion from @gaearon to setup a listener on my redux store. I'm using this format:

function observeStore(store, select, onChange) {
  let currentState;

  if (!Function.prototype.isPrototypeOf(select)) {
    select = (state) => state;
  }

  function handleChange() {
    let nextState = select(store.getState());
    if (nextState !== currentState) {
      currentState = nextState;
      onChange(currentState);
    }
  }

  let unsubscribe = store.subscribe(handleChange);
  handleChange();
  return unsubscribe;
}

I'm using this in an onEnter handler for a react-router route:

Entity.onEnter = function makeFetchEntity(store) {
  return function fetchEntity(nextState, replace, callback) {
    const disposeRouteHandler = observeStore(store, null, (state) => {
      const conditions = [
        isLoaded(state.thing1),
        isLoaded(state.thing2),
        isLoaded(state.thing3),
      ];

      if (conditions.every((test) => !!test) {
        callback(); // allow react-router to complete routing
        // I'm done: how do I dispose the store subscription???
      }
    });

    store.dispatch(
      entities.getOrCreate({
        entitiesState: store.getState().entities,
        nextState,
      })
    );
  };
};

Basically this helps gate the progression of the router while actions are finishing dispatching (async).

My problem is that I can't figure out where to call disposeRouteHandler() . If I call it right after the definition, my onChange function never gets a chance to do it's thing, and I can't put it inside the onChange function because it's not defined yet.

Appears to me to be a chicken-egg problem. Would really appreciate any help/guidance/insight.

How about:

Entity.onEnter = function makeFetchEntity(store) {
  return function fetchEntity(nextState, replace, callback) {
    let shouldDispose = false;
    const disposeRouteHandler = observeStore(store, null, (state) => {
      const conditions = [
        isLoaded(state.thing1),
        isLoaded(state.thing2),
        isLoaded(state.thing3),
      ];

      if (conditions.every((test) => !!test) {
        callback(); // allow react-router to complete routing
        if (disposeRouteHandler) {
          disposeRouteHandler();
        } else {
          shouldDispose = true;
        }
      }
    });
    if (shouldDispose) {
      disposeRouteHandler();
    }

    store.dispatch(
      entities.getOrCreate({
        entitiesState: store.getState().entities,
        nextState,
      })
    );
  };
};

Even though using the observable pattern leads to some buy-in, you can work around any difficulties with normal js code. Alternatively you can modify your observable to suit your needs better. For instance:

function observeStore(store, select, onChange) {
  let currentState, unsubscribe;

  if (!Function.prototype.isPrototypeOf(select)) {
    select = (state) => state;
  }

  function handleChange() {
    let nextState = select(store.getState());
    if (nextState !== currentState) {
      currentState = nextState;
      onChange(currentState, unsubscribe);
    }
  }

  unsubscribe = store.subscribe(handleChange);
  handleChange();
  return unsubscribe;
}

and

Entity.onEnter = function makeFetchEntity(store) {
  return function fetchEntity(nextState, replace, callback) {
    const disposeRouteHandler = observeStore(store, null, (state, disposeRouteHandler) => {
      const conditions = [
        isLoaded(state.thing1),
        isLoaded(state.thing2),
        isLoaded(state.thing3),
      ];

      if (conditions.every((test) => !!test) {
        callback(); // allow react-router to complete routing
        disposeRouteHandler();
      }
    }

    store.dispatch(
      entities.getOrCreate({
        entitiesState: store.getState().entities,
        nextState,
      })
    );
  };
};

It does add a strange argument to onChange but it's just one of many ways to do it.

The core problem is that handleChange gets called synchronously immediately when nothing has changed yet and asynchronously later. It's known as Zalgo .

Inspired by the suggestion from @DDS , I came up with the following alteration to the other pattern mentioned in @gaearon's comment :

export function toObservable(store) {
  return {
    subscribe({ onNext }) {
      let dispose = this.dispose = store.subscribe(() => {
        onNext.bind(this)(store.getState())
      });

      onNext.bind(this)(store.getState());

      return { dispose };
    },

    dispose: function() {},
  }
}

This allows me to invoke like:

Entity.onEnter = function makeFetchEntity(store) {
  return function fetchEntity(nextState, replace, callback) {
    toObservable(store).subscribe({
      onNext: function onNext(state) {
        const conditions = [/* many conditions */];

        if (conditions.every((test) => !!test) {
          callback(); // allow react-router to complete routing
          this.dispose(); // remove the store subscription
        }
      },
    });

    store.dispatch(/* action */);
  };
};

The key difference is that I'm passing a regular function in for onNext so as not to interfere with my bind(this) in toObservable ; I couldn't figure out how to force the binding to use the context I wanted.

This solution avoids

add[ing] a strange argument to onChange

... and in my opinion also conveys a bit more intent: this.dispose() is called from within onNext , so it kinda reads like onNext.dispose() , which is exactly what I want to do.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM