If you’ve worked on any large React project, you’ve probably run into a component or two that gave you quite a headache when you tried to reuse it. I definitely have. It’s almost inevitable when working on a large project with a substantial number of developers. Over the past few years, I’ve put together a list of lessons learned that have helped prevent this problem. It’s far from complete, but it’s substantial enough that it might help someone else.

I find it easier to communicate coding concepts when working with real code, so for the purposes of this blog I’ve written a component with a number of issues:

import React, { Component } from 'react';

import './Ticker.css';

const baseUri = 'https://api.iextrading.com/1.0';
const headers = {
 'Content-Type': 'application/json',
 'Accept': 'application/json',
};

export default class Ticker extends Component {
  constructor(props) {
    super(props);

    this.state = {
      isLoaded: false,
      latestPrice: 0,
    };
  }

  fetchData = async () => {
    const { params } = this.props;
    const { symbol } = params;

    const response = await fetch(`${baseUri}/stock/${symbol}/quote`, { headers });
    const data = await response.json();

    this.setState({
      data,
      isLoaded: true,
    });
  };

  componentDidMount() {
    this.fetchData();
  }

  componentWillReceiveProps(props) {
    if (this.props.params.symbol !== props.params.symbol) {
      this.fetchData();
    }
  }

  render() {
    const { isLoaded, data } = this.state;

    return isLoaded
      ? (
        <div className="ticker">
          <span className="symbol">{data.symbol}</span>
          <span className="price">{data.latestPrice}</span>
          <span className="disclaimer">Data provided for free by <a href="https://iextrading.com/developer">IEX</a>. View <a href="Data provided for free by IEX. View IEX’s Terms of Use.">IEX’s Terms of Use</a>.</span>
        </div>
      )
      : (
        <div className="ticker">
          <div className="loading-indicator">Loading...</div>
        </div>
      );
  }
}

Let’s improve it.

1. Use shallow properties

Passing objects as parameters into a React component often adds unnecessary levels of complexity. Properties like title and height and shouldShowFooter take simple strings, numbers, and booleans. A developer encountering these properties knows what to put into them. Properties like params and configuration and settings take objects with unknown structures as values. A developer encountering these properties has no idea what to put into them without digging deep into the code. With any hope, there is ample documentation, or there’s a thoroughly specific PropTypes on the component, but often there isn’t.

I’ve found one frequent exception to this rule: passing domain objects as parameters. If your application has standard User objects or Cat objects, it’s fine to pass those into a component as a parameter, so long as the parameter expects the exact same object schema used in the rest of the application.

Let’s refactor our component to use shallow properties.

export default class Ticker extends Component {
  constructor(props) {
    super(props);

    this.state = {
      isLoaded: false,
      latestPrice: 0,
    };
  }

  fetchData = async () => {
    const { symbol } = this.props;

    const response = await fetch(`${baseUri}/stock/${symbol}/quote`, { headers });
    const data = await response.json();

    this.setState({
      data,
      isLoaded: true,
    });
  };

  componentDidMount() {
    this.fetchData();
  }

  componentWillReceiveProps(props) {
    if (this.props.symbol !== props.symbol) {
      this.fetchData();
    }
  }

  render() {
    const { isLoaded, data } = this.state;

    return isLoaded
      ? (
        <div className="ticker">
          <span className="symbol">{data.symbol}</span>
          <span className="price">{data.latestPrice}</span>
          <span className="disclaimer">Data provided for free by <a href="https://iextrading.com/developer">IEX</a>. View <a href="Data provided for free by IEX. View IEX’s Terms of Use.">IEX’s Terms of Use</a>.</span>
        </div>
      )
      : (
        <div className="ticker">
          <div className="loading-indicator">Loading...</div>
        </div>
      );
  }
}

2. Split data fetching UI components into a UI component and a data component

If you’ve ever seen a UI component in an application you’re working on, dug into the code to find it, and then discovered that you can’t reuse it easily because it fetches its own data, then welcome to the club. Don’t do it! Build the UI separately from the data fetching.

Let’s refactor our component into two:

import React, { Component } from 'react';

import './Ticker.css';

const baseUri = 'https://api.iextrading.com/1.0';
const headers = {
 'Content-Type': 'application/json',
 'Accept': 'application/json',
};

