============================================
Model Class State With TypeScript Interfaces
============================================
In :doc:`../class_props/index` we made a child component using a class,
with one property passed in. We use classes for child components when they
have state or need hooking into one of React's lifecycle methods.
That's the topic of this step. We're going to have a reusable counter
component that has a count of clicks.
This step, though, will be just the minimum: no actual clicking to update
state, for example. We will stick to introducing component state and
modeling it in TypeScript.
Always Start With a Test
========================
It's becoming our pattern: we write a failing test first, then implement,
then wire into the parent. To begin, have ``Counter.tsx`` in the left-hand
tab and ``Counter.test.tsx`` in the right-hand tab. Also, stop the
``start`` process if it is running and make sure the ``Jest`` run config is
running.
Here's a ``Counter.test.tsx`` test to show that the counter starts at zero,
which fails, because we have a static ``1``:
.. code-block:: typescript
it('should default start at zero', () => {
const wrapper = shallow();
expect(wrapper.find('.counter span').text())
.toBe('1');
});
Over in ``Counter.tsx``, let's write our interface first. What does the
local state look like? Pretty easy:
.. code-block:: typescript
interface ICounterState {
count: number
}
Now the class definition and constructor can setup state, which we'll use
in the ``render`` method::
class Counter extends React.Component {
public static defaultProps = {
label: 'Count'
};
constructor(props: ICounterProps) {
super(props);
this.state = {
count: 0
};
}
public render() {
return (
{this.state.count}
);
}
}
Several things changed in this:
- ``React.Component<>`` has a generic with a second value, for the state
- We added a class constructor, which per the TSLint style, comes after
static methods
- This constructor is passed the props (which we'll use in a moment)
- The constructor *must* call the superclass's constructor
- We assign some local state
- In the JSX/TSX, we got autocompletion not only on ``.state``, but also
``.count``
Starting Value
==============
Sometimes we want a counter that starts somewhere besides zero. Let's pass
in an optional prop for the starting value. First, the test in
``Counter.test.tsx``:
.. code-block:: typescript
it('should custom start at another value', () => {
const wrapper = shallow();
expect(wrapper.find('.counter span').text())
.toBe('10');
});
As before, our test fails, but before that, our IDE warns us that we have
violated the ```` contract. We'll fix the interface in
``Counter.tsx``:
.. code-block:: typescript
:emphasize-lines: 3
interface ICounterProps {
label?: string
start?: number
}
Then, add it to the ``defaultProps``:
.. code-block:: typescript
:emphasize-lines: 3
public static defaultProps = {
label: 'Count',
start: 0
};
Finally, change the component *state* to get its initial value from the
component *props*:
.. code-block:: typescript
:emphasize-lines: 4
constructor(props: ICounterProps) {
super(props);
this.state = {
count: props.start
};
}
When we do this, though, TypeScript gets mad. We said the ``start``
property was optional, by putting a ``?`` in the interface field. As the
compiler error explains, this means it can be a ``number`` *or* a
``null``. In the component *state*, though, we say it can only be a
``number``.
`TypeScript 2.7 `_
provides an elegant fix for this with *definite assignment assertion*.
Sometimes you know better than the compiler. At the point of assignment,
make an "I'm sure" assignment -- a *definite* assignment -- by suffixing the
value with an exclamation:
.. code-block:: typescript
:emphasize-lines: 4
constructor(props: ICounterProps) {
super(props);
this.state = {
count: props.start!
};
}
Not only is the compiler happy, but our test is happy. We have a
```` component which shows a value from local component state and
which can optionally be passed in a starting value.
.. note::
We could also have solved the definite assignment issue using a
ternary. TypeScript knows how to infer the type from such
"control flow."
Wire Into UI
============
We wrap up each step by wiring the standalone component changes into the
parent component, first through testing, then by looking in the browser.
First up, we open ``App.test.tsx`` and add a single line to test the
initial counter value:
.. code-block:: typescript
:emphasize-lines: 7-8
it('renders the app and the heading', () => {
const wrapper = mount();
expect(wrapper.find('h1').text())
.toBe('Hello React');
expect(wrapper.find('.counter label').text())
.toBe('Current');
expect(wrapper.find('.counter span').text())
.toBe('0');
});
What changes in ``App.tsx``? In this case, nothing. We want to use the default
value of zero.
If you'd like, restart the ``start`` run configuration and view this in the
browser, so make sure everything still looks good. When done, terminate the
``start`` script.