1

What is the best way to call a dispatch to get initial data on a React component. My understanding is that ComponentWillMount is called before render. So in theory if I call dispatch on ComponentWillMount, by the time I hit render and then ComponentDidMount I should have my data in the component's props, right? I'm not seeing that.

I'm seeing that render gets called twice and that on the first go when the component is being initialized, I cannot access the data in props. It also seems like dispatch does not actually get called until the second render. I'm basically looking to have some light shed on the best way to call a dispatch when initially setting up a component. I'm essentially trying to do something like the following where I use a container component to get my data from dispatch and then pass it to a child component as props. But I also want to initialize some state variables in the ContainerComponent and then pass them to the ChildComponent as props. The thing is that the state variables I want to initialize depend on the data returned from dispatch and ideally I would do the initialization in ComponentWillMount or ComponentDidMount.

import React from 'react';
import axios from 'axios';

import { connect } from 'react-redux';
import ChildComponent from './ChildComponent.js';

import { getTransactionsAll } from '../actions/actions.js';

class ContainerComponent extends React.Component {
  constructor() {
    super();
    this.state = {
      acctList:[],
      acctChecked:[],
      categoryList:[]
    }
}
  componentWillMount() {

    console.log("componentWillMount entered");

    this.props.get_data();
    console.log(this.props.searchProps.transactions_all);//this is undefined meaning the dispatch has not assigned the data yet...??

  }

  componentDidMount() {
    console.log("componentDidMount entered");
    console.log(this.props.searchProps.transactions_all);//this is undefined meaning the dispatch has not assigned the data yet...??
}

  render() {

    console.log("TransactionManagerContainer render entered");
    console.log(this.props.searchProps.transactions_all);//this is undefined the first time around meaning the dispatch has not assigned the data yet...??, but is defined on the second call to render after the dispatch has actually occurred...

return <ChildComponent
             data={this.props.searchProps.data}/>;
}

const mapStateToProps = (state) => ({
  searchProps: state.searchProps
});

export default connect(mapStateToProps, {getTransactionsAll})(TransactionManagerContainer);

Here is my reducer that assigns the state:

import { combineReducers } from 'redux'

import {GET_TRANSACTIONS } from '../actions/actions.js'
import {GET_TRANSACTIONS_ALL } from '../actions/actions.js'

const INITIAL_STATE = { defaultYear: 2016, transactions: []};

function get_transactions(state = INITIAL_STATE, action) {
  // console.log("this is in the reducer: get_transactions");
  // console.log(action);
  switch(action.type) {
    case GET_TRANSACTIONS:
      // return { ...state, transactions: action.payload };
      return Object.assign({}, state, {
        transactions: action.payload,
        selectedYear: action.selectedYear
      })
    default:
      return state;
  }
}

function get_transactions_all(state = INITIAL_STATE, action) {
  console.log("this is the value of action in the reducer: get_transactions_all");
  console.log(action);
  switch(action.type) {
    case GET_TRANSACTIONS_ALL:
      // return { ...state, transactions: action.payload };
      return Object.assign({}, state, {
        transactions_all: action.payload
      })
      console.log("this is the value of state in the reducer after being set");
      console.log(state);
    default:
      return state;
  }
}

const rootReducer = combineReducers({
  //stateProps: get_transactions,
  searchProps: get_transactions_all
})

export default rootReducer

Here are my actions:

import axios from 'axios';

export const GET_TRANSACTIONS = 'GET_TRANSACTIONS';

export function getTransactions(year) {
 return function(dispatch) {
  axios.get(`http://localhost:3001/api/transfilter?year=${year}&grouping=2`)
   .then(response => {
     dispatch({
       type: GET_TRANSACTIONS,
       payload: response.data,
       selectedYear: year
     });
   })
   .catch((error) => {
     console.log(error);
   })
 }
}

export const GET_TRANSACTIONS_ALL = 'GET_TRANSACTIONS_ALL';

export function getTransactionsAll(year) {
 return function(dispatch) {
  axios.get(`http://localhost:3001/api/trans?limit=20`)
   .then(response => {

     dispatch({
       type: GET_TRANSACTIONS_ALL,
       payload: response.data
     });
   })
   .catch((error) => {
     console.log(error);
   })
 }
}
mo_maat
  • 2,110
  • 12
  • 44
  • 72
  • in a react/nuclearjs project we're calling the fetch API data method in componentDidMount so that should be working. can you should us how you're using the component in an HTML template? –  Jan 06 '17 at 21:56
  • The fetch works. Everything works. I just want to know the best way to do it and get a better understanding of the dispatch "lifecycle". As in when does the fetch actually get called because it does not seem to be right away since the render is called twice and the data is not available until the second time render is called. – mo_maat Jan 06 '17 at 22:12
  • where are you passing `searchProps`. You should show this containers parent component and your `get_data` function – azium Jan 06 '17 at 23:06
  • Edits made to include all the files (actions, reducer, etc). I'm passing searchProps from my reducer. – mo_maat Jan 06 '17 at 23:24
  • I think I misunderstand your question. Are you asking why your data only exists on the second render? – azium Jan 07 '17 at 01:08
  • ajax requests are asynchronous.. so when your component loads it calls render right away.. then some time later your ajax call returns and passes your component new props calling render again. that's how it's supposed to work. – azium Jan 07 '17 at 01:09
  • I see. But my question is where can I instantiate the state values in the containerComponent. I need to have access the returned data, filter it, and assign the filtered values to acctList, acctChecked, categoryList. When I try to access the data in the render I get an undefined error because on the first call it is indeed undefined. – mo_maat Jan 07 '17 at 03:24

1 Answers1

5

I believe your main question is:

What is the best way to call a dispatch to get initial data on a React component?

Getting initial data requests (or any AJAX requests in general) should go in the componentDidMount lifecycle event.

There are a few reasons for this, here are two important:

