Why is my React component render called twice, once without data and then later with data, but too late exception?

10,183

Solution 1

In terms of the error with your second render, the state must be getting overridden in a way you're not expecting. So in your reducer, you're returning array that contains whatever data is, and a splat of the current state. With arrays that does a concat.

var a = [1,2,3]
var b = [a, ...[2,3,4]]

Compiles to:

var a = [1, 2, 3];
var b = [a].concat([2, 3, 4]);

So given you're expecting a children property, what i think you actually want is a reducer that returns an object, not an array, and do something like this instead:

return Object.assign({}, state, { children: action.payload.data });

From there be sure to update the initial state to be an object, with an empty children array.

Get rid of this line since you're using props instead of state. State can be helpful if you need to manage changes just internally to the component. But you're leveraging connect, so that's not needed here.

this.state = {treedata: null};

Solution 2

Solved this by checking for the presence of this.props.treedata, etc. and if not available yet, just should div "loading...".

  render () {
    if (this.props.treedata && this.props.treedata[0] && this.props.treedata[0].children) {
      console.dir(this.props.treedata[0].children);
      return (
        <div className="tree-nav">
          {this.props.treedata[0].children.map(this.renderTreeData)}   
        </div>
      );
    } else {
      return <div>Loading...</div>;
    }
  }
Share:
10,183
Greg Lafrance
Author by

Greg Lafrance

Working in React, Angular, ROR, NodeJS, MongoDB, etc. Speak and read Japanese (lived there 6 years).

Updated on July 21, 2022

Comments

  • Greg Lafrance
    Greg Lafrance almost 2 years

    I have a component TreeNav whose data comes from api call. I have setup reducer/action/promise and all the plumbing, but in component render when I call map() over the data, getting "Uncaught TypeError: Cannot read property 'map' of undefined".

    Troubleshooting revealed TreeNav render() is called twice. 2nd time is after data comes back from api. But due to 1st render() error, 2nd render() never runs.

    Here are my code files:

    -------- reducers/index.js ---------

    import { combineReducers } from 'redux';
    import TreeDataReducer from './reducer_treedata';
    
    const rootReducer = combineReducers({
      treedata: TreeDataReducer
    });
    
    export default rootReducer;
    

    -------- reducers/reducer_treedata.js ---------

    import {FETCH_TREE_DATA} from '../actions/index';
    
    export default function (state=[], action) {
        switch (action.type) {
            case FETCH_TREE_DATA: {
                return [action.payload.data, ...state];
            }
        }
    
        return state;
    }
    

    -------- actions/index.js --------

    import axios from 'axios';
    
    const ROOT_URL = 'http://localhost:8080/api';
    
    export const FETCH_TREE_DATA = 'FETCH_TREE_DATA';
    
    export function fetchTreeData () {
        const url = `${ROOT_URL}/treedata`;
        const request = axios.get(url);
    
        return {
            type: FETCH_TREE_DATA,
            payload: request
        };
    }
    

    -------- components/tree_nav.js --------

    import React, {Component} from 'react';
    import {connect} from 'react-redux';
    import {bindActionCreators} from 'redux';
    import {fetchTreeData} from '../actions/index';
    
    class TreeNav extends Component {
      constructor (props) {
        super(props);
    
        this.state = {treedata: null};
        this.getTreeData();             
      }
    
      getTreeData () {
        this.props.fetchTreeData();
      }
    
      renderTreeData (treeNodeData) {
        const text = treeNodeData.text;
    
        return (
          <div>
            {text}
          </div>
        );
      }
    
      render () {
        return (
          <div className="tree-nav">
            {this.props.treedata.children.map(this.renderTreeData)}   
          </div>
        );
      }
    }
    
    function mapStateToProps ({treedata}) {
        return {treedata};
    }
    
    // anything returned from this function will end up as props 
    // on the tree nav
    function mapDispatchToProps (dispatch) {
      // whenever selectBook is called the result should be passed to all our reducers
      return bindActionCreators({fetchTreeData}, dispatch);
    }
    
    // Promote tree_nav from a component to a container. Needs to know about
    // this new dispatch method, fetchTreeData. Make it available as a prop.
    export default connect(mapStateToProps, mapDispatchToProps)(TreeNav);
    
  • agmcleod
    agmcleod over 7 years
    That answer is fine, I would recommend making the reducers more clever on the data object it builds out though. Just makes your view code cleaner :)
  • Joe Saad
    Joe Saad almost 7 years
    I'm currently using this check. But I'm wonder if there's a better solution for this that the render wouldn't have to run twice. Any updates?