Créer des formulaires dans une app React Native avec redux-form

Découvrons comment créer un formulaire dans une application React Native avec la librairie redux-form.

Notre store Redux en place, attardons-nous maintenant sur le formulaire de création d’une bière dans notre application React Native. Pour cela, nous allons utiliser redux-form.

Pour suivre cet article, il est conseillé de checkout la branche 3-forms-with-redux-form du repo de l’application React Native.

Usage de redux-form dans la gestion de formulaires React

La documentation de redux-form est disponible sur ce lien et permet de comprendre rapidement la logique de cette librairie.

Création de formulaires React Native avec Redux Form

Motivations

Dans une application React, le développement de formulaires peut s’avérer une tâche fastidieuse et, surtout, répétitive. Validation des formulaires, affichage des erreurs, gestion des événements sur chaque champ, etc. Tout ça se voit fortement simplifié par l’usage de librairies comme Final Form, Formik ou Redux Form.

Logique générale

L’intégration de redux-form dans une application mobile React Native se fait en trois étapes importantes :

  1. d’une part, l’enregistrement du reducer fourni par redux-form dans notre store Redux
  2. ensuite, l’usage de composants <Field /> ou <FieldArray /> (mis à disposition par redux-form) pour créer les champs dans nos composants de formulaire
  3. et enfin, la connexion de nos composants de formulaire au reducer créé à l’étape 1, via le composant d’ordre supérieur (HOC) reduxForm()

Vous cherchez un développeur React Native freelance ?

Je vous accompagne dans la conception de votre application mobile iOS et Android.


Implémentation technique de Redux Form

Ajout du reducer formReducer

La première étape est donc d’importer le reducer de Redux Form dans notre store. Dans ses coulisses, il enregistre les données d’un formulaire en cours de remplissage dans un objet JS. On y trouvera aussi des informations sur ce formulaire : est-il valide, a-t-il des erreurs, une interaction a-t-elle été faite dessus, quel est le champ actif, quels sont les valeurs des champs, etc.

Structure du reducer géré par Redux Form
Structure de l’objet de données du reducer géré par Redux Form

On va donc éditer configureStore.js pour intégrer ce reducer fourni par Redux Form.

import { reducer as formReducer } from "redux-form";

const persistConfig = {
	key: "brkp",
	storage: FilesystemStorage,
	blacklist: ["form"],
};

const rootReducer = combineReducers({
	form: formReducer,
	beers: beersReducer,
});

On profite donc de la fonction combineReducers() de Redux pour désormais cumuler deux reducers dans notre state d’application mobile : un pour stocker nos bières, l’autre pour Redux Form.

On note au passage, ligne 6, qu’on indique à redux-persist d’ignorer le reducer de Redux Form en l’ajoutant à sa liste noire. Nous n’avons aucun intérêt à enregistrer dans la mémoire du téléphone l’état des formulaires de notre application, au risque de retrouver nos champs de formulaire remplis de leurs anciennes valeurs à chaque réouverture de l’appli.

Création d’un composant de champ de formulaire de type texte

Concept général d’un champ <Field /> de Redux Form

Nous allons utiliser , nous allons créer ce composant TextInput en charge d’afficher un champ de type texte.

Redux Form va automatiquement quelques props « magiques » à notre composant <TextInput />. On aura accès à 2 props principales :

  1. this.props.input pour, entre autre, exécuter des événements relatifs à un champ (blur, change, focus) ou accéder à des informations du champ (checked, name, value).
  2. this.props.meta pour récupérer des métadonnées propres à ce champ (s’il est actif, s’il possède une erreur ou s’il est valide, s’il est « pristine » (s’il n’a jamais été touché), sa valeur initial, etc.)
Structure de base du <TextInput />
class TextInput extends Component {
	render() {
		const { input, ...inputProps } = this.props;

		return (
			<Input
				{...inputProps}
				onChangeText={input.onChange}
				onBlur={input.onBlur}
				onFocus={input.onFocus}
				value={input.value.toString()}
			/>
		);
	}
}

