Architecture atomique pour React-Native

Julien Haegman
8 min readDec 15, 2020

--

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

  1. Le design atomique
  2. Composants atomiques avec React
  3. Architecture du projet
  4. Bonnes pratiques
  5. Conclusion

Sources

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.tspour 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

Résultat attendu
  • 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ènement OnPress()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 ou position.

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 marginou position. 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 ou position.

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.tsx
import 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 et LoginWith, séparés par un atome MySeparator
  • 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 state isLoading dans le fichier LoginPage.tsx, qu’on passe en props au template LoginTemplate.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 … />
</>
}

--

--

Julien Haegman

Full-stack and mobile developer, working mainly with Angular / Symfony and React Native