Architecture atomique pour React-Native
Le design atomique permet d’obtenir de la modularité et une meilleure compréhension pour les grandes bibliothèques de composasnts.
Nous allons rappeler en premier lieu son concept, puis voir comment l’adapter sous React Native avec un exemple concret.
Repository GitHub
Sommaire
- Le design atomique
- Composants atomiques avec React
- Architecture du projet
- Bonnes pratiques
- Conclusion
Sources
- Atomic Design, by Brad Frost (📖🇬🇧)
- L’atomic design, une méthode de co-création prometteuse — Audrey Hacq (🇫🇷)
- An efficient way to structure React Native projects | Cheesecake Labs (🇬🇧)
Le design atomique
Présentation
Le design atomique permet de créer une bibliothèque de composants modulaire, réutilisable et hiérarchisée grâce des assemblages d’éléments, répartis sur plusieurs niveaux.
En s’inspirant du réel, un organisme sera composé de molécules, composées d’atomes.
Les organismes seront ensuite arrangés de manière à former un template, puis une page.
Atomes
- Un atome est un élément irréductible, indivisible et ayant un seul but précis. (ex: Bouton, Input)
- Il peut posséder des variantes pour une meilleure généralisation.
Molécules
- Une molécule est un assemblage d’atomes
- Elle doit avoir une fonction unique identifiable (ex : Champs de recherche)
Organismes
- Un organisme est un assemblage de molécules ou de molécules et d’atomes. Il forme une partie de l’interface finale.
Templates
- Un template représente le layout d’une page. Il permet principalement de tester l’assemblage des organismes.
Pages
- La page est le rendu final de l’interface. Les images et les données peuvent y être affichées. Les actions de l’utilisateur ayant une incidence sur le comportement de l’application (ex: click sur le bouton de connexion après avoir rempli les champs) y seront traités.
Architecture atomique d’un projet React Native
├── src/
│ ├── assets/
│ ├── components/
│ │ ├── atoms/
│ │ │ ├── index.ts
│ │ ├── molecules/
│ │ │ ├── index.ts
│ │ ├── organisms/
│ │ │ ├── index.ts
│ ├── entities/
│ ├── navigation/
│ ├── screens/
│ ├── services/
│ ├── styles/
│ ├── translations/
│ ├── utils/
- assets : Fichiers images / polices / logos
- components : Composants atomiques
- entities : Classes ou interfaces pour représenter des entités
- navigation : Fichier(s) pour gérer les écrans et la navigation sur l’application
- screens : Pages et templates
- services : Composants non graphique utiles au fonctionnement de l’application. (Ex: Appel à l’API)
- styles : Styles globaux utilisés par les composants atomiques
- translations : Fichiers de traduction
- utils : Fonctions et classes ne rentrant pas dans la catégorie des services ou de provenance externe
On crée un fichier index.ts
pour faciliter les importations. Au lieu d’importer chaque atome/molécule/organisme avec son chemin absolu, on peut simplement utiliser le chemin vers le type de composant.
// avant :
import { MyFirstAtom } from '../atoms/MyFirstAtom';
import { MySecondAtom } from '../atoms/MySecondAtom';// après :
import { MyFirstAtom, MySecondAtom } from '../atoms';
PROTIP : Vous pouvez utiliser ce script bash (gist) pour initialiser l’architecture plus rapidement.
Composants atomiques avec React
Objectif à réaliser
- Pour ce tutoriel, nous allons réaliser une copie de l’interface de connexion d’AirBnb.
- Les styles des composants sont isolés dans le fichier
src/styles/MyStyles
pour simplifier la démonstration.
Atomes
- Chaque atome peut avoir ses propres propriétés. Il peut émettre des évènements qui seront traités dans les niveaux plus haut.
Par exemple, l’évènementOnPress()
du bouton de login sera traité au niveau de la page. - Pour éviter la redondance, un atome peut aussi disposer de variantes ou de propriétés variables. Dans cet exemple, un bouton peut avoir un texte et une couleur propre et les inputs de connexion ont chacune une variante « Nom d’utilisateur » /« Mot de passe »
- La disposition finale d’un atome sera configurée dans les composants supérieurs. Il ne doit donc pas posséder d’attribut
margin
ouposition
.
On aura donc, pour un atome MyButton
:
// src/components/atoms/MyButton.tsximport { ColorValue, Text, TouchableOpacity } from ‘react-native’;
import MyStyles from ‘../../styles/MyStyles’;
import React from ‘react’;type MyButtonProps
text: string;
onPress: () => void;
// Optional background color
color?: ColorValue;
// Optional text color
textColor?: ColorValue;
};function MyButton(props: MyButtonProps) {
return (
<TouchableOpacity
style={[
MyStyles.myButton,
{ backgroundColor: props.color ? props.color : ‘#fff’ }
]}
onPress={props.onPress}
>
<Text
style={[
MyStyles.myButtonText,
{ color: props.textColor ? props.textColor : ‘#000’}
]}
>
{props.text}
</Text>
</TouchableOpacity>
);
}export default MyButton;
Ou pour un champs de login LoginField
:
// src/components/atoms/LoginInput.tsx
import React from ‘react’;
import { TextInput, View } from ‘react-native’;import MyStyles from ‘../../styles/MyStyles’;type LoginInputProps = {
text: string;
variant: ‘top’ | ‘bottom’;
};function LoginInput(props: LoginInputProps) {
return (
<View
style={[
MyStyles.loginInputContainer,
props.variant === ‘top’ ? MyStyles.loginInputContainer__top
: MyStyles.loginInputContainer__bottom,
]}
>
<TextInput placeholder={props.text} style={MyStyles.loginInput} />
</View>
);
}export default LoginInput;
Molécules
- Une molécule contient des atomes et/ou des éléments natifs.
- Elle ne possède pas d’attribut
margin
ouposition
. Sa disposition sera également définie dans les composants supérieurs.
Pour la molécule LoginFieldset
qui contient les deux champs de connexion, on aura :
// src/components/molecules/LoginFieldset.tsximport React from ‘react’;
import { View } from ‘react-native’;import { LoginInput } from ‘../atoms’;
import MyStyles from ‘../../styles/MyStyles’;function LoginFieldset() {
return (
<View style={MyStyles.loginFieldset}>
<LoginInput text=”Nom d’utilisateur” variant=’top’ />
<LoginInput text=’Mot de passe’ variant=’bottom’ />
</View>
);
}export default LoginFieldset;
Organismes
- Un organisme sera un assemblage d’atomes et / ou de molécules et / ou d’éléments natifs.
- Il doit être identifiable et avec un but précis. Son apparence doit être finale.
- Comme pour les atomes et molécules, il ne contient pas d’attribut
margin
ouposition
.
Pour l’organisme LoginForm
, qui contient la connexion classique :
// src/components/organisms/LoginForm.tsximport React from ‘react’;
import { Text, View } from ‘react-native’;import { MyButton } from ‘../atoms’;
import { LoginFieldset } from ‘../molecules’;
import MyStyles from ‘../../styles/MyStyles’;type LoginFormProps = {
onSubmit: () => void;
};function LoginForm(props: LoginFormProps) {
return (
<View style={MyStyles.loginForm}>
<Text style={[MyStyles.title, MyStyles.marginBottom]}>
Connexion / Inscription
</Text>
<View style={MyStyles.marginTop}>
<LoginFieldset />
</View>
<Text style={[MyStyles.paragraph, MyStyles.marginTop]}>
Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam
nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat
volutpat.
</Text>
<View style={MyStyles.marginTop}>
<MyButton
text=’Continuer’
onPress={props.onSubmit}
color={‘#583D72’}
textColor={‘#fff’}
/>
</View>
</View>
);
}export default LoginForm;
Et pour LoginWith
, qui contient les modes de connexion alternatifs :
// src/components/organisms/LoginWith.tsximport React from ‘react’;
import { View } from ‘react-native’;import { MyButton } from ‘../atoms’;
import MyStyles from ‘../../styles/MyStyles’;type LoginWithProps = {
onLoginWithFacebook: () => void;
onLoginWithGoogle: () => void;
onLoginWithApple: () => void;
onLoginWithTwitter: () => void;
};function LoginWith(props: LoginWithProps) {
return (
<View style={MyStyles.loginWithContainer}>
<View style={MyStyles.marginBottom}>
<MyButton
text=’Continuer avec Facebook’
onPress={props.onLoginWithFacebook}
/>
</View>
<View style={MyStyles.marginBottom}>
<MyButton
text=’Continuer avec Google’
onPress={props.onLoginWithGoogle}
/>
</View>
<View style={MyStyles.marginBottom}>
<MyButton
text=’Continuer avec Apple’
onPress={props.onLoginWithApple}
/>
</View>
<View style={MyStyles.marginBottom}>
<MyButton
text=’Continuer avec Twitter’
onPress={props.onLoginWithTwitter}
/>
</View>
</View>
);
}export default LoginWith;
Templates
- Le template correspond au rendu final. Il dispose les organismes ensemble, et peut intégrer quelques éléments natifs ou atomes/molécules si ceux-ci sont nécessaires. Dans le cadre de l’exemple, nous intégrons les organismes
LoginForm
etLoginWith
, séparés par un atomeMySeparator
- Aucun comportement fonctionnel de l’application n’est attendu ici. Si le template dispose de fonctions, celles-ci seront uniquement utilisées pour faire varier les affichages (affichage d’un loader, mise à jour graphique d’éléments etc).
Pour le template LoginTemplate
:
// src/screens/login/LoginTemplate.tsximport React from ‘react’;
import { View } from ‘react-native’;import { MySeparator } from ‘../../components/atoms’;
import { LoginForm, LoginWith } from ‘../../components/organisms’;
import MyStyles from ‘../../styles/MyStyles’;type LoginTemplateProps = {
onLogin: () => void;
onLoginWithFacebook: () => void;
onLoginWithGoogle: () => void;
onLoginWithApple: () => void;
onLoginWithTwitter: () => void;
};function LoginTemplate(props: LoginTemplateProps): JSX.Element {
return (
<View style={MyStyles.loginPage}>
<StatusBar style=’auto’ />
<LoginForm onSubmit={() => props.onLogin()} />
<View style={[MyStyles.marginTop, MyStyles.marginBottom]}>
<MySeparator />
</View>
<LoginWith
onLoginWithFacebook={() => props.onLoginWithFacebook()}
onLoginWithGoogle={() => props.onLoginWithGoogle()}
onLoginWithApple={() => props.onLoginWithApple()}
onLoginWithTwitter={() => props.onLoginWithTwitter()}
/>
</View>
);
}export default LoginTemplate;
Pages
- Pour finir, le côté fonctionnel est totalement dissocié du template dans un dernier fichier
*Page.tsx
. - Il contient les fonctions appelées par le template, et peut le mettre à jour indirectement en lui passant des props.
Par exemple, pour afficher un loader sur le formulaire de login lorsqu’il est soumit, on aura juste à déclarer un stateisLoading
dans le fichierLoginPage.tsx
, qu’on passe en props au templateLoginTemplate.tsx
. Ce dernier se mettra à jour graphiquement en lien avec les propriétés de la page. (non implémenté dans cet exemple)
Pour la page LoginPage
:
// src/screens/login/LoginPage.tsximport LoginTemplate from ‘./LoginTemplate’;
import React from ‘react’;class LoginPage extends React.Component {
handleLogin(): /* Connexion classique */}
handleLoginWithFacebook(): void {/* Connexion avec Facebook */}
handleLoginWithGoogle(): void {/* Connexion avec Google */}
handleLoginWithApple(): void {/* Connexion avec Apple */}
handleLoginWithTwitter(): void {/* Connexion avec Twitter */} render(): JSX.Element {
return (
<LoginTemplate
onLogin={() => this.handleLogin()}
onLoginWithFacebook={() => this.handleLoginWithFacebook()}
onLoginWithGoogle={() => this.handleLoginWithGoogle()}
onLoginWithApple={() => this.handleLoginWithApple()}
onLoginWithTwitter={() => this.handleLoginWithTwitter()}
/>
);
}
}export default LoginPage;
Bonnes pratiques
Gestion propre des importations
Pour réduire les importations et éviter les confusions , une bonne pratique peut être de grouper les éléments par type (boutons / champs / modals / éléments de design …).
Avant :
// 3 importations différentes pour des composants du même type
import MyStandardButton from “atoms/myButtons/MyStandardButton”;
import MySpecialButton from “atoms/myButtons/MySpecialButton”;
import MyExtraordinaryButton from “atoms/myButtons/MyExtraordinaryButton”;…// 3 tags différents -> confusion
render():JSX.Element{
<>
<MyStandardButton … />
<MySpecialButton … />
<MyExtraordinaryButton … />
</>
}
Après :
// src/components/atoms/myButtons/index.ts// 3 éléments de type MyButton défini au même endroit
import Standard from ‘./Standard’;
import Special from ‘./Special’;
import Extraordinary from ‘./Extraordinary’;export { Standard, Special, Extraordinary };
``````typescript
// Une seule importation à faire
import MyButtons from “atoms/myButtons”;…// 3 variantes préfixées du même type
render():JSX.Element{
<>
<MyButtons.Standard … />
<MyButtons.Special … />
<MyButtons.Extraordinary … />
</>
}