Click here to Skip to main content
15,887,404 members
Articles / Web Development / React

Watch Out for Undefined State

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
23 Jan 2017CPOL2 min read 6.8K  
Watch out for undefined state

Is your React component not rendering?

Quick quiz: When a React component loads data from the server in componentWillMount like this one below, what will it render?

Beware Undefined State

Original photo by Jay Galvin
JavaScript
class Quiz extends Component {
  componentWillMount() {
    axios.get('/thedata').then(res => {
      this.setState({items: res.data});
    });
  }

  render() {
    return (
      <ul>
        {this.state.items.map(item =>
          <li key={item.id}>{item.name}</li>
        )}
      </ul>
    );
  }
}

If you answered “nothing” or “a console error,” congrats!

If you answered “the data I fetched,” keep reading. ;)

State Starts Off Uninitialized

There are two important things to realize here:

  1. A component’s state (e.g. this.state) begins life as null.
  2. When you fetch data asynchronously, the component will render at least once before that data is loaded – regardless of whether it’s fetched in the constructor, componentWillMount, or componentDidMount.

Yes, even though constructor and componentWillMount are called before the initial render, asynchronous calls made there will not block the component from rendering. You will still hit this problem.

The Fix(es)

This is easy to fix. The simplest way: initialize state with reasonable default values in the constructor.

For the component above, it would look like this:

JavaScript
class Quiz extends Component {
  // Added this:
  constructor(props) {
    super(props);

    // Assign state itself, and a default value for items
    this.state = {
      items: []
    };
  }

  componentWillMount() {
    axios.get('/thedata').then(res => {
      this.setState({items: res.data});
    });
  }

  render() {
    return (
      <ul>
        {this.state.items.map(item =>
          <li key={item.id}>{item.name}</li>
        )}
      </ul>
    );
  }
}

You could also handle the empty data inside render, with something like this:

JavaScript
render() {
  return (
    <ul>
      {this.state && this.state.items && this.state.items.map(item =>
        <li key={item.id}>{item.name}</li>
      )}
    </ul>
  );
}

This is not the ideal way to handle it though. If you can provide a default value, do so.

Trickle-Down Failures

The lack of default or “empty state” data can bite you another way, too: when undefined state is passed as a prop to a child component.

Expanding on that example above, let’s say we extracted the list into its own component:

JavaScript
class Quiz extends React.Component {
  constructor(props) {
    super(props);
    
    // Initialized, but not enough
    this.state = {};
  }

  componentWillMount() {
    // Get the data "soon"
    axios.get('/thedata').then(res => {
      this.setState({items: res.data});
    });
  }

  render() {
    return (
      <ItemList items={this.state.items}/>
    );
  }
}

function ItemList({ items }) {
  return (
    <ul>
    {items.map(item =>
      <li key={item.id}>{item.name}</li>
    )}
    </ul>
  );
}

See the problem? When Quiz first renders, this.state.items is undefined. Which, in turn, means ItemList gets items as undefined, and you get an error – Uncaught TypeError: Cannot read property 'map' of undefined in the console.

Debugging this would be easier if ItemList had propTypes set up, like this:

JavaScript
function ItemList({ items }) {
  return (
    // same as above
  );
}
ItemList.propTypes = {
  items: React.PropTypes.array.isRequired
};

With this in place, you’ll get this helpful message in the console:

“Warning: Failed prop type: Required prop items was not specified in ItemList.”

However, you will still get the error – Uncaught TypeError: Cannot read property 'map' of undefined. A failed propType check does not prevent the component from rendering, it only warns.

But at least, this way, it’ll be easier to debug.

Default Props

One more way to fix this: you can provide default values for props.

Default props aren’t always the best answer. Before you set up a default prop, ask yourself if it’s a band-aid fix.

Is the default value there just to prevent transient errors when the data is uninitialized? Better to initialize the data properly.

Is the prop truly optional? Does it make sense to render this component without that prop provided? Then a default makes sense.

Wrap Up

In short:

  • Async calls during the component lifecycle means the component will render before that data is loaded, so…
  • Initialize state in the constructor and/or be sure to handle empty data.
  • Use PropTypes to aid debugging
  • Use default props when appropriate

Watch Out for Undefined State was originally published by Dave Ceddia at Dave Ceddia on January 16, 2017.

This article was originally posted at https://daveceddia.com/feed.xml

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
Dave is a Software Engineer in the Boston area and writes about AngularJS and other JavaScript things over at daveceddia.com

Comments and Discussions

 
-- There are no messages in this forum --