  1. Fiber, the next implementation of React’s reconciliation algorithm, will have the ability to start and stop rendering as needed for performance benefits. One of the trade-offs of this is that componentWillMount, the other lifecycle event where it might make sense to make an AJAX request, will be “non-deterministic”. What this means is that React may start calling componentWillMount at various times whenever it feels like it needs to. This would obviously be a bad formula for AJAX requests.

  2. You can’t guarantee the AJAX request won’t resolve before the component mounts. If it did, that would mean that you’d be trying to setState on an unmounted component, which not only won’t work, but React will yell at you for. Doing AJAX in componentDidMount will guarantee that there’s a component to update.

Credits: I learned that from here, there is also a discussion here.

Then, there are a lot of smaller question you've raised and it will be hard for me to answer all, but I'll try to cover most:

  • After reading the above, you now should understand why your data is undefined in componentWillMount and componentDidMount. That's simply because the data has not arrived yet;
  • It's normal that your data is undefined during the first render of the component. Initial render happens before data arrival;
  • It's normal that the data is defined during the second render. The dispatch triggers asynchronous data fetch. Right after data comes, a reducer is hit and component gets re-rendered (that's the second re-render).
  • If the child components in your main component require the data - check in the parent render method if data exists pass internal components conditionally, only if data is present. Like so:

    class ContainerComponent extends React.Component {
      // ... omitted for brevity
    
      render() {
        return (
          { this.props.searchProps.data ?
              <ChildComponent
                data={this.props.searchProps.data} />
              : <p>Loading</p>
          }
        );
      }
    }
    
Community
  • 1
  • 1
