Writing fully testable React components using the 'Helper Pattern'

siddiqus

Sabbir Siddiqui

Posted on December 14, 2019

Writing fully testable React components using the 'Helper Pattern'

I am still learning about React Hooks and Redux (I know, I need to catch up quick), and I know writing basic React classes is pretty much dated at this point, but I wanted to share a little tidbit that I found to be useful over the years.

The problem

Consider a CRUD application for products, where a user can view a list of products (paginated), and create, modify, or delete a product. The component class would look something like this

class ProductsPage extends React.Component {
    constructor(props) {
        super(props);
        this.state = {}; // state may include product list, product form variables, loadingStatus, etc.
        this.productService = new ProductService(); // some product service to manage products

        this.onAddButtonClicked = this.onAddButtonClicked.bind(this);
        this.onDeleteButtonClicked = this.onDeleteButtonClicked.bind(this);
        this.onUpdateButtonClicked = this.onUpdateButtonClicked.bind(this);
    }

    componentDidMount() { } // some code to fetch data for list view

    onAddButtonClicked() { }
    onDeleteButtonClicked() { }
    onUpdateButtonClicked() { }

    _renderPageHeaderWithCreateButton() { }
    _renderProductTable() { }
    _renderProductModal() { }
    _renderLoadingModal() { }
    _renderErrorAlert() { }

    render() {
        return (
            <React.Fragment>
                {this.state.error && this._renderErrorAlert()}
                {this._renderPageHeaderWithCreateButton()}
                {this._renderProductTable()}
                {this._renderProductModal()}
                {this._renderLoadingModal()}
            </React.Fragment>
        )
    }
}

This is usually how I like to organize my React classes. Apart from the usual functions for button clicks, I also like to split my render function into smaller chunks if it starts to get bigger, and then later decide to split this into separate components as needed. A user might see a simple list but there's a lot going on in this component.

After mounting, the class needs to set a 'loading' state, and then fetch data from a server using the 'ProductService', and if the call is successful, set the data to a 'productList' state variable, or otherwise handle errors. Then if a user wants to create or modify a product, you have to manage the state for a modal along with form variables. All in all, we end up with a lot of state variables and button actions to manage.

Apart from splitting this up into smaller components and having to pass down state and actions, could we make this one component less bulky and the state easier to manage? Also think about unit testing. Jest provides the tools to test React components, but do we really need those to test our logic? I tried using the Logic/View pattern before where there would be one React component for the view and one for managing all the logic e.g. 'ProductsPage' and 'ProductsPageView'. That seemed great at first, but the logic was still contained in a React component that didn't necessarily need to be. So I thought about flipping this pattern on it's head. Could I have a view class where I delegate managing all the logic to another class, but one that wasn't a React component? Yes I could!

The solution: The Helper Pattern

The idea was simple - each React component would have a Helper class that would manage all the logic for that component.

class ProductsPage extends React.Component {
    constructor(props) {
        super(props);

        this.helper = new ProductsPageHelper(this); // pay attention
        this.state = {}; // some state vars

        this.onAddButtonClicked = this.onAddButtonClicked.bind(this);
        this.onDeleteButtonClicked = this.onDeleteButtonClicked.bind(this);
        this.onUpdateButtonClicked = this.onUpdateButtonClicked.bind(this);
    }

    async onAddButtonClicked() {
        this.setState({
            loading: true
        });
        const newState = this.helper.addProduct();
        this.setState(newState);
    }

    // ... other stuff
}

If you notice, the helper class is initialized with 'this'. Why would we do this? (pun intended) We would have access to all of the React component's props and state variables and can manage the logic from there. Take a look at the new 'onAddButtonClicked' method, where most of the logic is taken away in the helper. Here is an example of the helper.

class ProductsPageHelper {
    constructor(component) {
        this.component = component; // our React component
        this.productService = new ProductService(); // this is now removed from the React class
    }

    async addProduct() {
        // some logic to add a product using the product service
        // returns new state e.g. new product list or errors
    }

    // ... other functions
}

Okay, great. We have some separation of logic from the React component, and most of the logic is now in a 'helper' class which is a regular Javascript class. Could we do better? The answer is yes! Why manage the state in two different places where you could manage the state in one? Finally after a few more iterations, this is what I came up with.