Dans sa version simple, notre composant de champ texte n’a besoin que de la prop input rendue disponible par Redux Form, pour :

  • déléguer les changements de valeurs du champ avec onChangeText={input.onChange}
  • déléguer les pertes de focus avec onBlur={input.onBlur}
  • déléguer les mises en focus avec onFocus={input.onFocus}
  • lui attribuer sa valeur via value={input.value.toString()}

Les événements de ce champ sont alors sous-traités à Redux Form qui se charge de sa magie pour mettre à jour le store, évaluer la valeur du champ pour le considérer comme valide ou non, et bien plus.

Structure avancée du champ de texte

Dans sa version complète, notre <TextInput /> se retrouve entouré d’un nouveau composant <FieldWrapper />.

class FieldWrapper extends Component {
	render() {
		return (
			<View>
				{this.props.children}
				{this.props.meta.touched && this.props.meta.error && (
					<Text style={styles.error}>{this.props.meta.error}</Text>
				)}

				{this.props.meta.warning && (
					<Text style={styles.warning}>
						{this.props.meta.warning}
					</Text>
				)}
			</View>
		);
	}
}

C’est un conteneur qui servira à tous nos éventuels futurs autres types de champs (menu déroulant, champ photo, case à cocher, etc.) et nous évitera de répéter des logiques communes.

En l’occurrence, on affiche un message d’erreur si le champ a été touché (meta.touched) et s’il a une erreur (meta.error), ou un message de conseil (warning) si existant. Ces messages sont affichés sous le champ (matérialisé par {this.props.children}).

Création du formulaire d’identification dans l’application mobile

Chaque formulaire sera encapsulé dans un composant de type classe à part entière et relié à la magie de Redux Form via le HOC reduxForm(), à la manière du connect() de Redux. Ces composants formulaires pourront alors être intégrés dans les écrans de notre application mobile.

Création du formulaire de login

On peut alors utiliser notre composant de champ texte pour créer notre formulaire qui servira à l’identification utilisateur.

import { reduxForm, Field } from "redux-form";

class LoginForm extends Component {
	render() {
		return (
			<View>
				<Field
					name="login"
					label="Identifiant"
					textContentType="username"
					autoCorrect={false}
					autoCapitalize="none"
					component={TextInput}
					icon="person"
				/>
				<Field
					name="password"
					label="Mot de passe"
					textContentType="password"
					secureTextEntry={true}
					autoCorrect={false}
					autoCapitalize="none"
					component={TextInput}
					icon="key"
				/>
				<Button full warning rounded onPress={this.props.handleSubmit}>
					<Text>Log in</Text>
				</Button>
			</View>
		);
	}
}

export default reduxForm({
	form: "login",
})(LoginForm);

Comme mentionné précédemment, nos champs <Field /> ont tous besoin au moins d’une prop name et component. Les autres props sont propres à l’<Input /> utilisé issu de NativeBase (pour définir un label, le type de contenu du champ, l’auto-correction ou auto-majuscule, et l’icône).

Avec nos champs déclarés, il manque seulement un bouton qui déclenchera l’action this.props.handleSubmit() fournie par Redux Form sur notre composant de formulaire.

Ce composant de formulaire est ensuite connecté à Redux Form et le store Redux grâce au composant d’ordre supérieur reduxForm(). Seule option obligatoire : form indiquant la clé représentant l’objet du formulaire dans le store.

Traitement des données du formulaire envoyé

Notre composant de formulaire <LoginForm /> étant enregistré, on peut l’utiliser dans notre écran d’identification. La seule nécessité requise par Redux Form est de lui fournir une prop onSubmit qui s’exécutera lorsque le formulaire sera envoyé (au clic sur le bouton « Log In ») — seulement si Redux Form l’aura considéré comme valide.

class Login extends Component {
	handleLoginFormSubmit = values => {
		console.log(values);

		// For now, fake Login Success and navigate to BeersList.
		this.props.navigation.navigate("BeersList");
	};

