rickyvetter / reductive

Redux in Reason
MIT License
408 stars 40 forks source link

"enhancer" is actually a middleware, not an enhancer #24

Open hedgepigdaniel opened 6 years ago

hedgepigdaniel commented 6 years ago

I think there is a misunderstanding in Reductive about what a Redux enhancer is. From the redux docs:

Everything that a middleware can do can be done by an enhancer (as evidenced by the fact that the applyMiddlewares function from redux returns an enhancer), but the opposite is not true! Some things that enhancers can do that middlewares cannot:

A middleware cannot do these things because it is only called when an action is dispatched to the store. An enhancer wraps the storeCreator, so it can change the state before the store is created and dispatch actions before other actions are dispatched.

I propose the following alternative:

This way it is clear how a store enhancer in the Redux sense can be applied, and the user controls the order in which enhancers and middlewares are applied.

Some types:

type store('action, 'state) = Reductive.Store.t('action, 'state);

type dispatch('action, 'state) = store('action, 'state) => 'action => unit;

type getState('action, 'state) = store('action, 'state) => 'state;

type reducer('action, 'state) = 'state => 'action => 'state;

type middleware('action, 'state) = store('action, 'state) => ('action => unit) => 'action => unit;

type storeCreator('action, 'state) = reducer('action, 'state) => 'state => store('action, 'state);

type enhancer('action, 'state) = storeCreator('action, 'state) => storeCreator('action, 'state);

type applyMiddleware('action, 'state) = middleware('action, 'state) => enhancer('action, 'state);
ambientlight commented 6 years ago

@rickyvetter, @hedgepigdaniel: It would be great to have some progress on this.

I personally don't think enhancer labeled argument need to be removed, we can just rename it to middleware. We don't need applyMiddleware function in this way, since reductive store's customDispatcher does what applyMiddleware when we chain the middleware with @@. It is great to have middleware available out of the box in reductive as it is now.

We can then have those types mentioned above available in reductive:

type store('action, 'state) = Store.t('action, 'state);
type reducer('action, 'state) = ('state, 'action) => 'state;

type middleware('action, 'state) =
  (store('action, 'state), 'action => unit, 'action) => unit;

type storeCreator('action, 'state) =
  (
    ~reducer: reducer('action, 'state),
    ~preloadedState: 'state,
    ~middleware: middleware('action, 'state)=?,
    unit
  ) =>
  store('action, 'state);

type storeEnhancer('action, 'state) =
  storeCreator('action, 'state) => storeCreator('action, 'state);

so we can define store enhancers with:

let logIt: storeEnhancer('action, 'state) = (storeCreator: storeCreator('action, 'state)) => (~reducer, ~preloadedState, ~middleware=?, ()) => {
  let someEffect = anything => Js.log(anything)

  let store = {
    ...storeCreator(~reducer, ~preloadedState, ~middleware?, ()),
    customDispatcher: Some((store, next, action) => {
      action |> someEffect;
      next(action);
      Store.getState(store) |> someEffect;  
    })
  };

  Store.getState(store) |> someEffect;
  store
};

and apply in the following way:

let storeCreator = Reductive.Enhancers.logIt @@ Reductive.Store.create;
let store = storeCreator(
  ~reducer=appReducer,
  ~preloadedState={counter: 0, content: ""},
  (),
);

Those renaming enhancer to middleware will result in breaking change. Is there anything like @ocaml.deprecated to show the warning for a deprecated parameter? Maybe we can just add another labeled parameter middleware and mark enhancer as deprecated? Alternatively we have two function for creating a store (let's say Reductive.Store.new with middleware labeled parameter) with current one Reductive.Store.create marked as deprecated so we avoid the breaking change. What do you think?

rickyvetter commented 6 years ago

Yeah I'm hesitant to do anything here because it would be a breaking change. I agree that enhancer probably isn't the best name because it doesn't match Redux, but it's still descriptive. @ambientlight makes good points that there really isn't a need for a dedicated applyMiddleware function since it's just function application. So we'd basically be making a breaking change from enhancer to middleware without actually using the enhancer name anywhere. It seems wasteful to introduce this churn without good reason.

ambientlight commented 6 years ago

@rickyvetter: so we can technically introduce another storeCreator function with middleware argument and mark Reductive.Store.create as deprecated. Calls will be highlighted with warning and we can keep the existing Reductive.Store.create infinitely long, just having the new storeCreator will remove this ambiguity. Or you think it's not worth it? Then maybe we should introduce the types above which will hint users about middleware and also mention it in docs to decrease this ambiguity?

rickyvetter commented 6 years ago

Definitely on board with mentioning in docs and adding types that might help clarify things.

As far as making a breaking change I'm more hesitant. Do you feel like there is enough confusion here to warrant a breaking change?

ambientlight commented 6 years ago

@rickyvetter: let's then maybe start with the pull requests with these types, docs and examples.

I would personally just attach deprecation notice that will emit warning to existing storeCreator and give an alternative storeCreator.

I am not super familiar, do we have enough editor/compiler support to give smart error messages here: the parameter was renamed?, then there will be less friction if we introduce the breaking change, and we might bump up the major version in this case so that CIs will not break (I assume mostly user's package.json will have ^1.0.0 form)