Watch Out for Undefined State

By Dave Ceddia Comment

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

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:

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:

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:

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:

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.

This can be done a few ways.

defaultProps property

This method works whether your component is a stateless functional one, or a class that inherits React.Component.

class MyComponent extends React.Component {
  render() {
    // ...
  }
}
MyComponent.defaultProps = {
  items: []
};

defaultProps static property

This method only works for classes, and only if your compiler is set up to support the static initializer syntax from ES7.

class MyComponent extends React.Component {
  static defaultProps = {
    items: []
  }

  render() {
    // ...
  }
}

Destructuring in render

A default can be provided using the ES6 destructuring syntax right in the render function.

class MyComponent extends React.Component {
  render() {
    const { items = [] } = this.props;

    return (
      <ItemList items={items}/>
    );
  }
}

This line says “extract the items key from this.props, and if it’s undefined, set it to an empty array”.

const { items = [] } = this.props;

Destructuring in arguments

If your component is of the stateless functional variety, you can destructure right in the arguments:

function ItemList({ items = []}) {
  return (
    // Use items here. It'll default to an empty array.
  );
}

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
  • Destructuring syntax is a clean, easy way to provide defaults
comments powered by Disqus