Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React-redux : listening to state changes to trigger action

I have my redux state like this:

{
  parks: [
    {
      _id:"ad1esdad",
      fullName : "Some Name"
    },
    {
      _id:"ad1es3s",
      fullName : "Some Name2"
    }
  ],
  parkInfo: {
    id : "search Id",
    start_time : "Some Time",
    end_time : "Some Time"
  }
}

I have a parkSelector component from which a user selects parkId and start_time and end_time

import React, { Component } from 'react';
import { changeParkInfo } from '../../Actions';

class ParkSelector extends Component {

  constructor(props) {
    super(props);

    this.handleApply = this.handleApply.bind(this);
    this.rederOptions = this.rederOptions.bind(this);

    this.state = {
      startDate: moment().subtract(1, 'days'),
      endDate: moment(),
      parkId : this.props.parks[0]
    };
  }

  handleApply(event) {
    this.setState({
      parkId : event.target.parkId.value
      startDate: event.target.start_time.value,
      endDate: event.target.end_time.value,
    });
    this.props.changeParkInfo(this.state.parkId,this.state.startDate,this.state.endDate);
  }


  rederOptions(){
    return _.map(this.props.parks,(park,index)=>{
      return(
        <option value={park._id} key={park._id}>{park.profile.fullName}</option>
      );
    });
  }

  render() {

    return (
      <div className="row">
        <div className="pb-4 col-sm-3">
          <form onSubmit={this.handleApply}>
          <select name="parkId" value={this.state.parkId} className="form-control input-sm">
              {this.rederOptions()}
          </select>
          <input name="start_time" type="date" />
          <input name="end_time" type="date" />
          <button type="submit">Apply</button> 
          </form>
        </div>
      </div>
    )
  }
}

function mapStateToProps(state){
  return {
    parks : state.parks
  };
}

export default connect(mapStateToProps,{ changeParkInfo })(ParkSelector);

I have another component 'stats' which needs to displays information related with parkInfo which will be loaded my api request.

import React, { Component } from 'react';
import StatsCard from '../../components/StatsCard';
import { getDashboardStats } from '../../Actions';

class Dashboard extends Component {

  constructor(props) {
    super(props);
  }

  render() {

    return (
      <div className="animated fadeIn">
        <div className="row">
          <StatsCard text="Revenue Collected" value={9999} cardStyle="card-success" />
          <StatsCard text="Total Checkins" value={39} cardStyle="card-info" />
          <StatsCard text="Total Checkouts" value={29} cardStyle="card-danger" />
          <StatsCard text="Passes Issued" value={119} cardStyle="card-warning" />
        </div>

      </div>
    )
  }
}

function mapStateToProps(state){
  return {
    parkInfo : state.parkInfo,
    dashboardStats : state.dashboardStats
  };
}

export default connect(mapStateToProps,{ getDashboardStats })(Dashboard);

I need to call getDashboardStats action (which makes api call and stores in results in dashboardStats of the redux state) whenever the redux state of parkInfo changes.

What is the best way to call this action, I have tried componentWillUpdate but it keeps on updating infinitely. What is best practice for this scenario ?

like image 877
Sanjeev Malagi Avatar asked Dec 07 '17 03:12

Sanjeev Malagi


3 Answers

I had a similar problem but found a suitable approach. I believe the problem has to do with how the responsibilities of reducers and subscribers in a Redux app are often interpreted. My project did not use React, it was Redux-only, but the underlying problem was the same. To put emphasis on answering the underlying problem, I approach the subject from a general perspective without directly referring to your React-specific code.

Problem re-cap: In the app, multiple actions P,Q,R can cause a state change C. You want this state change C to trigger an asynchronous action X regardless of the action that originally caused the state change C. In other words, the async action X is coupled to the state change but intentionally decoupled from the wide range of actions (P,Q,R) that could cause the change C. Such situation does not happen in simple hello-todo examples but does happen in real-world applications.

Naïve answer 1: You cannot trigger another action, it is going to cause infinite loop.

Naïve answer 2: You cannot trigger another action, reducer must not trigger actions or cause any side effects.

Although both naïve answers are true, their base assumptions are wrong. The first wrongly assumes the action X is triggered synchronously and without any stopping condition. The second wrongly assumes the action X is triggered in a reducer.