class TickerView extends Component {
  render() {
    const { isLoaded, symbol, price } = this.props;

    return isLoaded
      ? (
        <div className="ticker">
          <span className="symbol">{symbol}</span>
          <span className="price">{price}</span>
          <span className="disclaimer">Data provided for free by <a href="https://iextrading.com/developer">IEX</a>. View <a href="Data provided for free by IEX. View IEX’s Terms of Use.">IEX’s Terms of Use</a>.</span>
        </div>
      )
      : (
        <div className="ticker">
          <div className="loading-indicator">Loading...</div>
        </div>
      );
  }
}

export default class Ticker extends Component {
  constructor(props) {
    super(props);

    this.state = {
      isLoaded: false,
      latestPrice: 0,
      symbol: '',
    };
  }

  fetchData = async () => {
    const { symbol } = this.props;

    const response = await fetch(`${baseUri}/stock/${symbol}/quote`, { headers });
    const data = await response.json();

    if (data != null) {
      this.setState({
        latestPrice: data.latestPrice,
        symbol: data.symbol,
        isLoaded: true,
      });
    }
  };

  componentDidMount() {
    this.fetchData();
  }

  componentWillReceiveProps(props) {
    if (this.props.symbol !== props.symbol) {
      this.fetchData();
    }
  }

  render() {
    const { isLoaded, symbol, latestPrice } = this.state;
    
    return <TickerView isLoaded={isLoaded} symbol={symbol} price={latestPrice} />;
  }
}

3. Separate conditional rendering logic into its own component

If the ternary isLoaded ? ... : ... triggered your spidey sense, that’s a good thing. It should. A component shouldn’t know whether or not it’s loaded; that’s the responsibility of the component loading the data. This applies generally, and not just for loading data, though that’s where I’ve seen it done most frequently.

Let’s refactor again:

import React, { Component } from 'react';

import './Ticker.css';

const baseUri = 'https://api.iextrading.com/1.0';
const headers = {
 'Content-Type': 'application/json',
 'Accept': 'application/json',
};

const LoadingIndicator = () => <div className="loading-indicator">Loading...</div>;

class TickerView extends Component {
  render() {
    const { symbol, price } = this.props;

    return (
      <div className="ticker">
        <span className="symbol">{symbol}</span>
        <span className="price">{price}</span>
        <span className="disclaimer">Data provided for free by <a href="https://iextrading.com/developer">IEX</a>. View <a href="Data provided for free by IEX. View IEX’s Terms of Use.">IEX’s Terms of Use</a>.</span>
      </div>
    );
  }
}

export default class Ticker extends Component {
  constructor(props) {
    super(props);

    this.state = {
      isLoaded: false,
      latestPrice: 0,
      symbol: '',
    };
  }

  fetchData = async () => {
    const { symbol } = this.props;

    const response = await fetch(`${baseUri}/stock/${symbol}/quote`, { headers });
    const data = await response.json();

    if (data != null) {
      this.setState({
        latestPrice: data.latestPrice,
        symbol: data.symbol,
        isLoaded: true,
      });
    }
  };

  componentDidMount() {
    this.fetchData();
  }

  componentWillReceiveProps(props) {
    if (this.props.symbol !== props.symbol) {
      this.fetchData();
    }
  }

  render() {
    const { isLoaded, symbol, latestPrice } = this.state;
    
    return isLoaded
      ? <TickerView symbol={symbol} price={latestPrice} />
      : <LoadingIndicator />;
  }
}

4. Don’t fetch in components

Fetching ties a component directly to a specific API. This adds a lot of logic to your component that shouldn’t be there, like composing urls, parsing JSON, etc. Do this somewhere else. This keeps the code in your component to a minimum, and allows you to reuse the fetching logic elsewhere.

Let’s refactor again:

import React, { Component } from 'react';

import './Ticker.css';

const baseUri = 'https://api.iextrading.com/1.0';
const headers = {
 'Content-Type': 'application/json',
 'Accept': 'application/json',
};

const TickerRepository = {
  async quote(symbol) {
    const response = await fetch(`${baseUri}/stock/${symbol}/quote`, { headers });
    return await response.json();
  },
};

const LoadingIndicator = () => <div className="loading-indicator">Loading...</div>;

class TickerView extends Component {
  render() {
    const { symbol, price } = this.props;

    return (
      <div className="ticker">
        <span className="symbol">{symbol}</span>
        <span className="price">{price}</span>
        <span className="disclaimer">Data provided for free by <a href="https://iextrading.com/developer">IEX</a>. View <a href="Data provided for free by IEX. View IEX’s Terms of Use.">IEX’s Terms of Use</a>.</span>
      </div>
    );
  }
}