Kaloyan Kosev
  • 12,483
  • 8
  • 59
  • 90
  • Thanks for the conceptual explanations. It makes sense. So I copied and pasted the render you have above and I get an error: `111 | return ( > 112 | { this.props.searchProps.data? | ^ 113 | 115 | :

    Loading

    ` It is not cleanly formatted, not sure how to formate in comments. But essentially I get a syntax error unexpected token where the period is on the first call to this.props.
    – mo_maat Jan 07 '17 at 19:52
  • You're welcome. Hm, I am not sure - the error doesn't speak much to me. Maybe the issue is caused by `this.props.searchProps` being undefined. It's usually a good practice to set defaults to `props` that initially doesn't exist. Because otherwise, they will be always `undefined` during the first render. [Declare default props](http://stackoverflow.com/documentation/reactjs/6371/react-createclass-vs-extends-react-component/25321/declare-default-props-and-proptypes#t=201701071958593048809) and see if the issue is resolved. – Kaloyan Kosev Jan 07 '17 at 20:00
  • Thanks. I did that as you suggested in my other post that you responded to. And there too, setting default props is not working. I'm stumped. It seems as though my default props are being ignored. I've even gone to my babel.rc and made sure I have the 2016 initializers set. I've tried both the static method of setting default props in the constructor and doing it outside the class using Component.defaultProps. Neither seem to work cause I'm initially still getting undefined. – mo_maat Jan 07 '17 at 20:10
  • I am starting to think that there are too many places in your use-case that might cause troubles. My advice is that you split each question you have, try to reproduce it via simpler example, try to create a jsfiddle. Otherwise - it will be hard for me or anybody else to help :) PS: [See my edit in the other question you asked](http://stackoverflow.com/a/41522489/1333836). There is a huge chance the two issues you see are co-related and my answer there to help you to resolve both. – Kaloyan Kosev Jan 07 '17 at 20:20
  • I still have a question that is relevant to this post and was part of my initial question. What is the right way or best way to manipulate state based on the data returned from Redux? If I can only access the data on the second render, then I can't assign state in my component in the render because it will cause an infinite loop. For example in the data returned from Redux I want to get a list of the distince account types and put than in an acctList state object that is local to my ContainerComponent. I understand I could do it in Redux, but I want to isolate this to the ContainerComponent. – mo_maat Jan 07 '17 at 21:04
  • And maybe doing this in redux through an action such as GetAccountList and assigning it as a prop to ContainerComponent is the right way to do it. .. I just want to know what other alternatives there may be. – mo_maat Jan 07 '17 at 21:07
  • Each time when Redux hit a reducer and updates your application store (state), your component will re-render. Hm, the best way to manipulate state based on the data returned from Redux - it depends. It you need to manipulate your component state - you can use the [`componentWillReceiveProps()` lifecycle event](https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops). It is invoked before a mounted component receives new props and it is safe to `setState()` in this method. – Kaloyan Kosev Jan 07 '17 at 21:11
  • Alternatively - if the child components in your main component require the data - check in the parent render method if data exists pass internal components conditionally, only if data is present. Otherwise a "loading" indicator maybe. Both options are valid. – Kaloyan Kosev Jan 07 '17 at 21:12
  • Cool. I think that componentWillReceiveProps() may be the right place to do what I need. Thanks. – mo_maat Jan 07 '17 at 21:49
  • Ok. So I tried it and that won't work. When componentWillReceiveProps is called I don't seem to have the data yet. Maybe there is another issue. I'll play with this. – mo_maat Jan 07 '17 at 21:55
  • Yes, that's correct. `componentWillReceiveProps` should be always triggered as soon as you receive the data (if you're using the same Redux flow as we discussed). Please note that you must access the `nextProps` param coming from the `componentWillReceiveProps(nextProps) { ... }`. So when you're inside this function, you should check for your data in `nextProps.searchProps`, not `this.props. searchProps `. The second one will still represent be the old props. Maybe that's why your data is missing? – Kaloyan Kosev Jan 07 '17 at 22:05
  • Awesome!!! That worked!! Thanks so much. I'm going to have to read up a bit on redux. But running into these hurdles is the best way to learn. This solves all my problems from this and the other thread. Thanks again. – mo_maat Jan 07 '17 at 22:15
  • Great @user1991118! Good luck! I personally had similar issues when I was starting. Hard to explain and a lot of inter-collected things, but I am happy we finally solved the puzzle! PS: Since my huge answer finally solved your question, I'd like to kindly ask you to [accepting it](http://meta.stackexchange.com/a/5235/343165) by clicking the check-mark. This indicates to the wider community that you've found a solution, gives some reputation to both you & me, and It might help people experiencing a similar issue by getting your question noticed. – Kaloyan Kosev Jan 08 '17 at 16:14