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

Is it possible to dispatch action from the store? #184

Closed
amorphius opened this issue Jun 27, 2015 · 17 comments
Closed

Is it possible to dispatch action from the store? #184

amorphius opened this issue Jun 27, 2015 · 17 comments

Comments

@amorphius
Copy link

I know that this is bad idea, but I'm ultimately careful with this and don't know another option.
I have action handler inside the store which should emit actions through setInterval every few seconds. Is it possible or the only options is to move setInterval to component?

@emmenko
Copy link
Contributor

emmenko commented Jun 27, 2015

move setInterval to component

I think this would be the solution yes.

componentDidMount () {
  this.interval = setInterval(() => {
    this.props.actions.doSomething()
  }, 2000)
}

componentWillUnmount () {
  clearInterval(this.interval)
}

@amorphius
Copy link
Author

Yes, but it seems like a hack, not real "architectural" solution, because starting this timer is normal business logic and I cannot handle it in the store, but component.

So, I have buttons to start and stop timer, but what if those buttons are not in the same component? What is right approach to handle this with redux stores and actions?

@rpominov
Copy link

I believe this is what async action creators for. You can not only dispatch actions from them asynchronously, but also dispatch multiple actions or none. The code may look something like this:

import { TIMER_STARTED, TIMER_TICK, TIMER_STOPPED } from '../constants/ActionTypes';

export function startTimer() {
  return (dispatch, getState) => {
    const { timerId } = getState();
    if (timerId === null) {
      const timerId = setInterval(() => {
        dispatch({type: TIMER_TICK})
      }, 1000)
      dispatch({type: TIMER_STARTED, payload: timerId}) // a store supposed to save `timerId`
    }
  }
}

export function stopTimer() {
  return (dispatch, getState) => {
    const { timerId } = getState();
    if (timerId !== null) {
      clearInterval(timerId)
      dispatch({type: TIMER_STOPPED, payload: timerId}) // now store supposed to set `timerId` to `null` 
    }
  }
}

@johanneslumpe
Copy link
Contributor

If this is business logic you do not want to handle in your component, you can create a business logic object, which does that for you. So instead of having this inside the component you would have an object which subscribes to your redux instance.

If you want to start the timer you send out a START_TIMER action. The reducer in your store then sets timerShouldStart or how you may call it to true. Your business logic object will pick up the change and in the handler start the timer:

const business = {
  timer: null,
  maybeStartTimer() {
    // clean out old timer
    clearInterval(business.timer);
    // redux has been defined somewhere in scope
    const { timerStore } = redux.getState();
    if (timeStore.timerShouldStart) {
      business.interval = setInterval(() => {
        // the actions are accessible as well
        redux.dispatch(doSomething());
      }, 2000)
    }
  }
}

redux.subscribe(::business.maybeStartTimer);

Stopping could then be done with a STOP_TIMER action. The reducer would just set the variable back to false and the object would clear the timer.

@johanneslumpe
Copy link
Contributor

@rpominov I'm not sure I would store the timer id in the store, as it is not serializable (or rather, has no meaning if sent over the wire). But I do like your way of having the TICK dispatch in there

@rpominov
Copy link

@johanneslumpe Good point, my solution probably won't work in an isomorphic app.

@amorphius
Copy link
Author

@rpominov Great, I like your solution, and in isomorphic app I would handle timer initialization manually.

@gaearon
Copy link
Contributor

gaearon commented Jun 27, 2015

I'd like to see this in #140

@taylorhakes
Copy link
Contributor

Would this be the best way to handle WebSockets as well or is there another pattern?

@gaearon
Copy link
Contributor

gaearon commented Jun 27, 2015

@taylorhakes Seems sensible to me.

@pnkapadia6
Copy link

So what is the solution to make a timer work? @rpominov's suggestion?

@markerikson
Copy link
Contributor

I actually threw together a proof-of-concept for a timer middleware a while back. Haven't actually tested it myself, but ought to demonstrate the idea: https://gist.github.com/markerikson/ca96a82d6fdb29388aca4052a9455431

@aputivlskiy
Copy link

aputivlskiy commented Nov 1, 2016

@rpominov What if I want to stop timer automatically, lets say after 10 ticks? The ticks accumulating logic will definitely be placed in TIMER_TICK reducer. But what about the logic to stop timer once 10 is reached? Should I call stopTimer() from reducer?

@markerikson
Copy link
Contributor

@ravensteel : I actually wrote a small sample middleware to allow controlling timers using actions. The code for that is at https://gist.github.com/markerikson/ca96a82d6fdb29388aca4052a9455431 . I haven't actually tested it, but something like that ought to work, I think.

As for stopping the timer, the logic for checking that should _definitely not_ go in the reducer. You'd probably want to do that checking somewhere that has access to checking the state, such as a connected component, a middleware, or a saga. Basically, pretty much anything but a reducer.

@aputivlskiy
Copy link

@markerikson But that generally means scattering the business logic down to visual components? Increasing the ticks counter inside a reducer is one part of it, but no way to react on the resulting value?

@markerikson
Copy link
Contributor

Reducers are in charge of determining how the state updates based on the previous state and the incoming action. Other parts of the codebase should be looking at the updated state and determine what to do based on the new values, whether it be re-rendering with the latest data, or triggering some additional behavior based on the state change.

Also, note that it's entirely possible to have React components that don't actually render anything at all, but simply use the lifecycle methods to wrap up additional behavior (looking at changes in props to see if something should happen, etc).

@anonmily
Copy link

anonmily commented Nov 3, 2016

@aputivlskiy @markerikson I don't like having business logic not related to component rendering in the components, so I usually have use Models that contain the higher-level business logic that affects the data in Stores, but is not related to rendering/tied to any one component. The Models have access to dispatch from the stores as well as the action creators etc.

Lately, I've been use a library I wrote (https://www.npmjs.com/package/redux-actionize) as a wrapper for the action/reducer creation, and the get_actions method accepts the dispatch and does the binding automatically for all registered actions similar to how bindActionCreators works. So, Application.login_in_progress() is actually (1) creating the action of type "application_login_in_progress" and (2) dispatching the action.

I find this higher-level abstraction into Models for some core business logic useful, especially when interacting with data from AJAX requests (e.g. an API) or running background tasks/timers (e.g. setInterval).

Here's an example of an ApplicationModel that interacts with an API, and has a method for starting a timer:

// model_application.js

import Store from './Store'
const dispatch = Store.dispatch

import ApplicationState from './application_state'
const Application = ApplicationState.get_actions(dispatch)

import API from './MyAPI'

let ApplicationModel = {
    login: function(credentials){
        Application.login_in_progress()
        return API.login(credentials)
            .then( result => {
                if(result.success){
                    Application.login_success( result.user, result.token )
                }else{
                    Application.login_failure()
                }
            })
            .catch( err => {
                Application.login_failure()
            })
    },
        check_session(){
             // do something to check session
             setInterval( check_session, 10000 )
        }
}

Then, you can just use this model in your components to start the timer:

// Dashboard.jsx

import React, { Component }  from 'react'
import ApplicationModel from 'model_application.js'

export default class Dashboard extends Component{
    componentWillMount(){
        ApplicationModel.check_session()
    }
    render(){
        return (
            <div className="dashboard">
                  // stuff
            </div>
        )
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants