Class Components With Props

In the previous step we made a “dumb” presentational component with one property. In React, components can have properties and state. When they have state (or when they need access to React’s lifecycle methods), we use a class-based component instead.

We’re going to build a stateful counter component. Each click increments the click count, with the current count value stored in local component state. The <Counter/> component will be passed in some props.

In this step, we’ll show class component props – as done with a function in the previous step – for class-based components. We’ll do state in the next step.

First a Test

This tutorial series shows component development using testing instead of a browser. Let’s write a broken test first, then do the implementation which fixes the test.

Make a new file called Counter.test.tsx with this test:

it('should render a counter', () => {
    const wrapper = shallow(<Counter/>);
    expect(wrapper.find('.counter label').text())
        .toBe('Count');
});

It has several failures. For now, just click on shallow and hit Alt-Enter to generate that import.

Now create a file Counter.tsx. We’ll make it very simple to start:

import * as React from 'react';

class Counter extends React.Component {
    public render() {
        return (
            <div className="counter">
                <label>Count</label>
                <span>1</span>
            </div>
        );
    }
}

export default Counter;

Back in our test, click on <Counter/> and hit Alt-Enter. Your import is generated, but there’s still an error: React isn’t imported. Repeat the Alt-Enter to generate the React import. Save the file and see that your test passes.

Not a bad first step.

Pass In a Prop

As we did in the previous section, we’ll do our work first in the test. We’ll write a failing test, then fix our “wrapper” component, then fix the actual implementation. Also, we’ll presume that the component has a default label.

Thus, let’s add a test for the case of passing in a label:

it('should render a counter with custom label', () => {
    const wrapper = shallow(<Counter label={'Current'}/>);
    expect(wrapper.find('.counter label').text())
        .toBe('Current');
});

The test fails, which is good. What’s even better – TypeScript helped us “fail faster”. Before running the test, it told us we broke the contract saying no properties were expected. Even better, our IDE visually warned us with a very specific mouseover message.

Let’s now work on the implementation. Classes handle props with defaults a little differently:

class Counter extends React.Component<{ label?: string }> {
    public static defaultProps = {
        label: 'Count'
    };

Remember the ? means an optional field in the interface. Now make the <label> dynamic:

<label>{this.props.label}</label>

When you save Counter.tsx, your tests will now pass.

As we saw in the previous step, it’s nicer to put the props type information into its own interface. Let’s extract that into ICounterProps:

interface ICounterProps {
    label?: string;
}

class Counter extends React.Component<ICounterProps> {
    public static defaultProps = {
        label: 'Count'
    };

Wire Into UI

We have a <Counter/> prop that takes an optional label. Tests pass. Let’s now use it in our app and view it in the browser.

Open App.tsx and change the TSX that is returned:

public render() {
    return (
        <div>
            <Heading/>
            <Counter label={'Current'}/>
        </div>
    );
}

Did you notice the autocompletion by the IDE, which knew there was a component with a name starting with those letters, somewhere in the project? And when you accepted the completion, it generated the import? Also, the IDE helped on the available props and the types for those props.

All of our tests still pass. Let’s change the renders the app and the heading test in``App.test.tsx`` to look for the label in the new <Counter/> child component:

it('renders the app and the heading', () => {
    const wrapper = mount(<App/>);
    expect(wrapper.find('h1').text())
        .toBe('Hello React');
    expect(wrapper.find('.counter label').text())
        .toBe('Current');
});

Let’s restart the start script and look at the UI in the browser. We should now see Current 1 in the UI.

While this step didn’t do too much that was new – after all, we had optional props and interfaces in the previous step, with functions – it paves the way for stateful components.