=====================================
Presentation and Container Components
=====================================
React encourages a separation of concerns. UI components, aka presentation
components, aka dumb components, are managed by
container components, aka smart components. The container maintains state,
logic, and passes things into the presentation component.
Let's make our Counter a presentation component by moving the state up to
he container (App) and the incrementing logic as well.
Counter State
=============
We'll start by removing state from the counter. Instead, the count is
passed in as a prop. Also, the dumb child component will no longer decide
the starting value, so remove ``start`` from the interface:
.. code-block:: typescript
:emphasize-lines: 4
interface ICounterProps {
label?: string
count: number
}
As soon as we do that, the universe starts breaking. TypeScript yells at us
in every one of our tests, as our ```` component is not passing in
a required prop.
Next, let's change our ``Counter``
component to not have local state. We mentioned in
:doc:`../functional_components/index` that stateless presentation components
are best done with stateless functional components. Let's change
```` to an SFC::
const Counter: React.SFC = (
{label = 'Count', count}
) => {
return (
{count}
)
}
Note that we commented out, for now, the click handler. We can also delete
the ``ICounterState`` interface as it is no longer needed.
Let's fix the first two tests, to see if we are in the ballpark:
.. code-block:: typescript
:emphasize-lines: 2, 8
it('should render a counter', () => {
const wrapper = shallow();
expect(wrapper.find('.counter label').text())
.toBe('Count');
});
it('should render a counter with custom label', () => {
const wrapper = shallow();
expect(wrapper.find('.counter label').text())
.toBe('Current');
});
These two tests no longer have TypeScript complaints.
Since the ```` component no longer controls the starting value,
you can remove the
``should default start at zero`` and
``should custom start at another value`` tests from ``Counter.test.tsx``.
Passing In Click Function
=========================
The child component is no longer responsible for the count value. It's passed
in from the parent, which keeps track of the state. So how do we handle
clicks?
It sounds weird, but...in the same way. We're going to pass in an arrow
function from the parent. Meaning, the parent contains all the logic for what
happens when there is a click. All the child needs to know is "when the click
event comes in, call the function that was passed to me as a prop".
Here goes. First, since this click handler function will come in as a prop,
we need to change ``ICounterProps`` to model it:
.. code-block:: typescript
:emphasize-lines: 4
interface ICounterProps {
label?: string
count: number
onCounterIncrease: (event: React.MouseEvent) => void
}
Now *that's* an interface, baby. It captures quite a bit of the contract.
Next, use ES6 object destructuring to "unpack" that from the props into the
local scope, then refer to that prop in the ``onClick`` handler::
const Counter: React.SFC = (
{label = 'Count', count, onCounterIncrease}
) => {
return (
{count}
)
}
Note that the IDE, as you did the unpacking, knew how to autocomplete
``onCounterIncrease``.
Our tests, though, are having compiler trouble again. We broke the component
contract, because ``onCounterIncrease`` is a mandatory prop. It's easy to
shut up this test, because we aren't testing click handling:
.. code-block:: typescript
const handler = jest.fn();
const wrapper = shallow();
We used *Jest* mock functions to create a disposable arrow
function which we passed in as a prop.
Do this for both tests:
.. code-block:: typescript
it('should render a counter', () => {
const handler = jest.fn();
const wrapper = shallow();
expect(wrapper.find('.counter label').text())
.toBe('Count');
});
it('should render a counter with custom label', () => {
const handler = jest.fn();
const wrapper = shallow();
expect(wrapper.find('.counter label').text())
.toBe('Current');
});
Event handling is a bit trickier. We need a "spy" that tells whether our
passed-in handler gets called, and called the right way. Also, we don't
test whether the value updates, since the container is responsible for
that.
Let's change the third test:
.. code-block:: typescript
it('should call the handler on click', () => {
const handler = jest.fn();
const wrapper = shallow();
wrapper.find('.counter').simulate('click', {shiftKey: false});
expect(handler).toBeCalledWith({shiftKey: false});
});
We're simply ensuring that clicking the value calls the callback. We could
delete the last test, as it isn't the responsibility of the ````
to handle the click. All the logic is in container, not the presentation
component.
Dumb Component Gets a Little Smarter
====================================
But is that strictly true? What if the presentation component took care of
dissecting HTML event information, extracted the relevant data, and *then*
called the callback? That's a better division of responsibilities. The
container would then be truly UI-less for this functionality.
First, let's change the contract. Our callback will be called *not* with the
raw event, but with a boolean for the shift information:
.. code-block:: typescript
interface ICounterProps {
label?: string
count: number
onCounterIncrease: (isShift: boolean) => void
}
Our SFC gains a local arrow function which does the extraction and calling::
const Counter: React.SFC = (
{label = 'Count', count, onCounterIncrease}
) => {
const handleClick = (event: React.MouseEvent) => {
onCounterIncrease(event.shiftKey);
};
return (
{count}
)
}
Our third test can now change, to see if our "spy" was called with a boolean
instead of an event object:
.. code-block:: typescript
:emphasize-lines: 5
it('should call the handler on click', () => {
const handler = jest.fn();
const wrapper = shallow();
wrapper.find('.counter').simulate('click', {shiftKey: false});
expect(handler).toBeCalledWith(false);
});
Updating the Container
======================
We now have a ```` presentation component that passes tests. But
we've shifted some responsibility to the parent. Let's do the updates. Start
by opening ``App.tsx`` and ``App.test.tsx`` side-by-side.
First, this ```` component will now have some state. Make an interface
for it:
.. code-block:: typescript
interface ICounterState {
count: number
}
Change the class setup to use this, with a constructor that sets up the
initial state::
class App extends React.Component