Skip to content

Get rid of global objects and side effects #181

@smikula

Description

@smikula

This is a proposal for a breaking change to Satchel — this would be Satchel v5. The goal here is to get rid of the two great evils in the existing implementation: global objects and side effects.

Motivation

Today Satchel's internal state — including stores, subscriptions, middleware, etc. — is stored in a global object hanging off the window. Mutators and orchestrators are registered by applying the mutator and orchestrator "decorators" to the handlers. (These APIs aren't really decorators — typically a decorator would just wrap the function in a higher order function — because they aren't wrapping the function but rather registering the function as a callback in the Satchel dispatcher.) This makes for very convenient coding patterns, but it presents some problems:

  • If there are multiple instances of Satchel on a page — for instance if some imported library or component also uses Satchel — then there may be conflicts within the store or configuration.
  • Because mutators and orchestrators are registered as side effects (the registration happens as part of code that runs at module scope) tools like Webpack have trouble applying optimizations like tree shaking to Satchel projects.

Proposal

Create satchel instances with createSatchel

Instead of assuming there's a global store available, consumers will create an instance of a satchel. There can be multiple satchels, configured separately and completely isolated in terms of data and subscribers — but in practice a codebase should only ever create one.

import { createSatchel } from 'satcheljs';

const satchel = createSatchel(options);

The options would include the middleware, replacing the applyMiddleware API. (I don't think there are any other options at this point.)

Explicitly register subscribers

Subscribers (mutators and orchestrators) must be explicitly registered on the satchel. Now mutator and orchestrator will be true decorators — they'll just wrap the handler in an object to be registered separately.

// myMutator.ts
import { mutator } from 'satcheljs';
import { someAction } from './someAction';

export const myMutator = mutator(someAction, message => {
	// Mutate stuff here
});
// initializeMutators.ts
import { mySatchel } from './mySatchel';
import { myMutator } from './myMutator';

export function initializeMutators() {
	mySatchel.register(myMutator);
}

The mutator and orchestrator APIs will simply return an object with some properties. (If we care to keep the properties private we might encapsulate them in a closure and return that.) The properties will include:

  • The action to subscribe to
  • The callback
  • Whether it is a mutator or orchestrator (or, alternately, we could have separate registerMutator and registerOrchestrator APIs on the satchel, but I think just register is cleaner. We could also call it subscribe — thoughts?)
  • A flag indicating whether it has already been registered. Registering the same subscriber subsequent times is no-op. (We don't want the same subscriber get registered multiple times.)

Other APIs

Now that satchel is instanced, various other APIs will need to be changed:

  • mutatorAction will need to take in the satchel as an agument so that it knows where to register the mutator
  • dispatch will become a method on the satchel
  • action will become a method on a satchel so that it knows where to dispatch the action
  • getRootStore will become a method on the satchel

Add a satchel.hasSubscribers(action) API

This will allow consumers to check if there are any subscribers for an action. While not strictly necessary, this will be useful for middleware that wants to know if the dispatched action is handled by anything.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions