const React = require("react"); const ReactDOM = require("react-dom");
React applications are composed of nested components. As React-based applications grow, these component trees and the dependencies between them become increasingly complex.
Flow’s static analysis makes building large Web apps with React safe by tracking the types of props and state. Flow understands which props are required and also supports default props.
React provides a few different ways to define components:
This document explains how to make strongly-typed components using all of the above styles and includes an example of a higher order component.
React.createClass
factory
React ships with PropTypes, which verify the props provided to a component. Unlike the static analysis performed by Flow, PropTypes are only checked at runtime. If your testing doesn’t trigger every code path that provides props to a component, you might not notice a type error in your program.
Flow has built-in support for PropTypes. When Flow sees a createClass
factory that specifies PropTypes, it is able to statically verify that all elements are constructed using valid props.
const Greeter = React.createClass({ propTypes: { name: React.PropTypes.string.isRequired, }, render() { return <p>Hello, {this.props.name}!</p>; }, }); <Greeter />; // Missing `name` <Greeter name={null} />; // `name` should be a string <Greeter name="World" />; // "Hello, World!"
$> flow
10: <Greeter />; // Missing `name` ^^^^^^^^^^^ React element `Greeter` 2: propTypes: { ^ property `name`. Property not found in 10: <Greeter />; // Missing `name` ^^^^^^^^^^^ props of React element `Greeter` 11: <Greeter name={null} />; // `name` should be a string ^^^^^^^^^^^^^^^^^^^^^^^ props of React element `Greeter`. This type is incompatible with 2: propTypes: { ^ propTypes of React component
Flow understands when a default value is specified via getDefaultProps
and will not error when the prop is not provided.
Note that it’s still a good idea to specify isRequired
, even when a default value is provided, to protect against null
prop values. React will only use a default value if the prop value is undefined
.
const DefaultGreeter = React.createClass({ propTypes: { name: React.PropTypes.string.isRequired, }, getDefaultProps() { return {name: "World"}; }, render() { return <p>Hello, {this.props.name}!</p>; }, }); <DefaultGreeter />; // "Hello, World!" <DefaultGreeter name={null} />; // `name` should still be a string <DefaultGreeter name="Flow" />; // "Hello, Flow!"
$> flow
14: <DefaultGreeter name={null} />; // `name` should still be a string ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ props of React element `DefaultGreeter` 14: <DefaultGreeter name={null} />; // `name` should still be a string ^^^^ null. This type is incompatible with 6: return {name: "World"}; ^^^^^^^ string
Flow ensures that state reads and writes are consistent with the object returned from getInitialState
.
const Counter = React.createClass({ getInitialState() { return { value: 0, }; }, increment() { this.setState({ value: this.state.value + "oops!", }); }, decrement() { // Note: Typo below is intentional this.setState({ valu: this.state.value - 1, }); }, render() { return ( <div> <button onClick={this.increment}>+</button> <input type="text" size="2" value={this.state.value} /> <button onClick={this.decrement}>-</button> </div> ); }, });
$> flow
9: value: this.state.value + "oops!", ^^^^^^^^^^^^^^^^^^^^^^^^^^ string. This type is incompatible with 4: value: 0, ^ number
React.Component
subclasses
While PropTypes are great, they are quite limited. For example, it’s possible to specify that a prop is some kind of function, but not what parameters that function accepts or what kind of value it might return.
Flow has a much richer type system which is able to express those constraints and more. With class-based components, you can specify the components’ props, default props, and state using Flow’s annotation syntax.
type Props = { title: string, visited: boolean, onClick: () => void, }; class Button extends React.Component { props: Props; state: { display: 'static' | 'hover' | 'active', }; static defaultProps: { visited: boolean }; onMouseEnter: () => void; onMouseLeave: () => void; onMouseDown: () => void; constructor(props: Props) { super(props); this.state = { display: 'static', }; const setDisplay = display => this.setState({display}); this.onMouseEnter = () => setDisplay('hover'); this.onMouseLeave = () => setDisplay('static'); this.onMouseDown = () => setDisplay('active'); } render() { let className = 'button ' + this.state.display; if (this.props.visited) { className += ' visited'; } return ( <div className={className} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} onMouseDown={this.onMouseDown} onClick={this.props.onClick}> {this.props.title} </div> ); } } Button.defaultProps = { visited: false }; function renderButton(container: HTMLElement, visited?: boolean) { const element = ( <Button title="Click me!" visited={visited} onClick={() => { renderButton(container, true); }} /> ); ReactDOM.render(element, container); }
Any function that returns a React element can be used as a component class in a JSX expression.
function SayAgain(props: { message: string }) { return ( <div> <p>{props.message}</p> <p>{props.message}</p> </div> ); } <SayAgain message="Echo!" />; <SayAgain message="Save the whales!" />;
Stateless functional components can specify default props as destructuring with default values.
function Echo({ message, times = 2 }: { message: string, times?: number }) { var messages = new Array(times).fill(<p>{message}</p>); return ( <div> {messages} </div> ); } <Echo message="Helloooo!" />; <Echo message="Flow rocks!" times={42} />;
Occasionally, repeated patterns in React components can be abstracted into functions that transform one component class into another.
In the example below, the HOC loadAsync
takes a component that requires some arbitrary config and a promise that will eventually provide that config and returns a component that takes care of loading the data and displaying the component asynchronously.
function loadAsync<Config>( ComposedComponent: ReactClass<Config>, configPromise: Promise<Config>, ): ReactClass<{}> { return class extends React.Component { state: { config: ?Config, loading: boolean, }; load: () => void; constructor(props) { super(props); this.state = { config: null, loading: false, } this.load = () => { this.setState({loading: true}); configPromise.then(config => this.setState({ loading: false, config, })); } } render() { if (this.state.config == null) { let label = this.state.loading ? "Loading..." : "Load"; return ( <button disabled={this.state.loading} onClick={this.load}> {label} </button> ); } else { return <ComposedComponent {...this.state.config} /> } } } } const AsyncGreeter = loadAsync(Greeter, new Promise((resolve, reject) => { setTimeout(() => { resolve({ name: "Async World" }); }, 1000); })); <AsyncGreeter />;
$> flow
39: return <ComposedComponent {...this.state.config} /> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ props of React element `ComposedComponent`. Expected object instead of 39: return <ComposedComponent {...this.state.config} /> ^^^^^^^^^^^^^^^^^ Config
Another common use for HOCs is to wrap a React component in a HOC that takes fewer or different properties. In this example, we will demonstrate how to write a HOC that wraps a component to create a component that expects a subset of the required properties
First, here is a simple React component that has 3 required properties
type TimelyProps = { date: Date, name: string, excited: boolean }; class Timely extends React.Component<void, TimelyProps, void> { render() { const hours = this.props.date.getHours(); const timeOfDay = hours > 17 ? 'Evening' : hours > 12 ? 'Afternoon' : 'Morning'; return ( <div> Good {timeOfDay} {this.props.name} {this.props.excited ? '!' : ''} </div> ); } }
Using Timely
will error if you omit any of the 3 properties or if they have the wrong type
<Timely />; // Missing all the required props <Timely name='John' />; // Missing date and excited <Timely name='John' excited={true} />; // Missing date <Timely date={new Date()} name='John' excited={true} /> // Ok!
$> flow
1: <Timely />; // Missing all the required props ^^^^^^^^^^ React element `Timely` -18: class Timely extends React.Component<void, TimelyProps, void> { ^^^^^^^^^^^ property `date`. Property not found in 1: <Timely />; // Missing all the required props ^^^^^^^^^^ props of React element `Timely` 1: <Timely />; // Missing all the required props ^^^^^^^^^^ React element `Timely` -18: class Timely extends React.Component<void, TimelyProps, void> { ^^^^^^^^^^^ property `excited`. Property not found in 1: <Timely />; // Missing all the required props ^^^^^^^^^^ props of React element `Timely` 1: <Timely />; // Missing all the required props ^^^^^^^^^^ React element `Timely` -18: class Timely extends React.Component<void, TimelyProps, void> { ^^^^^^^^^^^ property `name`. Property not found in 1: <Timely />; // Missing all the required props ^^^^^^^^^^ props of React element `Timely` 2: <Timely name='John' />; // Missing date and excited ^^^^^^^^^^^^^^^^^^^^^^ React element `Timely` -18: class Timely extends React.Component<void, TimelyProps, void> { ^^^^^^^^^^^ property `date`. Property not found in 2: <Timely name='John' />; // Missing date and excited ^^^^^^^^^^^^^^^^^^^^^^ props of React element `Timely` 2: <Timely name='John' />; // Missing date and excited ^^^^^^^^^^^^^^^^^^^^^^ React element `Timely` -18: class Timely extends React.Component<void, TimelyProps, void> { ^^^^^^^^^^^ property `excited`. Property not found in 2: <Timely name='John' />; // Missing date and excited ^^^^^^^^^^^^^^^^^^^^^^ props of React element `Timely` 3: <Timely name='John' excited={true} />; // Missing date ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ React element `Timely` -18: class Timely extends React.Component<void, TimelyProps, void> { ^^^^^^^^^^^ property `date`. Property not found in 3: <Timely name='John' excited={true} />; // Missing date ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ props of React element `Timely`
Now let’s say we wanted to wrap Timely in a component that automatically provides the date
declare function injectDate<Props, C: React.Component<*, Props, *>>( Komponent: Class<C> ): Class<React.Component<void, $Diff<Props, {date: Date}>, void>>; const Timeless = injectDate(Timely);
The name and excited properties are still required, but now you can omit date, since it’s automatically provided!
<Timeless />; // props not satisfied. <Timeless excited={true} />; // props not satisfied. <Timeless name='Sally' />; // props not satisfied. <Timeless name={1234} excited={true} />; // name must be a string <Timeless name='Sally' excited={true} />; // This works! <Timeless name='Sally' excited={true} date={1234} /> // date must still be a Date object
$> flow
1: <Timeless />; // props not satisfied. ^^^^^^^^^^^^ React element `Timeless` -38: class Timely extends React.Component<void, TimelyProps, void> { ^^^^^^^^^^^ property `excited`. Property not found in 1: <Timeless />; // props not satisfied. ^^^^^^^^^^^^ props of React element `Timeless` 1: <Timeless />; // props not satisfied. ^^^^^^^^^^^^ React element `Timeless` -38: class Timely extends React.Component<void, TimelyProps, void> { ^^^^^^^^^^^ property `name`. Property not found in 1: <Timeless />; // props not satisfied. ^^^^^^^^^^^^ props of React element `Timeless` 2: <Timeless excited={true} />; // props not satisfied. ^^^^^^^^^^^^^^^^^^^^^^^^^^^ React element `Timeless` -38: class Timely extends React.Component<void, TimelyProps, void> { ^^^^^^^^^^^ property `name`. Property not found in 2: <Timeless excited={true} />; // props not satisfied. ^^^^^^^^^^^^^^^^^^^^^^^^^^^ props of React element `Timeless` 3: <Timeless name='Sally' />; // props not satisfied. ^^^^^^^^^^^^^^^^^^^^^^^^^ React element `Timeless` -38: class Timely extends React.Component<void, TimelyProps, void> { ^^^^^^^^^^^ property `excited`. Property not found in 3: <Timeless name='Sally' />; // props not satisfied. ^^^^^^^^^^^^^^^^^^^^^^^^^ props of React element `Timeless` 4: <Timeless name={1234} excited={true} />; // name must be a string ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ props of React element `Timeless`. This type is incompatible with -38: class Timely extends React.Component<void, TimelyProps, void> { ^^^^^^^^^^^ object type 7: <Timeless ^ props of React element `Timeless` 10: date={1234} /> // date must still be a Date object ^^^^ number. This type is incompatible with -8: ): Class<React.Component<void, $Diff<Props, {date: Date}>, void>>; ^^^^ Date
© 2013–present Facebook Inc.
Licensed under the BSD License.
https://flowtype.org/docs/react.html