Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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);
like image 673
Greg Lafrance Avatar asked Aug 21 '16 15:08

Greg Lafrance


2 Answers

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};
like image 160
agmcleod Avatar answered Sep 22 '22 06:09

agmcleod


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>;
    }
  }
like image 43
Greg Lafrance Avatar answered Sep 24 '22 06:09

Greg Lafrance