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

Support custom dispatchers #60

Merged
merged 1 commit into from
Jun 9, 2015
Merged

Conversation

gaearon
Copy link
Contributor

@gaearon gaearon commented Jun 9, 2015

Goals of this PR:

This PR does not try to solve middleware API or the middleware composition. Instead, it allows to implement #55 and #6 in the userland by separating “Redux instance” (the state holder) and the Dispatcher. By providing a custom Dispatcher, you can experiment with different composable middleware APIs without changing the core.

New definitions

The Dispatcher is a function responsible for turning actions into state changes. Its signature is (initialState, setState) => (action) => (). You won't use it directly, and will instead hand it off to the Redux instance.

The Redux instance is ignorant of how actions turn into state. It has no knowledge of dispatching, or stores. It is a facade for Redux, providing you getState and dispatch, and letting you specify an initial state for hydration.

The Redux library provides a default dispatcher that implements the current behavior with createDispatcher:

import { createRedux, createDispatcher, composeStores } from 'redux';
import * as stores from '../stores/index';

// Compose all your Stores into a single Store function with `composeStores`:
const store = composeStores(stores);

// Create a Dispatcher function for your composite Store:
const dispatcher = createDispatcher(store);

// Create a Redux instance using the dispatcher function:
const redux = createRedux(dispatcher);

The built-in Dispatcher

The default dispatcher looks like this:

export default function createDispatcher(store) {
  return function dispatcher(initialState, setState) {
    let state = store(initialState, {});
    setState(state);

    function dispatchSync(action) {
      state = store(state, action);
      setState(state);
      return action;
    }

    function dispatch(action) {
      return typeof action === 'function' ?
        action(dispatch, state) :
        dispatchSync(action);
    }

    return dispatch;
  };
}

As you can see, perform is no more. Now dispatch behaves exactly like perform used to behave because that's what the Dispatcher does.

Custom Dispatchers

To provide a custom dispatcher, just don't use createDispatcher and pass your own function instead with the same (initialState, setState) => (action) => () signature.

It doesn't even have to understand the concept of Stores. It may do something completely different—the only important part is that it has to translate the actions into the state updates.

Convenience API

Finally, this PR brings back a simpler convenience API for most people who don't need to customize the Dispatcher or the Store composition:

import { createRedux } from 'redux';
import * as stores from '../stores/index';

const redux = createRedux(stores);

This was referenced Jun 9, 2015
@threepointone
Copy link

lovely!

(also, and not sure this makes sense... one can now compose dispatchers?)

@vslinko
Copy link
Contributor

vslinko commented Jun 9, 2015

👍

1 similar comment
@emmenko
Copy link
Contributor

emmenko commented Jun 9, 2015

👍

gaearon added a commit that referenced this pull request Jun 9, 2015
@gaearon gaearon merged commit d5c2b16 into master Jun 9, 2015
@gaearon gaearon deleted the middleware-single-extension-point branch June 9, 2015 09:33
@rpominov
Copy link

rpominov commented Jun 9, 2015

Solves all problems. 👍

@vslinko
Copy link
Contributor

vslinko commented Jun 9, 2015

Hmm. Why dispatcher stores a copy of state? May be redux should pass getState into dispatcher?

export default function createDispatcher(store) {
  return function dispatcher(getState, setState) {
    setState(store(getState(), {}));

    function dispatchSync(action) {
      state = store(getState(), action);
      setState(state);
      return action;
    }

    function dispatch(action) {
      return typeof action === 'function' ?
        action(dispatch, state) :
        dispatchSync(action);
    }

    return dispatch;
  };
}

@tsingson
Copy link

tsingson commented Jun 9, 2015

great job.

@gaearon
Copy link
Contributor Author

gaearon commented Jun 9, 2015

May be redux should pass getState into dispatcher?

I wanted “only the dispatcher may set the state” to be a hard constraint. If it's not true, it's much more difficult to implement transactions, as we saw in #55.

Having the dispatcher maintain the reference to state reinforces this idea—since it's the only thing that can change it, providing getState is unnecessary in my opinion.

@gaearon
Copy link
Contributor Author

gaearon commented Jun 9, 2015

This change has a funny consequence!

Now I can write createReplayingDispatcher and change createDispatcher to createReplayingDispatcher while the code is running, and the dispatcher will be hot reloaded. Which means I don't even need transactions because I can just write an über dispatcher that switches between those folks with options like this:

const dispatcher = createUberDispatcher({ replay: true, log: true }); // can tweak options with hot reloading

@acdlite
Copy link
Collaborator

acdlite commented Jun 9, 2015

Sorry I'm just now looking at this, but yay! Looks great.

@gaearon
Copy link
Contributor Author

gaearon commented Jun 9, 2015

@vslinko

I think you were right now about getState. Passing it instead of initialState makes the custom dispatchers composable. 👍

I'll change it to be (getState, setState) => (action) => () instead.

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.

7 participants