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

Cross store actions #201

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft

Conversation

albertogasparin
Copy link
Collaborator

Expose a new API as part of StoreActionsApi called dispatchTo:

const myAction = () => ({ dispatchTo }) => {
  dispatchTo(OtherStore, otherActions.update())
}

Limitations

To reduce chances of unexpected behaviour due to stores container instances, it will only work in 2 cases:

  • on a container-less scenario (global -> global)
  • whenever containedBy value is the same on both dispatching store and targeted store

In any other case it will throw a runtime error: a bit harsh but otherwise a different store might be created/affected and that would make debugging the problem harder.

@albertogasparin
Copy link
Collaborator Author

@anacierdem What do you think?


const noop = () => () => {};

export default class Container extends Component {
Copy link
Collaborator Author

@albertogasparin albertogasparin May 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will happen in another PR, just wanted to have a cleaner file and care about only one implementation

@anacierdem
Copy link

I will have a look 👍🏼

@theKashey
Copy link

Being able to communicate with other stores is a very important moment 👍

I was always "OK" to do that outside of sweet-state to follow a sandwich pattern - store -> logic -> store, but I am never was "ok" to do it in React.

My concern here is a limitation of "global->global" or "contained -> contained"

What about onboarding some principles from "friend classes"(C++) or nestjs @module - basically an annotation that store X can communicate with store Y.
The details do not matter, it all about converting unexpected behaviour to agreed behaviour. What is more important - clearly distinguish such operations from the others. They better have a sandwich taste.

@anacierdem
Copy link

anacierdem commented May 22, 2023

Considering the use cases in #146, this change would only cover a very limited portion of it. Let me try to list the useful use-cases and then let's think about each of them based on the proposed API.

  • Using data from another store. Simplest example is keeping an activeId in another store and I want to fetch some data in current store.

The new addition does not help with this. Still I need to use container props.

  • Using actions from another store. As an example, I want to refresh some other store when something happens in current store.

This can be somewhat achieved if you move both stores under the same sharable container. That means the data boundary should be the same for both stores as an additional restriction. This may not always be meaningful. The ability to dispatch actions on other stores is definitely an improvement, but the limitations are a bit too strict.