Answer:

Trigger the action X in a subscriber (aka renderer) and in asynchronous manner. It might sound weird at first but it is not. Even the simplest Redux applications do it. They listen state changes and act based on the change. Let me explain.

Subscribers, in addition to rendering HTML elements, define how actions are triggered in a response to user behaviour. As well as dealing with user behaviour, they can define how actions are triggered in a response to any other change in the world. There is little difference between a user clicking a button after a five seconds and a setTimeout triggering an action after five seconds. As well as we let subscribers to bind an action to a click event or, say, found GPS location, we can let them bind an action to a timeout event. After init, these bindings are allowed to be modified at each state change just like how we can re-render a button or the whole page at a state change.

An action triggered by setTimeout will cause a loop-like structure. Timeout triggers an action, reducers update the state, redux calls subscribers, subscribers set a new timeout, timeout triggers action etc. But again, there is little difference to the loop-like structure caused by normal rendering and binding of events with user behaviour. They are both asynchronous and intended cyclic processes that allow the app to communicate with the world and behave as we like.

Therefore, detect your state change of interest in a subscriber and freely trigger the action if the change happened. Use of setTimeout can be recommended, even with the delay of zero, just to keep the event loop execution order clear to you.

A busy loop is of course a problem and must be avoided. If the action X itself causes such state change that will immediately trigger it again, then we have a busy loop and the app will stop to respond or become sluggish. Thus, make sure the triggered action does not cause such a state change.

If you like to implement a repeating refresh mechanism, for example to update a timer each second, it could be done in same manner. However, such simple repetition does not need to listen state changes. Therefore in those cases it is better to use redux-thunk or write an asynchronous action creator otherwise. That way the intention of the code becomes easier to understand.

like image 55
Akseli Palén Avatar answered Nov 20 '22 06:11

Akseli Palén


If my understanding is correct, you need to make the API call to get the dashboardStats in your Dashboard component, whenever the parkInfo changes.

The correct life-cycle hook in this scenario would be the componentWillReceiveProps

    componentWillReceiveProps(nextProps){
         // this check makes sure that the getDashboardStats action is not getting called for other prop changes
         if(this.props.parkInfo !== nextProps.parkInfo){ 
              this.props.getDashboardStats()
         }
    }

Also note that, componentWillReceiveProps will not be called for the first time, so you may have to call the this.props.getDashboardStats() in componentDidMount too.

like image 20
mkr Avatar answered Nov 20 '22 05:11

mkr


Goal: A change in parkInfo redux-state should prompt Dashboard to dispatch getDashboardInfo and re-render. (This behavior will also be similar in other components).

I use babel transform-class-properties, syntax is slightly different.

example:

// SomeLayout.js

import ParkSelector from 'containers/ParkSelector'
import Dashboard from 'containers/Dashboard'

const SomeLayout = () => {
  return (
    <div>
      <ParkSelector />
      <Dashboard />
    </div>
  )
}

export default SomeLayout

-

// Dashboard.js

// connect maps redux-state to *props* not state, so a new park selection
//  will not trigger this component to re-render, so no infinite loop there

@connect((store) => ({ currentParkId: store.parkInfo.id }, //decorator syntax
                     { getDashboardStats })
)
class Dashboard extends Component {
  state = {
    currentId: this.props.currentParkID,
    parkInfoFoo: '',
    parkInfoBar: ''
  }

  // using null for when no park has been selected, in which case nothing runs
  //  here.
  componentWillReceiveProps(nextProps) {

    // when Dashboard receives new id via props make API call
    // assumes you are setting initial state of id to null in your reducer
    if (nextProps.currentParkId !== null) {
      getDashboardStats(`someurl/info/${nextProps.id}`).then((data) => {

        // update state of Dashboard, triggering a re-render
        this.setState({
          currentId: nextProps.id 
          parkInfoFoo: data.foo,
          parkInfoBar: data.bar 
        })
      })
    }
  }

  render() {
    const { currentId, parkInfoFoo } = this.state

    if (currentId !== null) {
       return <span>{parkInfoFoo}</span>
    }
    return null 
  }  
}

export default Dashboard
like image 1
Stefan Avatar answered Nov 20 '22 06:11

Stefan