export default class Ticker extends Component {
  constructor(props) {
    super(props);

    this.state = {
      isLoaded: false,
      latestPrice: 0,
      symbol: props.symbol,
    };
  }

  fetchData = async () => {
    const { symbol } = this.props;

    const quote = await TickerRepository.quote(symbol);

    if (quote != null) {
      this.setState({
        ...quote,
        isLoaded: true,
      });
    }
  };

  componentDidMount() {
    this.fetchData();
  }

  componentWillReceiveProps(props) {
    if (this.props.symbol !== props.symbol) {
      this.fetchData();
    }
  }

  render() {
    const { isLoaded, symbol, latestPrice } = this.state;
    
    return isLoaded
      ? <TickerView symbol={symbol} price={latestPrice} />
      : <LoadingIndicator />;
  }
}

5. Use PropTypes

Always use PropTypes. They provide intellisense in some IDE’s, warnings in React, and a simple way for developers to understand what’s required when reading a component’s code.

Refactor:

import React, { Component } from 'react';
import PropTypes from 'prop-types';

import './Ticker.css';

const baseUri = 'https://api.iextrading.com/1.0';
const headers = {
 'Content-Type': 'application/json',
 'Accept': 'application/json',
};

const TickerRepository = {
  async quote(symbol) {
    const response = await fetch(`${baseUri}/stock/${symbol}/quote`, { headers });
    return await response.json();
  },
};

const LoadingIndicator = () => <div className="loading-indicator">Loading...</div>;

class TickerView extends Component {
  static propTypes = {
    symbol: PropTypes.string.isRequired,
    price: PropTypes.number,
  };

  static defaultProps = {
    price: 0,
  };

  render() {
    const { symbol, price } = this.props;

    return (
      <div className="ticker">
        <span className="symbol">{symbol}</span>
        <span className="price">{price}</span>
        <span className="disclaimer">Data provided for free by <a href="https://iextrading.com/developer">IEX</a>. View <a href="Data provided for free by IEX. View IEX’s Terms of Use.">IEX’s Terms of Use</a>.</span>
      </div>
    );
  }
}

export default class Ticker extends Component {
  static propTypes = {
    symbol: PropTypes.string.isRequired,
  };

  constructor(props) {
    super(props);

    this.state = {
      isLoaded: false,
      latestPrice: 0,
      symbol: props.symbol,
    };
  }

  fetchData = async () => {
    const { symbol } = this.props;

    const quote = await TickerRepository.quote(symbol);

    if (quote != null) {
      this.setState({
        ...quote,
        isLoaded: true,
      });
    }
  };

  componentDidMount() {
    this.fetchData();
  }

  componentWillReceiveProps(props) {
    if (this.props.symbol !== props.symbol) {
      this.fetchData();
    }
  }

  render() {
    const { isLoaded, symbol, latestPrice } = this.state;
    
    return isLoaded
      ? <TickerView symbol={symbol} price={latestPrice} />
      : <LoadingIndicator />;
  }
}

6. Don’t put positioning logic in the root element

The root element shouldn’t know anything about its container. This prevents it from being used in multiple places. I’ve seen some code bases that roughly do the following:

const Inner = () => (
  <div style={{ width: '100%', height: '100%' }}>I'm unusable!</div>
);

const Outer = () => (
  <div style={{ position: 'relative', width: 100, height: 100 }}>
    <Inner />
  </div>
);

Never do this. The inner component will baffle and confuse every other developer who encounters it. When they find out how it works, it will baffle and confuse them even more. Instead of this, you can inject a classname from the outer class that adds positional information.

For example:

.outer-inner {
  width: 100%;
  height: 100%;
}
import classnames from 'classnames';

const Inner = ({ className }) => (
  <div className={classnames('inner', className)}>I'm usuable!</div>
);

const Outer = () => (
  <div style={{ position: 'relative', width: 100, height: 100 }}>
    <Inner className="outer-inner" />
  </div>
);

Outro

This isn’t a complete list by any stretch of the imagination. I’ve seen these various issues crop up very frequently, so I thought it was worth pointing them out. If you have anything to add, please send it over and I’ll add it to the list!

Happy coding!