React, TypeScript, and TDD Part 2
Tags: react, typescript, testing
Published: 2021-06-09 17:24:58

React component development is kinda fun. What's even...uhhh...funner? Driving your component development from inside a *test*.

No, I'm serious.

As we saw in the [previous article introducing this](https://dev.to/pauleveritt/react-typescript-and-tdd-1ne7), React+TDD isn't just about "quality" (scare quotes) and eating your vegetables. Particularly when paired with TypeScript and smart tooling, it's a pleasurable mode of development -- faster, joyful, puppies.

Let's get more specific in this article by going through some of the modes of component development. As a reminder, this article follows a [full video+text+code tutorial](https://www.jetbrains.com/webstorm/guide/tutorials/react_typescript_tdd/) in the [WebStorm Guide](https://www.jetbrains.com/webstorm/guide/).

## TSX and ES6

Using React and TypeScript means good JSX (TSX) and ES6+ support, especially in smart editors. We can see this in action from the [tutorial step](https://www.jetbrains.com/webstorm/guide/tutorials/react_typescript_tdd/tsx_es6/) on this subject.

Imagine we have some React code:

```typescript
import React from "react";

function App() {
    return (
        <div>
            <h1>Hello React</h1>
        </div>
    );
}

export default App;
```

...and the test that goes with it:

```typescript
import React from "react";
import {render} from "@testing-library/react";
import App from "./App";

test("renders hello react", () => {
    const {getByText} = render(<App/>);
    const linkElement = getByText(/hello react/i);
    expect(linkElement).toBeInTheDocument();
});
```

We can then run some tests. Here we see the Jest test runner integrated into a "smart editor", in this case, [WebStorm](https://www.jetbrains.com/webstorm/):

![01](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u0l9l82ghdof2taptx45.png)

Let's do some TDD and show some ES6 features along the way.

### Extracted Heading

What's something you do All The Time(tm) in React? Decompose big components into smaller components. Let's extract a `Heading` component from this `App` component, starting with a *new* test. One that fails, of course:

```typescript
test("renders heading", () => {
  const { getByText } = render(<Heading />);
  const linkElement = getByText(/hello react/i);
  expect(linkElement).toBeInTheDocument();
});
```

We can't even import our component because...it doesn't exist. Let's now write our first attempt at an extracted `Heading` component:

```typescript
import React from "react";

export function Heading() {
    return <h1>Hello React</h1>;
}
```

When our test adds the import of `Heading`, the new test will then pass:

![02](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9za3wwg2zite28kq2ih9.png)

Of course, extracting a component into the same file somewhat violates the React community's adherence to "one component per file." Let's move our component to its own `Heading.tsx` file:

```typescript
export function Heading() {
  return <h1>Hello React</h1>;
}
```

...with a companion `Heading.test.tsx`:

```typescript
import React from "react";
import {render} from "@testing-library/react";
import {Heading} from "./Heading";

test("renders heading", () => {
    const {getByText} = render(<Heading/>);
    const linkElement = getByText(/hello react/i);
    expect(linkElement).toBeInTheDocument();
});
```

When we run the test in this file, it passes again:

![03](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ujwosz5l54suzbfxa1kt.png)

We need to change our `App.tsx` to import this `Heading` component and use it:

```typescript
import React from "react";
import {Heading} from "./Heading";

function App() {
    return (
        <div>
            <Heading/>
        </div>
    );
}

export default App;
```

Our test in `App.test.tsx` still passes -- it doesn't really know that `Hello React` came from a subcomponent.

We can now show some testing of parent and child components.

### Props and Types

That is a *boring* component. It says the same thing every time! Let's change it so "parent" components can pass in a value for the "name" to say hello to. 

We first write a (failing) first test in `Heading.test.tsx`:

```typescript
test("renders heading with argument", () => {
  const { getByText } = render(<Heading name={`World`}/>);
  const linkElement = getByText(/hello world/i);
  expect(linkElement).toBeInTheDocument();
});
```

Thanks to TypeScript and tooling, we "failed faster": it immediately told us, with a red-squiggly, that we violated the contract. `Heading` doesn't (yet) take a name prop:

![04](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v1m1af8gv9hh9v4d2cxs.png)

Let's head to the Heading component and fix it:

```typescript
export function Heading({ name }) {
  return <h1>Hello {name}</h1>;
}
```

Our *new* test passes. The previous test is broken -- no `name` was passed in. We'll handle that in a bit.

What's up with the `{name}` as the function argument? This is ES6 [object destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), a cool way to pick out the values you want from an argument.

Our test passes but TypeScript is unhappy:

![05](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bsb4p9d6do127c03rh9s.png)

We don't have any type information on the props. We could add the type information inline:

```typescript
export function Heading({ name }: {name: string}) {
  return <h1>Hello {name}</h1>;
}
```

It is better, though, to put this in a standalone `type` or `interface`, then use that in the function arguments:

```typescript
type HeadingProps = { name: string };

export function Heading({ name }: HeadingProps) {
  return <h1>Hello {name}</h1>;
}
```

Let's now look at fixing the first test.

## Default Prop Value

We want `Heading` to accept a `name` prop but not require it. Sounds like a change to the `type` definition, marking `name` as an optional field:

```typescript
type HeadingProps = { name?: string };
```

We can then use another ES6 feature -- default values in object destructuring:

```typescript
export function Heading({name = "React"}: HeadingProps) {
    return <h1>Hello {name}</h1>;
}
```

With this, `Heading` will use `React` as the prop value if the calling component doesn't provide it. Our first test in `Heading.test.tsx` now passes.

You know who *else* doesn't provide that prop? Our `App` component. And guess what -- our tests in `App.test.tsx` now pass again:

![06](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3gcr0vgth68u1ksnow4t.png)

At each step during development of this, we "failed faster" thanks to TypeScript and test-first. Even better -- we have yet to look at the browser. We stayed "in the flow".

## Class Components With Props

The React community has become very enthusiastic about functional programming and pure, function-based components. But the class-based component syntax is still there for all the old die-hards. (Narrator: He means himself.)

Let's make a new `Counter` component, written as a class-based component which takes a single prop. We'll be following along the [tutorial step](https://www.jetbrains.com/webstorm/guide/tutorials/react_typescript_tdd/class_props/) that matches this section. In the next section, we'll introduce state into the class.

Of course, we'll start with a *failing* `Counter.test.tsx` test which uses Testing Library's [getByTestId](https://testing-library.com/docs/queries/bytestid) query:

```typescript
import React from "react";
import {render} from "@testing-library/react";
import {Counter} from "./Counter";

test("should render a label and counter", () => {
    const {getByTestId} = render(<Counter/>);
    const label = getByTestId("counter-label");
    expect(label).toBeInTheDocument();
    const counter = getByTestId("counter");
    expect(counter).toBeInTheDocument();
});
```

We create a new `Counter.tsx` file:

```typescript
import React, {Component} from "react";

export class Counter extends Component {
    render() {
        return (
            <div>
                <div data-testid="counter-label">Count</div>
                <div data-testid="counter">
          1
        </div>
            </div>
        );
    }
}
```

Our test passes. But it's boring: we want the label displayed next to the count to be configurable, passed in by the parent as a prop. Here's the (failing) test:

```typescript
test("should render a counter with custom label", () => {
    const {getByTestId} = render(<Counter label={`Current`}/>);
    const label = getByTestId("counter-label");
    expect(label).toBeInTheDocument();
});
```

This failed even before I ran the test, as TypeScript told us we broke the contract:

![07](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l5qds4yfh9kh8omp38zd.png)

Back in the implementation, we need two things: a `type` definition for the props then a changed class which uses the prop:

```typescript
import React, {Component} from "react";

export type CounterProps = { label?: string };

export class Counter extends Component<CounterProps> {
    render() {
        const {label = "Count"} = this.props;
        return (
            <div>
                <div data-testid="counter-label">{label}</div>
                <div data-testid="counter">
                    1
                </div>
            </div>
        );
    }
}
```

Our `Counter` tests now pass. We have a class-based `Counter` component which accepts a prop.

## Class Components With State

"Yay, us" in a way, but the `Counter` doesn't...count. Let's make a *stateful* class-based component. This section matches the [tutorial step for Class Components With State](https://www.jetbrains.com/webstorm/guide/tutorials/react_typescript_tdd/class_state/).

What's the first step? Hint: it rhymes with "best". That's right, let's start with a failing *test* in `Counter.test.tsx`:

```typescript
test("should start at zero", () => {
    const {getByTestId} = render(<Counter/>);
    const counter = getByTestId("counter");
    expect(counter).toHaveTextContent("0");
});
```

Now on to the implementation. When we did the component prop, we wrote a TypeScript `type` to model the prop shape. Same for the state:

```typescript
export type CounterState = { count: number };
```

We then change our `Counter` class to point to and implement that state:

```typescript
export class Counter extends Component<CounterProps, CounterState> {
    state: CounterState = {
        count: 0,
    };

    render() {
        const {label = "Count"} = this.props;
        return (
            <div>
                <div data-testid="counter-label">{label}</div>
                <div data-testid="counter">
                    {this.state.count}
                </div>
            </div>
        );
    }
}
```

Our test passes. The value of the state is done as a class variable, which then meant we got autocomplete on `this.state.count`. But if we try to do an assignment, we know that React will complain that we didn't use `setState`.

Fortunately this is something TypeScript can help with. Let's move the initialization of the state to the module scope, then change the type definition:

```typescript
const initialState = {count: 0};
export type CounterState = Readonly<typeof initialState>;
```

Our class now points at this initial state:

```typescript
export class Counter extends Component<CounterProps, CounterState> {
    readonly state: CounterState = initialState;

    render() {
        const {label = "Count"} = this.props;
        return (
            <div>
                <div data-testid="counter-label">{label}</div>
                <div data-testid="counter">
                    {this.state.count}
                </div>
            </div>
        );
    }
}
```

Our test still passes. Again, this is what's nice about test-driven development: you can make changes with confidence, while staying in the tool.

Let's make a change to allow the starting value of the counter to passed in as a prop. First, a failing test:

```typescript
test("should start at another value", () => {
    const {getByTestId} = render(<Counter/>);
    const counter = getByTestId("counter");
    expect(counter).toHaveTextContent("10");
});
```

Not only does the test fail, but TypeScript yells at us about the contract, even before the test is run:

![08](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qs47pwzuv11toihxky2t.png)


We need to change the type definition for our props:

```typescript
export type CounterProps = {
    label?: string;
    start?: number;
};
```

With this in place, we can make a call to `setState` to update the value. We will do it in a lifecycle method:

```typescript
componentDidMount() {
    if (this.props.start) {
      this.setState({
        count: this.props.start,
      });
    }
  }
```

Our test now passes. The counter has a default starting count, but can accept a new one passed in as a prop.

## Conclusion

We covered a lot in these three steps: the use of ES6 niceties, type definitions for props and state, and the use of class-based components. All without visiting a browser.

In the third and final installment, we will wire up event handlers and refactor into smarter parent/child components. We'll do both in a way that let both TypeScript and testing help us "fail faster."