Sharing Component Props Using Type Information¶
Components and subcomponents are the zen of React. Information is shared from parent to child using properties, and TypeScript helps us formalize that relationship.
In this tutorial step, we make our child component reusable by passing the value that should be displayed in the greeting. Along the way, we formalize the parent/child component interface with…an interface, of course.
We’ll start from the ending of
the previous step. Remember, we’re
doing TDD, so let’s have Heading.tsx
and Heading.test.tsx
open
side-by-side, with the Jest run configuration running.
Hello Recipient¶
We’re going to change the Heading
component to accept the name of the
recipient to say hello to. This value will come in as a prop. For example,
<Heading recipient={'World'}/>
.
As usual, let’s start in our tests. In Heading.test.tsx
, change our wrapper
construction to the following:
const wrapper = shallow(<Heading recipient={'World'}/>);
Our tests still pass but the IDE tells us TypeScript doesn’t compile:
Error:(6, 38) TS2559: Type '{ recipient: string; }' has no properties in
common with type 'IntrinsicAttributes & { children?: ReactNode; }'.
Our test provided an object { recipient: string; }
as props but the
component’s TypeScript definition didn’t accept that. Let’s change the props to
allow recipient
as a string. In Heading.tsx
:
const Heading: React.SFC<{recipient: string}> = () => <h1>Hello React</h1>;
The React.SFC
type is a generic which accepts props as the first
argument and optional state as the second. We defined the type information
for our props inline and the error went away: TypeScript now knows that a
recipient
string is a required argument.
Doing type information inline is clunky. Let’s use a TypeScript interface to define our type information:
interface IHeadingProps {
recipient: string
}
const Heading: React.SFC<IHeadingProps> = () => <h1>Hello React</h1>;
One useful tip: the IDE can do the extraction for you. Put the cursor in the
the {recipient: string}
object and do Ctrl-T | Interface
then type
in the name.
Note
The React TypeScript community has gone back and forth on I
as a prefix. At the time of this writing, TSLint had added a
default rule that warns if an interface name fails to start
with an I
.
Our component isn’t using this prop. The most obvious solution: grab the
props
:
const Heading: React.SFC<IHeadingProps> = (props) =>
<h1>Hello {props.recipient}</h1>;
Good news, our tests fail, as expected! Let’s fix just the test in
Heading.test.tsx
by having it expect the value toBe('Hello World')
.
When that test is updated, that test will pass. We’ll get to the failing
App.test.tsx
tests in a moment.
It can be cumbersome to type props.
in front of every prop. ES6 has some
called object destructuring which lets you “unpack” an object and bring into
scope just the value you want. As a side benefit, it makes it clear at the
entry point what that arrow function wants.
Let’s switch to object destructuring, and since our line is getting long, use a block:
const Heading: React.SFC<IHeadingProps> = ({recipient}) => {
return <h1>Hello {recipient}</h1>;
}
Note that, as you were typing inside ({})
, the IDE knew what were the
possible completions. This is from the TypeScript interface on the props.
Default Prop¶
We can shut up the the App.test.tsx
tests by having a default recipient.
We’ll use ES6 object destructuring’s syntax for setting a value when the
destructured object doesn’t have that key:
const Heading: React.SFC<IHeadingProps> = ({recipient = 'React'}) => {
return <h1>Hello {recipient}</h1>;
}
Yay, all our tests pass! But if you revisit App.tsx
you’ll see that
TypeScript isn’t happy about <Heading/>
:
Type '{}' is not assignable to type 'IHeadingProps'.
Property 'recipient' is missing in type '{}'.
That defeats the purpose of a default value. Good news: TypeScript thought of
that and lets you mark an interface field as optional using a question mark.
Back in Heading.tsx
:
interface IHeadingProps {
recipient?: string
}
Our tests pass and TypeScript is happy. But we forgot to write a test for
the default value. Let’s add this to Heading.test.tsx
:
it('renders the default heading', () => {
const wrapper = shallow(<Heading/>);
expect(wrapper.find('h1').text())
.toBe('Hello React');
});
We now have a child component that is passed in an optional value, with a default, and an enforceable contract saying it must be a string. We did all of this with simple idioms from TypeScript and ES6.
And guess what? We never looked at the browser. If you’d like, first up the
start
run configuration and take a look at the browser to confirm it’s
still working. Make sure to turn off start
when done.
Note
The use of SFCs is encouraged, especially for leaf nodes with no state. But beware: putting them in a listing with thousands of items can be a performance killer, as each function is recreated on every render, which might be 60 times per second.