class ProductsPage extends React.Component {
    constructor(props) {
        super(props);

        this.productsPageHelper = new ProductsPageHelper(this);
        this.state = this.productsPageHelper.getInitialState(); // init state variables

        this.onAddButtonClicked = this.onAddButtonClicked.bind(this);
        this.onDeleteButtonClicked = this.onDeleteButtonClicked.bind(this);
        this.onUpdateButtonClicked = this.onUpdateButtonClicked.bind(this);
    }

    componentDidMount() {
        this.helper.getProducts(); // state fully managed here
    }

    onAddButtonClicked() {
        this.helper.addProduct(); // state fully managed here
    }

    onDeleteButtonClicked(product) {
        this.helper.deleteProduct(product); // state fully managed here
    }

    onUpdateButtonClicked(product) { 
        this.helper.updateProduct(product); // state fully managed here
    }

    // ...render functions
}

Notice:

  1. I initialized the state from the helper method 'getInitialState', so the developer working on the helper class knows what state variables the component has without actually looking at the view component.
  2. All the state is now fully managed from the helper class

You could reduce the React component code even further by getting rid of the event functions and the 'bind' code, by using the arrow function syntax in JSX. For example:

// for a product list view
{
    this.state.productList.map((product) => {
        return (
            <Row>
                {/* {some code for product row} */}
                <button onClick={() => this.helper.deleteProduct(product)}>
                    Delete
                </button>
            </Row>
        )
    });
}

Here is the helper class now:

class ProductsPageHelper {
    constructor(component) {
        this.component = component; // our React component

        this.productService = new ProductService(); // this is now removed from the React class
    }

    _updateState(state){
        this.component.setState(state);
    }

    getInitialState() {
        return {
            loading: false,
            productList: [],
            error: false,
            errorMessage: "",
            productFormVars: {},
            productModalIsShown: false
        }
    }

    _handleGetProductsSuccess(productList){
        this._updateState({
            loading: false,
            error: false,
            productList
        });
    }

    _handleGetProductsError(error) {
        // some error handling
        this._updateState({
            loading: false,
            error: true,
            errorMessage: "some error message"
        })
    }

    async getProducts() {
        this.component.setState({
            loading: true
        });

        try {
            const productList = await this.productService.getProducts();
            this._handleGetProductsSuccess(productList);
        } catch (error) {
            this._handleGetProductsError(error);
        }
    }

    // ... other functions
}

Woohoo! As you can see, the state can be accessed/managed just by using this.component.state and this.component.setState. Now since the helper is just any other Javascript class, we can easily get full test coverage on this. For example, to test the logic for 'componentDidMount':

describe("ProductsPageHelperTest", () => {
    it("Should get products and set state properly", async () => {
        const mockComponent = {
            setState: jest.fn()
        };

        const helper = new ProductsPageHelper(mockComponent);
        const mockProductList = [1, 2, 3];
        helper.productService = {
            getProducts: jest.fn().mockResolvedValue(mockProductList)
        }

        await helper.getProducts();

        expect(mockComponent.setState).toHaveBeenCalledWith({
            loading: true
        });
        expect(helper.productService.getProducts).toHaveBeenCalled();
        expect(mockComponent.setState).toHaveBeenCalledWith({
            loading: false,
            error: false,
            productList: mockProductList
        });
    });
});

We can just pass a 'mockComponent' object with an initial state and the 'setState' stubbed function to fully test the state change behavior. Testing the React component also became easier since all the logic was driven by the Helper class, you could write tests by stubbing the helper methods and checking to see if those were called when appropriate.

Outcomes

What was the benefit from doing all of this?

  1. Leaner component classes - The previously bulky React classes were now much leaner and easier to go through at a glance.
  2. Code consistency - The pattern helped to make development easier for everyone on the team as all the components' states were managed in the same way, so team members knew what to expect.
  3. Improved productivity and collaboration - You could have multiple team members working in parallel on the same component where one person would work on the view while another person could work on the logic. A backend engineer with some Javascript experience could work on the helper class, as long as the methods / contracts were defined. This made the team more cross functional.
  4. Code coverage - Before using this pattern, team members avoided writing unit tests since using Enzyme was painful when it came to logic contained within the React components. Code coverage went from 30% to over 80% after using the Helper pattern, since now all of the logic could be tested.

Now it's time to leave this behind and move forward into the world of Redux and Hooks! :)

💖 💪 🙅 🚩
siddiqus
Sabbir Siddiqui

Posted on December 14, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related