Jack Franklin

Extracting Logic from React Components

In the previous screencast we took a React component that was doing too much and refactored it, splitting it into two components that are easier to maintain, use and test. Although I'd recommend watching that video first, you don't need to have watched it to read this blog post. You can find all the code on GitHub if you'd like to run it locally.

The starting point

Let's start by looking at the Money component, that takes some value and formats it onto the page:

class Money extends Component {
static propTypes = {
currency: PropTypes.string.isRequired,
amount: PropTypes.number.isRequired,
};

getCurrencyData(currency) {
return {
GBP: { base: 100, symbol: '£' },
USD: { base: 100, symbol: '$' },
}[this.props.currency];
}

formatAmount(amount, base) {
return parseFloat(amount / base).toFixed(2);
}

render() {
const currency = this.getCurrencyData();
if (currency) {
const { symbol, base } = currency;
const formatted = this.formatAmount(this.props.amount, base);

return (
<span>
{symbol}
{formatted}
</span>
);
} else {
return <span>{this.props.amount}</span>;
}
}
}

There's two parts of functionality here that I would look at extracting into a separate class:

The reason that I want to extract these is so I can test them in complete isolation. Right now to test formatting of amounts I have to create and mount a React component, but I should be able to just call that function and check the result.

Extracting amount formating

Let's create src/format-currency.js which will house the formatAmount function that is currently in our Money component.

export const formatAmount = (amount, base) => {
return parseFloat(amount / base).toFixed(2);
};

I've just lifted the function in its entirety to the new file and added an export to the beginning.

To test this, we can replace the body of Money's formatAmount so that it just calls the new function from our format-currency.js module:

import { formatAmount } from './format-currency'

class Money extends Component {
...
formatAmount(amount, base) {
return formatAmount(amount, base)
}
...
}

Notice that I've still left the formatAmount function defined on Money; when pulling code apart like this you should do it in small steps; doing it like this decreases the chance of inadvertently breaking the code and also makes it easier to retrace your steps if something does go wrong.

Because these components are well tested, I can run yarn test to ensure everything passes, which it does.

Next up, I'll remove the formatAmount function from Money and update the render function to call our external function directly:

// inside Money component
render() {
const currency = this.getCurrencyData()

if (currency) {
const { symbol, base } = currency
// this used to say this.formatAmount
const formatted = formatAmount(this.props.amount, base)

return (
<span>{symbol}{formatted}</span>
)
} else {
return <span>{this.props.amount}</span>
}
}

Once again, yarn test confirms that we are good. Now all our original tests are passing, we can add some new tests to test `formatAmount in isolation. It's important to always do it this way round - get all your existing tests green before adding new ones.

import { formatAmount } from './format-currency';

test('it formats the amount to 2 dp', () => {
expect(formatAmount(2000, 100)).toEqual('20.00');
});

test('respects the base', () => {
expect(formatAmount(2000, 10)).toEqual('200.00');
});

test('it deals with decimal places correctly', () => {
expect(formatAmount(2050, 100)).toEqual('20.50');
});

We now have thorough tests for formatting amounts that are not attached to our React component at all. Sure, the formatAmount function is very straightforward for now, but as it grows we can now test it very easily without any need to fire up a React component to do so.

Extracting the currency data

One down, one to go! Let's now pull out getCurrencyData using a very similar method to above. First, I'll create currency-data.js and pull our function over:

export const getCurrencyData = currency => {
return {
GBP: { base: 100, symbol: '£' },
USD: { base: 100, symbol: '$' },
}[this.props.currency];
};

But wait! There's a bug - the function takes in a currency argument but actually completely ignores it in favour of this.props.currency. This is entirely accidental but shows the value of separating business logic from component UI logic. In a React component it's too easy to refer to this.props or this.state and it becomes hard to track which functions use which values. Pulling them out into their own modules forces you to pass arguments through, which in turn helps clarify the API and help you think about what data the function really needs.

Once I fix up that bug by making sure we call getCurrencyData with the right value, and update the function to refer to the currency argument, not this.props.currency, we can make Money's getCurrencyData delegate to the new function:

...
import { getCurrencyData } from './currency-data'

class Money extends Component {
...
getCurrencyData(currency) {
return getCurrencyData(currency)
}

render() {
const currency = this.getCurrencyData(this.props.currency)
...
}
}

And once again yarn test confirms that nothing has broken. Now we can make the next step of entirely deleting getCurrencyData in Money and just call the external function from render:

render() {
const currency = getCurrencyData(this.props.currency)
...
}

Now let's write somet tests for getCurrencyData:

import { getCurrencyData } from './currency-data';

test('for GBP it returns the right data', () => {
expect(getCurrencyData('GBP')).toEqual({
base: 100,
symbol: '£',
});
});

For the sake of this tutorial - and also due to the data being simplified - I'll leave it there for tests for this function, but in a more complex situation we would write a full suite of tests as required.

Slimming down the money component

Now, with everything passing, take a look at the Money implementation:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { formatAmount } from './format-currency';
import { getCurrencyData } from './currency-data';

class Money extends Component {
static propTypes = {
currency: PropTypes.string.isRequired,
amount: PropTypes.number.isRequired,
};

render() {
const currency = getCurrencyData(this.props.currency);
if (currency) {
const { symbol, base } = currency;
const formatted = formatAmount(this.props.amount, base);

return (
<span>
{symbol}
{formatted}
</span>
);
} else {
return <span>{this.props.amount}</span>;
}
}
}

export default Money;

Money has now just a single method, render, implemented. This is a great chance to move Money to a functional, stateless component (FSC). If you are not familiar with the how, whats and whys of FSCs, you can read a previous blog post on the subject. I can now rewrite Money in this way:

import React from 'react';
import PropTypes from 'prop-types';
import { formatAmount } from './format-currency';
import { getCurrencyData } from './currency-data';

const Money = ({ currency, amount }) => {
const currencyData = getCurrencyData(currency);
if (currencyData) {
const { symbol, base } = currencyData;
const formatted = formatAmount(amount, base);

return (
<span>
{symbol}
{formatted}
</span>
);
} else {
return <span>{amount}</span>;
}
};

Money.propTypes = {
currency: PropTypes.string.isRequired,
amount: PropTypes.number.isRequired,
};

export default Money;

I am a huge fan of FSCs; they encourage simple components and the separation of logic from UI, and it's no coincidence that by doing this refactoring today we've come to realise that our Money component can be written in this way.

Conclusion

By looking through our components and finding standalone functions that we can pull out, we've greatly simplified our component whilst increasing our test coverage and clarity of our application greatly. I highly encourage you to think twice about adding arbitrary methods onto React components; it's too easy to refer to this.props.X.

By pulling the functions into their own modules you are forced to consider which props are needed and how your function will work. It makes code clearer, it's easier to see which props are used where and it means as your business logic gets more complex you can test it without having to get your UI components involved.

If you'd like to play with the code yourself, it's all on GitHub. Feel free to raise an issue if you have any questions.