  • Using state from external (non-RSS sources).

I still need to pass in the external stuff from container props at the place where I render the container. Note that this is an important constraint when deciding to use a global/local container and when deciding where to place the container. When combined with the global/containedBy restriction for the new API, there would still be cases where it is impossible to dispatch to another store because of the container formation.

This also generally requires keeping a copy of the state (#131) and this will still hold.

So overall I think the ability to dispatch actions on other stores is useful but will solve a very limited set of problems especially with the restriction imposed. Also I think we should keep the other requirements in mind when searching for a solution even though we don't need to solve all at once in a single PR.

P.S This also looks like an official solution to #139

@albertogasparin
Copy link
Collaborator Author

albertogasparin commented May 22, 2023

My concern here is a limitation of "global->global" or "contained -> contained"
That means the data boundary should be the same for both stores as an additional restriction. This may not always be meaningful. The ability to dispatch actions on other stores is definitely an improvement, but the limitations are a bit too strict.

I see the point but how would you protect from inadvertently getting data / triggering actions on the wrong store?
If we don't have the containedBy shared boundary, how do we protect from wrong effects? Should we not care then? Maybe I'm being too conservative...
Eg:

<App>
  <AllObjectsContainer>
    <AllObjects />
  </AllObjectsContainer>
  <SelectedObjectsContainer>
    <SelectedObjects>
  </SelectedObjectsContainer>

How do we tell that the developer intended to have AllObjects and SelectedObjects to talk? Especially if those containers are local (they don't even know that each other exist) or if multiple exist (we don't know which one should be targeted) 🤔
Don't get me wrong, technically it could work and RSS will select the right store instance closer to the consumer, but how do we tell it is the one they expect if the selection is implicit? This is the same as in the Wrapper example you wrote @anacierdem, useExternalStore expects the devs to have set the containers properly (is any).

Using data from another store

My answer for this was either push or pull. Means either AllObjects pushes updates to SelectedObjects, or SelectedObjects asks for data from AllObjects via getters. We could introduce also a listenTo API to let SelectedObjects to be notified on AllObjects automatically (easy enough) but you'll likely have to pick a "direction" regardless.

@anacierdem
Copy link

anacierdem commented May 22, 2023

how would you protect from inadvertently getting data / triggering actions on the wrong store

I think the container rules should apply, for the above example those stores should not be able to talk to each other. It does not make sense to dispatch an AllObjects action from within this tree:

<SelectedObjectsContainer>
    <SelectedObjects>
</SelectedObjectsContainer>

It wouldn't be much different than regular RSS usage. The same rules when enforcing a container if containedBy is provided (#200) will apply. You won't be able to dispatch to another store if that store declares it should be contained in a specific boundary, but you are not also in it as the dispatching store.

useExternalStore expects the devs to have set the containers properly

Yes, which is already the case in the current state. You forget a container, and you end up on a global store. I think this is still inline with how we think container boundaries are set-up per-store when declaring the store.

Now thinking about it more, having the new api in StoreActionsApi might still be a little to "free" and make relations between stores difficult to follow. Think about a generic action that dispatches to another store, it would almost be impossible to track in the code. So I agree, there should definitely be something to constrain it one way or another.

I think this would be a better restriction:

"whenever targeted store's containedBy value is satisfied by the dispatching store"

For example:

When they are contained in their own boundary, these cannot talk:

<AllObjectsContainer>
  <AllObjects />
</AllObjectsContainer>
<SelectedObjectsContainer>
  <SelectedObjects>
</SelectedObjectsContainer>

These can:

<SelectedObjectsContainer>
  <AllObjectsContainer>
    <AllObjects />
    <SelectedObjects>
  </AllObjectsContainer>
</SelectedObjectsContainer>

Following along;

  • if both are global, they can freely talk.
  • if there is a shared container, they can still talk as long as they are both in the container.
  • if one of the stores is global, but the other is contained;
    • When the global one tries to access the contained store, it will communicate through the container that also contains itself or fail if it is outside the target store's container.
    • If the contained is dispatching to global, that's fine.

I am pretty sure there are some edge cases, but should not be very difficult to list all possible cases.

Would this be a viable strategy?

So basically, you should be containedBy to be able to interact with a store whether it is accessing state via a hook or dispatching an action from another store.

@albertogasparin
Copy link
Collaborator Author

Ok, I have updated the implementation, making it work in a more general way. I'll rase a PR to refactor some of the internals before merging this, so it is easier to grasp.

"whenever targeted store's containedBy value is satisfied by the dispatching store"

In reality the limitation with the current reworked implementation is:
"whenever targeted store's containedBy value is satisfied by the dispatching consumer"

As either the validation is done at container level (old implementation) or at consumer/hook level (new one). I guess the hook level provides more flexibility and it is more similar to the hook composition pattern anyway.

Think about a generic action that dispatches to another store, it would almost be impossible to track in the code. So I agree, there should definitely be something to constrain it one way or another.

Cannot think of any better API. We could enforce a whitelist on store creation, by requiring a linkedStores/interactsWith/... property so it is more controlled in the wild:

const MainStore = createStore({
  // ...
  linkedTo: [OtherStore],
});

And so if some action calls dispatchTo we can validate that the store has been configured accordingly and complain if not enabled to do so. It would be a double validation (both MainStore and MainAction will need explicit reference to OtherStore) but that's the most effective way that also maintains type safety.

@anacierdem
Copy link

whenever targeted store's containedBy value is satisfied by the dispatching consumer

💯

With the boundary validation (containers throwing when an invalid consumer tries to access a store), I think accesses are pretty safe, but if we really want to we can introduce the linkedTo concept as well. I believe it is very similar to what @theKashey suggested with the "friend" concept.

I haven't looked at the implementation yet. I will do that soon as well. Thank you for the immense effort you are putting into this @albertogasparin.

@theKashey
Copy link

so let's talk in abstractions.

As I understand two containers are allowed to communicate if the coexists at the whole continuity of their existence 🤓

  • global cannot talk to local, because it exists and may be used "before" local
  • local can talk to global, because global is everywhere where local is
  • If stores have the same containerBy - then they exists in the same space because they have the same root
  • the problem is one container store inside another contained store - while the inner one is "able to" communicate with the parent, the current logic will prohibits it.

So two questions to answer:

  • what mean global store? It should not matter, it's any store defined before me.
  • "friend"? Can be used to support the case above, but friends has to obey the rules above.

So let's try to derive simple rule:

  • One store can talk to another store if
    • another store has boundary before first one (including not having boundary at all)
    • another store has no boundary after first one (fractal stores are rare but real)

@anacierdem
Copy link

anacierdem commented May 24, 2023

When I started thinking about this, that was exactly what I thought @theKashey but as I think about it more, the order of containers started to look like it should not be important. I remember rendering the same container with the same scope twice at two positions to solve this issue before (item 2 in #146). With the new shared container, this is probably not an issue but still someone may want to keep separate containers while communicating between stores. I think that would be an artificial limitation (unless there is some implementation detail that prevents this for some reason).

So in the light of this position;

global cannot talk to local, because it exists and may be used "before" local

In this case (where store A is contained by StoreAContainer),

<StoreAContainer>
  <ChildComponent/>
</StoreAContainer>

The global store's actions should be able to talk with the local store A instance if they are used inside ChildComponent. If they are outside of a StoreAContainer, it will throw an error as the usage of store A is not correctly contained.

the problem is one container store inside another contained store - while the inner one is "able to" communicate with the parent, the current logic will prohibits it.

In the following scenario;

<StoreAContainer>
  <StoreBContainer>
    <ChildComponent/>
  </StoreBContainer>
</StoreAContainer>

Store B's action in the child component can definitely access store A. Similarly, store A's action should be able to access store B if dispatched in the child component as target store's (B) container is fulfilled.

One store can talk to another store if

  • another store has boundary before first one (including not having boundary at all)

I am not exactly sure why the order of the boundaries should matter? As long as the dispatching location fulfills the container requirement, it should be fine. If I can use both A & Bs hooks in the above child component, there is no reason why those two stores cannot talk to each other. So effectively, this is equivalent from child component PoV:

<StoreBContainer>
  <StoreAContainer>
    <ChildComponent/>
  </StoreAContainer>
</StoreBContainer>

So if we are introducing this as a native API, I think we should make it as smooth as possible. OTOH, let me know if this has a very bad unwanted consequence that I cannot see.

@anacierdem
Copy link

anacierdem commented May 24, 2023

@albertogasparin back to the data sharing:

listenTo would be a pretty good solution to sync two "friend" stores.

That wouldn't need to keep an additional state in the listening store either as it is directly bound to another RSS state already. When it is through the container props OTOH, there is no guarantee around this and it could change irrespectively via other mechanisms, thus you'd need to keep a "previous" state to re-initiate an action on the listening store when the prop changes.

When listening to another store OTOH, the only way that state changes unexpectedly (at least according to my current definition) is changing the container structure that is affecting the listening consumer. In that case, if the underlying mechanism re-calls the listener with the new container's value (as the old value is not valid anymore), there should still no need to keep an additional state.

We can even instead do the same-value optimization and don't call the listener in the library.

@theKashey
Copy link

I am a little worried about allowing more than we should. Unidirectional data flow got adopted for a good reason - less 🎉 SURPRICES 😀 at work.

So let's put aside "why" one need to dispatch actions from one store to another and focus on another important aspect - observability.

Ie how to keep things as they are today:

  • detangled (small separate stores)
  • isolated (containers)
  • observable (simple things don't need extra observability)

In a little more abstract theory, two entities communicate via explicit "channels", aka wires between two stores. In/Out channels are more usually just separate API into a few different buckets facing different "types" of consumers.

In port and adapters (and XState) that "wire" is a separate entity creating a connection between two entities not aware of each other.

As example we can use today:

  • get actions from one store
  • put them as props to container of another store
  • another store can access them and do what is required
  • another store can trigger an externalAction injecting "own actions" in order to establish two way comminication
  • the only piece connection those elements is "mediator"
const someStateActions = useSomeStateActions();
return <AnotherStateContainer externalActions={someStateActions}>...
  • and this construction can be simplified via listenTo or analogs

The essence of everything above - how to create explicit hardwiring between stores, like these are stores I need to talk(read data from or use in other way, they should be defined "among me"), and I am not going to use any other store.

In other words - let's stick to the Tangerine goals and keep drawing artificial boundaries.

@anacierdem
Copy link

First of all even with the boundary order restriction, I think enabling communication would be a great addition. Also I totally agree on the fact that the API should be designed such that the data flow is clearly and easily visible with a very explicit definition to prevent any surprises. Now let me think out loud:

Today we lean on the data flow model of React by delegating the communication to jsx. This is not necessarily a bad thing, it is well known and predictable. OTOH, we all agree that jsx/React world is not the best place to do this communication and we are looking into ways in the RSS API itself. This means we are not subject to the unidirectional flow in the React tree. This does not mean we shouldn't have a regulated data flow though.

With RSS, it is already possible to communicate between React tree nodes irrespective of the unidirectional data flow. Global stores can talk to the same state container irrespective of where they are in the tree, similarly scoped containers allow such a mechanism as well. I don't think there is a reason to prevent a similar communication channel between two different stores as long as we can find a way to declare this. Essentially we are creating independent stores to encapsulate their state but at the same time we want them to work together as if they are the parts of a bigger machine.

If we are strict on the boundary order, there are a few ways how an A action can dispatch some action in B when the natural container order is in reverse;

Via an artificial scope

This works in practice, I don't exactly remember if there were any side-effects. But clearly this is not something you would want. This fact forces you to reverse the container order just to make this more comfortable.

<StoreAContainer scope='scope-to-inject-B-actions'>
  <OtherthingsUsingStoreA/>

  <StoreBContainer>
    <StoreBConsumer>{
      (_, {actionInB}) => {
        <StoreAContainer scope='scope-to-inject-B-actions' externalAction={actionInB}>
          <ChildComponent/>
        </StoreAContainer>
      }
    }</StoreBConsumer>
  </StoreBContainer>
</StoreAContainer>

Through jsx

This means the relation between the action in store A and B is lost in a huge pile of jsx unless you know where to look at. This is a good example how you can loose observability in your analogy.

<StoreAContainer>
  <OtherthingsUsingStoreA/>

  <StoreBContainer>
    <StoreBConsumer>{
      (_, {actionInB}) => {
        <ChildComponent onActionAComplete={actionInB()}/>
      }
    }</StoreBConsumer>
  </StoreBContainer>
</StoreAContainer>

With a ref

I actually never tried this method. It will highly likely work as expected but it is not very pretty. It is highly likely that you'll need to drill down the ref when StoreBContainer is deeper in the React tree.

const actionInB = useRef();
<StoreAContainer externalAction={actionInB.current}>
  <OtherthingsUsingStoreA/>

  <StoreBContainer>
    <StoreBConsumer>{
      (_, {actionInB}) => {
        actionInB.current = actionInB
      }
    }</StoreBConsumer>
  </StoreBContainer>
</StoreAContainer>

This is why I think we should at least consider relaxing the constraint. Now how should we declare the wiring between them is another topic. Let me brainstorm assuming we allow two way communication given relaxed boundaries;

// Here store A is clearly importing from another module and the relation is clear
import StoreB, { actions as actionsB } from '../store-b';
const actions = {
  actionA: () => ({dispatchTo}) => { dispatchTo(StoreB, actionsB.do()); }
};
const storeA = createStore({
  initialState,
  actions,
  containedBy: containerA
})

So in general one can analyze who is using a store's actions (thus depending on it) irrespective of the React tree shape and this looks pretty much clear to me at this point. In this example, StoreA depends on StoreB, meaning any use of it should also be contained by an allowed container of B. So in reality it is not the React tree that defines the relation but the store definition itself. React tree is a constraint that follows the store implementation.

Another potential edge case is A importing B and B importing A causing a circular dependency but this is not something we can solve with React tree restrictions or RSS API anyways. It is a programming reality 😄

@theKashey
Copy link

So let's pick the first thing to agree on:

  • one store can dispatch a particular action to another particular store. This makes storeA aware of storeB and "naturally" prevent cyclic dependencies (on module level)
  • one store can dispatch a particular action and a Middleware can wire it to another store. Stores are entangled. Connections are explicit and have a "connection point"

Reatom for example solves this from an interesting point of view - action has access to all stores, but does not belong to any particular one. Another example of "middleware" using API equal to the one @albertogasparin proposed in this PR.

export const onSubmit = action((ctx) => {
  const a = ctx.get(atomA);
  const b = ctx.get(atomB);
  ctx.set(atomC, a + b);
  // dispatchTo(storeC, otherActions.update())
}, 'onSubmit')

So what about adding unstable_ prefix to dispatchTo and evaluating solution before making any decision? It does not have to be all or nothing, we need to experiment and feel the impact first.

@anacierdem
Copy link

I call these the "units" of a state management system:

  • A hook in case of pure React (even though would be very verbose on its own),
  • A saga for redux-saga,
  • An epic for redux-observable,
  • TIL, an action for Reatom.

All these can freely import and use other parts of the state tree and communicate via direct dependencies and this enables something very important IMO. You can design "flow"s that describe business logic that span multiple concerns.
Similarly the unit of RSS is the action definition. It can already dispatch other actions but currently restricted to the same store. Once we remove this constraint, it will become a very good tool to design UI flows.

I am totally onboard with an unstable_ prefix. No better way to battle test something before we decide.

@albertogasparin
Copy link
Collaborator Author

Another potential edge case is A importing B and B importing A causing a circular dependency but this is not something we can solve with React tree restrictions or RSS API anyways. It is a programming reality 😄

That is why I was suggesting something like a linkedTo: given it is a property that would need to be defined on both stores at initialisation time, we can check that both stores are not linked to each other and throw an error. However, if someone wants to create a circle between stores/data flow, should RSS API forbid that? Other libs do not forbid, just recommend.

So what about adding unstable_ prefix to dispatchTo and evaluating solution before making any decision?

Happy to, I think the dispatchTo API is clear enough.
What I'm not yet sure is how/if do we enable a Store to subscribe to another one. Going back to AllObjects/SelectedObjects example, we might want SelectedObjects to do something when AllObjects changes, and still be able to affect/update it.
Doing a getter in an action is easy:

const actions = {
  actionA: () => ({dispatchTo}) => { 
    const allData = dispatchTo(AllObjects, actionsAll.getData()); 
    ...
 }
};

but having AllObjects pushing data to SelectedObjects is complicated:

  1. what should be the "subscription" boundary? The container? The hook? Or the store itself?
  2. how do we pass that data to selectors?
  3. how do we handle change in subscription (un/re-subscribe)?

Having hard time to map to an API as while for an action we have a clear origin (either consumer hook or container), the subscription seems a bit more iffy.
My initial idea was:

const SelectedObjects = createStore({
  initialState,
  actions,
  handlers: {
    onInit: () => ({ listenTo, setState }) => {
      listenTo(AllObjects, (newState) => {
        // how do we make newState available? If we have to put it in state, is it really an improvement?
        setState({ allData: newState });
      });
    }
})

But that will not make "compound" data handling much easier, as SelectedObjects might need to store AllObjects state in other places like selectors. At that point container props behaviour is a bit nicer as at least they will be available in all actions by default 🤔

@theKashey
Copy link

You've opened a pandora box, basically You Might Not Need an Effect, or actors vs observers

  • should one store listen to another and react on changes (useEffect)
  • should stores communicate with events, and that's it (useCallback)

The first one is easy to implement, easy to use, easy everything including easy to make a mess.
The second one is easy to debug and maintain, even (and especially) if it requires some "API design" first.

There is a long history between each concept with wars, wins and losses. There are good use cases for each one, and maybe one day we will have them both.
But I truly believe - for the first iteration it should be "actors". They just guarantee a little more predictable tomorrow.

@anacierdem
Copy link

what should be the "subscription" boundary? The container? The hook? Or the store itself?

I think it should be the container. While thinking about dispatchTo above, I thought that listenTo would be subject to the same constraints. That would be less things to learn for the user and consistent with RSS in general.

how do we pass that data to selectors?

From the PoV of the "provider" store, selectors would behave exactly the same. They only trigger the consuming store if the output of the selector is not shallow equal. That would mean we need to find an API to reference the selector in the consumer, that's true. In #146, we use plain react hooks from createHook.

how do we handle change in subscription (un/re-subscribe)?

Is there really a need to unsubscribe? I never had that requirement, I think we can simply postpone this extension.

how do we make newState available? If we have to put it in state, is it really an improvement?

Not much of an improvement, I'd agree. That's why we have to use a Wrapper around to inject them as container props in #146. OTOH, most of the time you just want to start an action based on the listened value rather than persist it (other than for comparison purposes). It is already persisted on the other store and I can have a getter action if I really need to access the value. This is a more practical use-case with a newly made-up selector API:

const SelectedObjects = createStore({
  initialState,
  actions,
  handlers: {
    onInit: () => ({ listenTo, dispatchTo }) => {
      listenTo({
        store: AllObjects,
        selector: allObjectsSelectors.idSelector,
      }, (newState) => {
        // In general, we want to "react" to the changes in the other store. If listenTo only triggers when
        // the output of the selector changes, we don't have to keep it in our state, we can just use it in
        // the action as a bound parameter on the stack.
        actions.updateSelected(newState);

        // If I really want to access a value from the other store:
        const data = await dispatchTo(AllObjects, allObjectsActions.getData());
      });
    }
})

should one store listen to another and react on changes (useEffect)

That's exactly what we do in #146 and in the above example as well. Whether it is safe or not is another question, that's true, but the only alternative right now is already using an effect in React world with the existing API.

should stores communicate with events, and that's it (useCallback)

I am not sure I get this one @theKashey, can you provide an example?

But I truly believe - for the first iteration it should be "actors". They just guarantee a little more predictable tomorrow.

This, I agree but then this is only a partial solution to the store communication problem.

If this is something we should not rush, what do you think of the Wrapper API proposed in #146? With dispatchTo, we provide an alternative to "push" style communication and still, Wrapper is also useful for easily injecting outside (non RSS as well) state into all instances of a container.

With the new setup though, I think createContainer would not be the correct place anymore as we want to keep it lightweight. createStore does not fit the semantics OTOH.

@anacierdem
Copy link

anacierdem commented May 31, 2023

should stores communicate with events, and that's it (useCallback)

Hmm, I think it would look something like this if I'm getting it right;

const SelectedObjects = createStore({
  initialState,
  actions,
  handlers: {
    onInit: () => ({ dispatchTo }) => {
      // listenToX simply sets a callback to AllObjects' state that it will call it when it sees necessary
      dispatchTo(AllObjects, allObjectsActions.listenToX((newState) => {
        actions.updateSelected(newState);
      }))
    }
})

I think we already have most of listenTo with dispatchTo, you're right. We can definitely build upon this after we stabilize it.

@theKashey
Copy link

Let's look on the problem from coupling point of view

  • listenTo + selector: I want to observe a piece of another store and I will decide on which changes I want to react
const SelectedObjects = createStore({
  initialState,
  actions,
  handlers: {
    onInit: () => ({ listenTo }) => {
      listenTo({
        store: AllObjects,
        selector: myAllObjectsSelectors.idSelector,
      }, (newState) => {
        actions.updateSelected(newState);
      });
    }
})
  • listenTo + event: I want to be notified on change in store and They will decide on which changes I will react
const AllObjects = createStore({
  initialState,
  actions: {
    //...
    someAction: ({setState, emit}) => {
       setState({thatThing:1});
       emit('to-whom-it-may-concern');
    }
  }
  handlers: {
    onChange: (diff, {emit}) => {
      if(someSelector(diff.before, diff.after)) {
        emit('some-change', diff.after);
      }
    }
})

const SelectedObjects = createStore({
  initialState,
  actions,
  handlers: {
    onInit: () => ({ on }) => {
      on({
        store: AllObjects,
        event: 'some-change',
      }, (newState) => {
        actions.updateSelected(newState);
      });
    }
})

I really don't like the second example, even if the majority of event-driven system looks like it.

Looking at the code samples I could say:

  • as we use actions to let store decide how and then it can be updated...
  • use listenTo+selector to let store decide how and then it should be updated. ✅ @anacierdem
  • use dispatchTo to communicate from one store to another store. ✅ @albertogasparin. May be expose a different set of actions to be used by other stores, or "agree" on naming patterns to distinguish such actions
    • there is no clear answer to "why" one should want such separation.
  • 🤔 do we want to let accessing getState of another store, or everything should go via listenTo, so "subscription" will be tracked and kept in sync automatically?

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

Successfully merging this pull request may close these issues.

3 participants