Comment ajouter de manière incrémentielle Flow à une application React existante

Flow est un vérificateur de type statique pour Javascript. Cet article est destiné à ceux qui ont entendu parler de Flow, mais qui n'ont pas encore essayé de l'utiliser dans une application React. Si c'est la première fois que vous entendez parler de Flow, je peux recommander ces quatre articles de Preethi Kasireddy comme une excellente introduction.

Une grande chose à propos de Flow est qu'il est possible de l'utiliser de manière incrémentielle. Il n'est pas nécessaire de refactoriser complètement un projet existant pour commencer à l'utiliser. Il peut être ajouté uniquement à de nouveaux fichiers ou lentement essayé dans des fichiers existants pour voir s'il apporte des avantages à votre projet spécifique avant de s'engager pleinement.

Comme la configuration d'un nouvel outil peut souvent être la plus difficile, dans cet article, nous allons prendre un projet existant et parcourir la configuration de l'ajout de Flow. Une introduction générale à la syntaxe est couverte dans le deuxième des articles de Preethi, et les documents Flow sont également très lisibles.

Nous utiliserons cet exemple de dépôt, avec deux répertoires pour le pré et le post-Flow. Il utilise le script personnalisé de l'application Create React de Skyscanner backpack-react-scripts, associé à leurs composants de sac à dos personnalisés. Cela vise à créer des exemples plus complexes que des extraits de code simples, mais toujours lisibles même si vous ne les connaissez pas.

La nature exacte de l'application n'a pas d'importance par rapport à la différence entre son implémentation sans et avec Flow. Très peu de fichiers changent ici, mais ils sont souvent les plus frustrants à faire!

Passons en revue chaque étape, puis examinons la conversion des exemples de composants.

Installez les dépendances principales

Parallèlement à Flow lui-même, installez babel-cli et babel-preset-flow afin que babel puisse supprimer les annotations de type lors de la compilation.

npm install flow-bin babel-cli babel-preset-flow --save-dev

Configurer Babel

Pour que ceux-ci prennent effet, créez un .babelrcfichier ou ajoutez à votre .babelrcconfiguration existante la suivante:

{ "presets": ["flow"] }

Scripts de configuration

Si vous utilisez des hooks, tels qu'un script de prétest, vous souhaiterez peut-être les mettre à jour et ajouter le script Flow de base à votre package.json:

"scripts": { "flow": "flow", "pretest": "npm run flow && npm run lint" }

Générer un flowconfig

Si vous exécutez flow pour la première fois, vous pouvez générer un modèle .flowconfigen exécutant npm run flow init. Dans notre exemple, nous pouvons voir que nous l'étendons pour ajouter ce qui suit:

Ignorer les modèles

Pour éviter que Flow analyse vos modules de nœuds et génère la sortie, ceux-ci peuvent facilement être ignorés.

[ignore].*/node_modules/*.*/build/*

Ajouter la prise en charge des modules CSS

Si vous utilisez des modules CSS, leur type doit être spécifié pour que Flow les comprenne, sinon vous recevrez cette erreur:

C'est fait en deux étapes. Tout d'abord, ce qui suit est ajouté à votre .flowconfig:

[libs] ./src/types/global.js // this can be any path and filename you wish [options] module.name_mapper='^\(.*\)\.scss$' -> 'CSSModule' module.system=haste

Et deuxièmement, un type de module CSS est créé dans le fichier référencé dans [libs].

// @flow declare module CSSModule { declare var exports: { [key: string]: string }; declare export default typeof exports; }

Synchroniser avec les autres linters utilisés

Dans l'exemple de projet, ESLint est déjà utilisé pour fournir un peluchage standard. Certaines étapes de configuration initiales sont nécessaires pour que ESLint joue correctement avec Flow, et d'autres plus tard en raison des types spécifiques utilisés dans ce projet.

Pour la configuration générale, ce qui suit est ajouté à notre .eslintrc:

"extends": [ "plugin:flowtype/recommended" ], "plugins": [ "flowtype" ]

Les extensions spécifiques à cet exemple, et les erreurs qu'elles évitent, seront couvertes vers la fin de cet article.

Libdefs typés par flux

La dernière partie de la configuration consiste à se préparer à l'utilisation libdefscréée à l'aide du flow-typedpackage NPM. Ceci est utilisé pour créer des définitions pour les modules de noeud installés et, par défaut, crée ces fichiers dans un flow-typed/répertoire.

Nous ne voulons engager ce fichier, mais ne veulent pas ESLint à Lint il. Cela crée un problème, car précédemment notre script de linting dans notre package.jsonest configuré pour utiliser notre .gitignorepour savoir alors que les fichiers ESLint devraient également ignorer:

"lint:js": "eslint . --ignore-path .gitignore --ext .js,.jsx",

Nous voulons maintenant changer cela, car nous voulons qu'ESLint ignore également le flow-typed/répertoire à créer . Nous pouvons modifier notre script pour:

"lint:js": "eslint . --ext .js,.jsx",

Cela signifie qu'il va maintenant revenir à l'utilisation d'un .eslintignorefichier, nous devons donc le créer, dupliquer ce qui se trouve dans notre .gitignore, et ajouter le répertoire supplémentaire pour l'ignorer.

Enfin, nous devons installer flow-types. Nous faisons cela à l'échelle mondiale.

npm install flow-typed -g

libdefspeuvent être des définitions complètes ou des stubs acceptant tous les types. Une liste de définitions complètes est maintenue. Pour voir s'il y en a un disponible pour un package que vous utilisez, utilisez

flow-typed install [email protected]

et cela l'ajoutera à votre flow-typedrépertoire ou vous invitera à créer un stub en utilisant

flow-typed create-stub [email protected]

Si vous souhaitez créer une définition complète, vous pouvez le faire, et également la contribuer au référentiel afin qu'elle soit disponible pour d'autres développeurs.

Un processus simple à suivre est de créer uniquement libdefscomme ils sont spécifiquement requis. Pour chaque composant que vous convertissez pour utiliser Flow, ajoutez ses importations en utilisant flow-typedà ce moment-là, il n'est pas nécessaire d'ajouter des types pour toutes les dépendances si elles ne sont pas utilisées dans les fichiers où Flow est également utilisé.

Conversion de composants existants

That is all the general setup done, now we can look at converting our example components!

We have two, a stateful component and a function component. Overall these create a banner than has some text and a button. The text on the banner can be clicked to open a popover, containing a bullet pointed list.

Add flow-typed definitions

For any component, the first step is to create flow-typed definitions for any imports in the component we are working in.

For example, if we only had imports of

import React from 'react'; import BpkButton from 'bpk-component-button';

then we would try:

flow-typed install [email protected]on>

if it was not available, and it currently is not, then we would stub its definition:

flow-typed create-stub [email protected]

In the example repo we can see the list of all created definitions for the components we moved to using Flow. These were added one at a time as each component had Flow integrated with them.

Function Components

In our example without Flow we use PropTypes for some limited type checking and their ability to define defaultProps for use in development.

It may look a little complex on first glance, but there is relatively little that we need to change in order to add Flow.

Original text


To transform this to use Flow we can first remove the PropTypes import and definitions. The // @flow annotation can then be added to line one.

For this component we are only going to type check the props passed in. To do so we will first create a Props type, much cleaner than defining each prop individually inline.

type Props = { strings: { [string_key: string]: string }, onClose: Function, isOpen: boolean, target: Function, };

Here the latter three types are self-explanatory. As strings is an object of strings an object as a map has been used, checking each key and value in the object received to check that their types match, without having to specify their exact string keys.

The prop-types definitions can then be removed along with its import. As defaultProps are not tied to this import they can, and should, remain. *See the closing ESLint comments for any errors reported at this point.

The component should now look like this:

Stateful Components

Stateful components follow some slightly different declarations. As this component is more complex we will also look at declaring types for some additional aspects.

As before, first take a look at the component before adding Flow.

Props and State

As in the function component we first remove the propTypes definition and import, and add the // @flow annotation.

First we will take a look at adding types for Props and State. Again we will create types for these:

type Props = { strings: { [string_key: string]: string }, hideBannerClick: Function, }; type State = { popoverIsOpen: boolean, };

and specify that the component will use them:

class Banner extends Component { constructor(props: Props) { super(props); this.state = { popoverIsOpen: false, }; ... }; ... };

Next we hit our first difference between Function and Stateful components, defaultProps. In a Function component these were declared as we are used to, in Stateful components the external Banner.defaultProps syntax is removed, and instead the defaults are declared within the class:

class Banner extends Component { static defaultProps = { strings: defaultStrings, }; constructor(props: Props) { ... // the below is removed // Banner.defaultProps = { // strings: defaultStrings, // };

Constructor declarations

stringWithPlaceholder is declared within the constructor. Here we are not looking at why it is declared there (we will assume there is good reason), but rather to see whether flow can be added without any changes to the existing code.

If run in its existing state we would encounter the error Cannot get this.stringWithPlaceholder because property stringWithPlaceholder is missing in Banner [1].

To fix this we must add a single line inside the Banner class block, just beneath and outside of the constructor:

class Banner extends Component { constructor(props: Props) { super(props); this.state = { popoverIsOpen: false, }; this.stringWithPlaceholder = ... }; stringWithPlaceholder: string; ... };

This variable is created in the constructor but not passed in as props. As we are using Flow for type checking the props passed into the constructor, it requires everything within the constructor be type checked. It is known that Flow requires this, and this can be done by specifying their type in the class block.

At this point Props and State are complete. Let’s look at some quick additional examples of type checking within this component. *See the closing ESLint comments for any errors reported at this point.

Return, Event, and Node types

togglePopover takes no arguments, so a simple example of specifying no return value can be seen:

togglePopover = (): void => { ... };

keyboardOnlyTogglePopover returns nothing, but has a single parameter. This is an event, specifically a keypress event. SyntheticKeyboardEvent is used as

React uses its own event system so it is important to use the SyntheticEvent types instead of the DOM types such as Event, KeyboardEvent, and MouseEvent.
keyboardOnlyTogglePopover = (e: SyntheticKeyboardEvent): void => { ... };

Popover is defined in render() and returns an instance of the ListPopover Function component we looked a previously. We can specify its return type as a React Node. However, to be able to do so, we must first import it, as it is not accessible by default. There is more than one way to import it, one of which shown below:

import React, { Component } from 'react'; import type { Node } from 'react'; ... const Popover: Node = (  document.getElementById('ListPopoverLink')} /> );

Type checking imported React components

When Prop types have been declared in a component, they can be used when using that component within another. However, if you are using an index.js to export the first component then the flow, // @flow will need to be added to the index.

For example:

// @flow import ListPopover from './ListPopover'; export default ListPopover;

Marking props as optional

A prop can be marked as optional using the prop?: type syntax, for example:

type Props = { strings: { [string_key: string]: string }, hideBannerClick?: Function, };

This is supported, but no longer recommended by Flow. Instead all props should be left as required, with no ? , even if optional, as Flow automatically detects defaultProps and marks props with a default as optional internally.

In the section below we can see how manually marking props as optional can cause conflicts with other tools in some cases.

ESLint extensions, default props, and props validation error solutions

Two additions are made to our .eslintrc. For this project specifically you can simply accept their use, or read the detail below if you see any of the three errors:

  • x missing in props validation
  • error defaultProp "x" defined for isRequired propType
  • Cannot get strings.xxx because property xxx is missing in undefined

The rules added, with reasoning, are:

"react/default-props-match-prop-types": [ "error", { "allowRequiredDefaults": true } ]

When using objects as maps (in this case for the 'strings' prop) a missing in props validation error occurs. This is a bug and so is explicitly ignored here.

"react/default-props-match-prop-types": [ "error", { "allowRequiredDefaults": true }]

When using objects as maps complexities between ESLint, flow, and prop-types come into play.

strings is a required prop, passed as an object of strings. The flow type checks that for each entry in the object the string key is a string, and the value is a string. This is far more maintainable than having to list out the prop type of each specific key.

If the prop is marked as required in Flow then ESLint would error stating: error defaultProp "strings" defined for isRequired propType.

If the prop is manually marked as optional then Flow will error with Cannot get strings.xxx because property xxx is missing in undefined [1].

This is known and is due to refinement invalidation as JSX can transform method calls so Flow cannot be sure that xxx has not been redefined.

This leaves us with fixing the ESLint error. The rules above allows defaultProps to be defined while the Flow type is not marked as optional. Flow will understand this and convert it to optional. ESLint is marked to "allowRequiredDefaults": true, meaning that although ESLint sees the prop as required it will not error.

Final thoughts

Once over the initial hurdle of installation, Flow is fairly straightforward to use. The ability to add it incrementally definitely helps, rather than having to refactor an entire project in one go.

Hopefully the setup instructions and examples here prove useful if you are looking to try Flow out yourself.

Thanks for reading ?

You may also enjoy:

  • Testing React with Jest and Enzyme I
  • A beginner’s guide to Amazon’s Elastic Container Service
  • Using Pa11y CI and Drone as accessibility testing gatekeepers