	render() {
		return (
			<View>
				<LoginForm onSubmit={this.handleLoginFormSubmit} />
			</View>
		);
	}
}

Création du formulaire d’ajout de nouvelle bière

Création du formulaire d’ajout de bière

class BeerAddForm extends Component {
	render() {
		return (
			<View>
				<Field name="name" label="Beer name" autoCorrect={false} component={TextInput} />
				<Field name="brewery" label="Brewery" autoCorrect={false} component={TextInput} />
				<Field name="style" label="Style" autoCorrect={false} component={TextInput} />
				<Field name="abv" label="ABV (%)" autoCorrect={false} component={TextInput} />
				<Field name="aromas" label="Aromas" autoCorrect={false} component={TextInput} />
				<Field name="comment" label="Comment" autoCorrect={true} component={TextInput} />
				<Field name="rating" label="Rating" autoCorrect={false} component={TextInput} />

				<Button full warning rounded onPress={this.props.handleSubmit}>
					<Text>Add beer</Text>
				</Button>
			</View>
		);
	}
}

export default reduxForm({
	form: "beerAdd",
})(BeerAddForm);

La logique est sensiblement la même que le formulaire de login. Ici, notre formulaire d’ajout de bière présente seulement un peu plus de champs textes.

Relation du formulaire envoyé avec l’action Redux addBeer()

class BeerAdd extends Component {
	handleBeerAddFormSubmit = values => {
		UUIDGenerator.getRandomUUID().then(uid => {
			// First, add the beer.
			this.props.addBeer({
				uid: uid,
				createdAt: Date.now(),
				editedAt: null,
				deletedAt: null,
				photo: dummyBeerImage,
				...values,
			});

			// Then, redirect back to BeersList.
			this.props.navigation.navigate("BeersList");
		});
	};

	render() {
		return (
			<View>
				<BeerAddForm onSubmit={this.handleBeerAddFormSubmit} />
			</View>
		);
	}
}

const mapDispatchToProps = dispatch => {
	return {
		addBeer: data => dispatch(addBeer(data)),
	};
};

export default connect(
	null,
	mapDispatchToProps
)(BeerAdd);

Même logique pour l’intégration du formulaire sur notre écran dédié à la création d’une nouvelle bière.

Cette fois par contre, notre écran est lui-même directement relié au store Redux pour pouvoir dispatch une action addBeer(). Cette action est exécutée dans une méthode handleBeerAddFormSubmit() à la soumission du formulaire <BeerAddForm /> précédemment créé.

En plus des valeurs du formulaires fournies par Redux Form via le paramètre values, on va y ajouter quelques autres valeurs extras utiles à l’identification et datation de notre bière :

  1. un identifiant unique uid généré par la librairie react-native-uuid-generator, pour assigner à la bière un identifiant unique (UUID de version 4) et pouvoir la retrouver plus tard pour l’éditer ou la supprimer
  2. un timestamp createdAt pour stocker sa date de création
  3. deux autres timestamps editedAt et deletedAt pour l’instant vides, mais qui risqueront d’être remplis plus tard
  4. et une clé photo contenant pour l’instant un objet image factice. Sa valeur pourra à l’avenir provenir d’un champ de type photo.

Une fois la création de cette bière bien envoyé à Redux via this.props.addBeer(), on redirige l’utilisateur vers l’écran listant ses bières avec this.props.navigation.navigate("BeersList").

Conclusion

Nous avons ainsi développé deux formulaires dans notre application React Native (login et création de bière). L’un est pour l’instant factice tandis que l’autre enregistre bien des données dans le store Redux, mais nous analyserons l’authentification utilisateur via AJAX entre React Native & WordPress dans un futur article.

Dans l’épisode suivant, nous améliorerons nos formulaires pour intégrer des validations, conseils et formatage automatique des valeurs.


Vous avez aimé cet article ?

Partagez-le sur vos réseaux sociaux en guise de remerciement :)


1 commentaire

Laisser un commentaire