angular-redux / ng-redux

Angular bindings for Redux
MIT License
1.16k stars 177 forks source link

Allow dependency injectable reducers #65

Open mikemclin opened 8 years ago

mikemclin commented 8 years ago

Similar to dependency injectable middleware, allow the reducer to be a string of an Angular service as well, which would then be injected.

The reason why is because currently I can't seem to find a way to use the library in a project that doesn't use modules (not using webpack, browserify, etc). So, import reducers from './reducers'; won't work for me.

My main reducer is an Angular factory, and it consumes other reducers, which are also Angular factories.

It looks like you aren't actually creating the Redux store until after the config phase anyways (since you need to wait for the injectable middleware).

wbuchwalter commented 8 years ago

See my answer to #59, which the same issue as this one. Like explained there, I most likely won't be supporting injectable reducers. Do you have a specific reason for not using a bundler? Or you just never needed one?

mikemclin commented 8 years ago

I have a project that is being refactored to use a redux pattern. Also having to refactor it to use webpack, etc makes the task that much bigger. There are also cases that I'd like to use redux, but don't want fellow developers (who aren't familiar with modules, or even ES2015 yet, for that matter) to have to tackle too much.

Currently, I'm just using the official Redux library in an Angular project (which works great). Your library offers several conveniences and is Angular aware, so I'd prefer to use it instead. Plus, I'd like to use it, because eventually, when I do adopt webpack, I can use a redux service that I am already familiar with.

I understand that bringing reducers in from a bundler is better. No argument here. I guess I'm just asking that those of us that want to take baby steps to get there can be supported too.

If not, no worries. Thanks for your contribution :+1: to Angular.

wbuchwalter commented 8 years ago

You bring some valid points, I have a bad tendency to assume everyone using Redux is comfortable with ES2015 etc. Did you try replaceReducer? What I'm mostly worried about are the bad practice that a reducer as a service enables. But this could be somewhat prevented through good documentation. I'll update you on this soon, just trying to see if there could be a better way to achieve what you need.

mikemclin commented 8 years ago

I did not try replaceReducer, and was unaware. I see the store is created and put on the $ngRedux service. So, I would just run $ngRedux.store.replaceReducer(myAngularReducersFactory), (unless Im mistaken)?

I'm actually content with that flow.

Also, I understand your concern on not breaking the redux pattern, but unless I'm mistaken, using modules and a bundler instead of the Angular DI does nothing to enforce the pattern. Couldn't I simply require('./reducers') anywhere I wanted and still use them, breaking the redux flow? It's not like modules make me use redux correctly.

wbuchwalter commented 8 years ago

Yes, you just create the store with an empty reducer and then replace it after the config phase with your factory.

And yes you can break the redux pattern with modules, it's not ng-redux role to prevent that anyway. What I meant is that in my opinion it feels almost correct to inject things in your reducer when you have angular's DI on hand. Especially since newcomers to redux often don't know if they should put their async logic in reducers or in action creators. But like I said, with a good documentation on what not to do, I would be comfortable too.

Let me know how replaceReducer works for you or if you encounter issues.

wbuchwalter commented 8 years ago

(I'm reopening this issue, since there will be something to do about it, either in code or doc)

mikemclin commented 8 years ago

replaceReducer worked as expected. Thanks.

Ruto8 commented 8 years ago

Currently in our project we're writing reducers as angular factories and using dependency injection like that:

angular.module("MyApp").config ($ngReduxProvider, FactoryProvider) ->
  reducers =
    reducerFromFactory: FactoryProvider.$get()
  $ngReduxProvider.createStoreWith(Redux.combineReducers(reducers), middlewares, storeEnhancers)

Hope that might help. Pardon for my coffee and ng-annotate :)

maxime1992 commented 8 years ago

@Ruto8 I need to be able to use $http in my actions. How can I do that please ? I do not understand your code but I'm sure I need something like that.

Basically, I want to call $http and then dispatch an action.

EDIT 1 : What's reducerFromFactory ?

szaccaria commented 7 years ago

Hi @maxime1992, @wbuchwalter and rest of you all. Also me I trying to use $http or other service in action creator... I want the "holy grail" for use async call in redux flow, but I would to use the angular injection. I have try something of similiar... but I'm afraid to does it in the wrong way, could you correct me.

In the config phase I wrote this, using the thunk middleware

$ngReduxProvider.createStoreWith(RootReducer, [window.ReduxThunk.default]);

(I don't have any bundlers at the moment at all, so I had musted write "window.ReduxThunk.default").

And this is my action creator

var filterActions = {
    loadFilter : asyncLoadFiler,
    loaded : loaded
}

function asyncLoadFiler(value) {
    return function(dispatch) {
        var injector = angular.injector([ 'ng' ]);
        var $timeout = injector.get('$timeout');
        return $timeout(function() {
            dispatch(loaded(new Date()))
        }, 2000);
    }
}

function loaded(value) {
    return {
        type : FILTER.LOADED,
        payload : value
    }
}

I don't like

var injector = angular.injector([ 'ng' ]);
var $timeout = injector.get('$timeout');

but it is the better that I had can does, somebody have better suggestion?

for completeness, this is my reducer

var filterReducer = function(state, action) {
    if (angular.isUndefined(state)) {
        return {}
    }
    switch (action.type) {
    case FILTER.LOADED:
        var newState = angular.copy(state);
        newState.filters = action.payload;
        return newState;
        break;
    }
    return state;
}

Thanks in advance

Andrew-Lahikainen commented 6 years ago

@szaccaria I know this is old, but Redux Thunk supports an extra argument, you could use that to inject commonly used angular services.

From the docs:

const store = createStore(
  reducer,
  applyMiddleware(thunk.withExtraArgument(api))
)

// later
function fetchUser(id) {
  return (dispatch, getState, api) => {
    // you can use api here
  }
}

To pass multiple things, just wrap them in a single object and use destructuring:

const store = createStore(
  reducer,
  applyMiddleware(thunk.withExtraArgument({ api, whatever }))
)

// later
function fetchUser(id) {
  return (dispatch, getState, { api, whatever }) => {
    // you can use api and something else here here
  }
}

Your example uses $timeout, wouldn't it be better to include some sort of delay middleware that looks for a delay property on your actions, and it would just delay by that amount? More middleware like that could reduce the number of dependencies you will need to inject.

Example middleware (from Redux docs):

/**
 * Schedules actions with { meta: { delay: N } } to be delayed by N milliseconds.
 * Makes `dispatch` return a function to cancel the timeout in this case.
 */
const timeoutScheduler = store => next => action => {
  if (!action.meta || !action.meta.delay) {
    return next(action)
  }
​
  const timeoutId = setTimeout(
    () => next(action),
    action.meta.delay
  )
​
  return function cancel() {
    clearTimeout(timeoutId)
  }
}

Example action:

function loaded(value) {
    return {
        type: FILTER.LOADED,
        payload: value,
        meta: {
            delay: 2 * 1000
        }
    }
}
szaccaria commented 6 years ago

@Andrew-Lahikainen thanks! It is never too late for learn somethings good :) Anyway in the meantime I resolved the problem with thunk, like you suggested... passing to typescript and to webpack... now the life is more easy :)

Thanks again for yor effort!