AccueilClientsExpertisesOpen SourceBlogContactEstimer

15 avril 2025

Lynx: le remplaçant de React Native ?

10 minutes de lecture

Lynx: le remplaçant de React Native ?
🇺🇸 This post is also available in english

Sorti en open source il y a maintenant plusieurs semaines, Lynx est un nouvel arrivant dans le monde du développement mobile. Développé à l'origine par ByteDance, la librairie est déjà utilisée en production au sein de l'application TikTok, de la même manière que Facebook utilisait React Native au sein de ses applications mobiles. Maintenant disponible au grand public, la question se pose de savoir si Lynx peut devenir un concurrent voire un remplaçant de React Native.

Comparatif technique

En parcourant la documentation officielle, on se rend tout de suite compte que Lynx a puisé beaucoup d'inspiration sur des frameworks déjà existants, notamment React Native et Flutter, afin d'offrir une expérience de développement très proche de celle que l'on pourrait retrouver sur une application Web :

  • Bundler moderne basé sur Rspack
  • Librairie React basée sur React 17, permettant de faire le pont avec les composants natifs
  • Support des feuilles de style CSS et de la majorité des propriétés CSS
  • Support des sélecteurs, que ce soit pour du CSS ou au runtime

De la même manière qu'Expo Go, Lynx fournit une application à installer sur son simulateur, Lynx Explorer, permettant d'exécuter un bundle depuis une URL (locale ou distante). C'est d'ailleurs grâce à cela qu'il est possible d'exécuter les différents exemples fournis dans la documentation.

Une application de DevTools est aussi disponible avec un inspecteur d'élément (HTML et natifs) et une console JavaScript. On notera l'absence d'un inspecteur de réseau, fortement appréciable du côté des DevTools Expo.

Côté runtime, on retrouve PrimJS en tant que moteur JavaScript, développé spécifiquement pour Lynx, de la même manière que React Native utilise Hermes.

Niveau support, Lynx a été pensé pour supporter les plateformes mobiles ainsi que le web, de la même manière que React Native supporte Android, iOS (Windows, MacOS, Web supportés aussi par l'intermédiaire de librairies tierces).

La découverte par la pratique

Pour découvrir plus en détail Lynx, nous allons développer une simple application de deux écrans, utilisant l'API JSON Placeholder, pour lister des posts, accéder aux détails d'un post et y lister des commentaires.

Création du projet

Créer un projet Lynx est assez simple :

yarn create rspeedy

Après que le projet soit créé, il ne reste plus qu'à installer les dépendances :

yarn install

Puis on lance notre serveur de développement :

yarn dev

Une URL va être générée. Il suffit de copier-coller cette URL dans l'application Lynx Explorer, et voilà !

Notre premier écran

Entrons dans le vif du sujet en effacant tout le rendu du composant App, et créons notre liste de posts :

import "./App.css";
import PostItem from "./components/PostItem.jsx";
import usePosts from "./hooks/usePosts.js";

export function App() {
  const { data: posts, fetchNextPage } = usePosts();

  return (
    <list
      className="safe-area posts-list"
      list-type="single"
      lower-threshold-item-count={2}
      bindscrolltolower={() => fetchNextPage()}
    >
      {posts?.pages?.flat().map((post) => {
        return (
          <list-item key={post.id} item-key={post.id.toString()}>
            <PostItem body={post.body} id={post.id} title={post.title} />
          </list-item>
        );
      })}
    </list>
  );
}

Et voilà, notre liste est prête. On note déjà plusieurs aspects qui diffèrent par rapport à React Native :

  • pas d'import de composants (View, Text) : ils sont disponibles via le moteur de rendu (ici la librairie React)
  • possibilité d'utiliser des classes CSS. Il est aussi possible de mettre le style directement dans une prop style ou bien d'utiliser l'attribut id, comme en HTML classique

L'élément list a un comportement qui se veut similaire à ce que propose la FlatList de React Native. Cependant, le concept de recyclage des vues est ici totalement natif, là où React Native l'implémente du côté JS, même si plusieurs librairies alternatives tentent de résoudre ce problème (FlashList, Legend List, ShadowList). L'API fournie pour la liste de Lynx est assez complète, rendant ainsi le système de layout très modulable. Par exemple, pour des listes multi colonnes, il est assez facile de faire qu'un sorte qu'un élément en particulier prenne toute la largeur disponible. La documentation fournit de nombreux exemples présentant différents types de listes assez populaires.

Les plus attentifs auront noté le nom de la prop bindscrolltolower, en particulier le préfixe bind. Contrairement à React et React Native où les événements ont tendances à être préfixés par un on (par exemple onPress), Lynx utilise différents préfixes en fonction de la manière dont vous souhaitez intercepter un événement.

Détail d'un post

Naturellement, nous souhaitons maintenant pouvoir cliquer sur un post pour afficher son détail ainsi que ses commentaires. Nous aurons donc besoin d'un système de navigation...inexistant ! En tout cas, ce système n'est pas fourni par Lynx, qui préconise plutôt d'utiliser React Router en version 6. Alors évidemment ça n'est pas idéal : contrairement à React Navigation qui fournit des composants mappés sur des écrans natifs (donc avec des animations natives), on n'aura aucune animation de navigation, aucun composant natif de navigation (header, tab bar, etc) ou même de possibilité d'interactivité avec les gestures avec React Router. Nous allons tout de même utiliser React Router pour gérer la navigation entre les pages.

import { root } from "@lynx-js/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MemoryRouter, Route, Routes } from "react-router";
import { App } from "./App.js";
import Post from "./Post.jsx";

const queryClient = new QueryClient();

root.render(
  <QueryClientProvider client={queryClient}>
    <MemoryRouter>
      <Routes>
        <Route index element={<App />} />
        <Route path="/post/:id" element={<Post />} />
      </Routes>
    </MemoryRouter>
  </QueryClientProvider>,
);

if (import.meta.webpackHot) {
  import.meta.webpackHot.accept();
}

Un peu d'animation ?

Composante essentielle des applications mobiles, les animations donnent de la vie à nos écrans. En React Native, bien qu'une API d'animation soit disponible, on a tendance à utiliser la librairie Reanimated qui, dans ses dernières versions, a sorti un support expérimental des animations CSS. Eh bien Lynx supporte cela par défaut, que ce soit via les feuilles de style CSS ou bien de manière impérative via une fonction appelée au runtime.

Ajoutons une animation CSS sur notre liste de posts :

@keyframes bounce {
    0% {
        transform: scale(1);
    }
    50% {
        transform: scale(1.02);
    }
    100% {
        transform: scale(1);
    }
}

.post-item {
    display: flex;
    flex-direction: column;
    background-color: white;
    border-radius: 12px;
    box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
    padding: 16px;
    gap: 8px;
    animation: bounce 2s ease-in-out infinite;
}

Animons maintenant un élément de la liste au moment d'un clic :

import { useNavigate } from "react-router";

type Props = {
  body: string;
  id: number;
  title: string;
};

const PostItem = ({ body, id, title }: Props) => {
  const navigate = useNavigate();

  const onTap = () => {
    navigate(`/post/${id}`);
  };

  return (
    <view
      className="post-item"
      id={`post-${id}`}
      bindtap={() => {
        lynx.getElementById(`post-${id}`).animate(
          [
            {
              transform: "rotate(20deg)",
              "animation-timing-function": "linear",
            },
            {
              transform: "rotate(0deg)",
              "animation-timing-function": "linear",
            },
            {
              transform: "rotate(-20deg)",
              "animation-timing-function": "linear",
            },
            {
              transform: "rotate(0deg)",
              "animation-timing-function": "linear",
            },
          ],
          {
            name: "shake-rotate-anim",
            duration: 1000,
            iterations: 1,
            easing: "ease-in-out",
          },
        );
      }}
    >
      <text className="post-item-title">{title}</text>
      <text className="post-item-content">{body}</text>
    </view>
  );
};

export default PostItem;

Compatibilité Web

Lynx propose une compatibilité web par défaut, mais sa mise en place est un peu plus complexe que ce que propose Expo par exemple.

Tout d'abord il faut modifier notre fichier lynx.config.ts à la racine :

import { defineConfig } from "@lynx-js/rspeedy";

import { pluginQRCode } from "@lynx-js/qrcode-rsbuild-plugin";
import { pluginReactLynx } from "@lynx-js/react-rsbuild-plugin";

export default defineConfig({
  plugins: [
    pluginQRCode({
      schema(url) {
        // We use `?fullscreen=true` to open the page in LynxExplorer in full screen mode
        return `${url}?fullscreen=true`;
      },
    }),
    pluginReactLynx(),
  ],
  environments: {
    web: {
      output: {
        assetPrefix: "/",
      },
    },
    lynx: {},
  },
});

Puis on crée un bundle :

yarn build

On crée ensuite un projet rsbuild à la racine de notre projet Lynx :

yarn create rsbuild

Dans notre nouveau projet, on install de nouvelles dépendances :

yarn add @lynx-js/web-core @lynx-js/web-elements

On édite ensuite le fichier src/App.tsx :

import "@lynx-js/web-core/index.css";
import "@lynx-js/web-elements/index.css";
import "@lynx-js/web-core";
import "@lynx-js/web-elements/all";

const App = () => {
  return (
    <lynx-view
      style={{ height: "100vh", width: "100vw" }}
      url="/main.web.bundle"
    ></lynx-view>
  );
};

export default App;

On édite ensuite le fichier rsbuild.config.ts :

import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default defineConfig({
  plugins: [pluginReact()],
  server: {
    publicDir: [
      {
        name: path.join(__dirname, "..", "dist"),
      },
    ],
  },
});

Puis on lance notre serveur de développement :

yarn dev

L'app est disponible sur http://localhost:3000. En inspectant le HTML rendu, on remarque une structure peu conventionnelle, remplie de web components, et très peu de tags natifs. Notre animation au clic implémentée précédemment ne fonctionne pas non plus. On a ce sentiment que le support web n'est pas vraiment au point, mais il est présent !

Performances

Côté performances, Lynx est assez similaire à ce que propose React Native, mais propose une fonctionnalité supplémentaire : l'Instant First-Frame Rendering, qui est très semblable au concept de SSR que l'on connaît dans l'univers de React. L'IFR permet d'afficher le premier écran de notre application de manière quasi instantanée.

Architecturalement parlant, Lynx utilise, tout comme React Native, deux threads. Il est possible de choisir sur quel thread exécuter nos fonctions grâce aux directives 'background only' et 'main thread' en fonction du contexte d'utilisation. Par exemple pour une requête asynchrone comme l'envoi d'un formulaire, on privilégiera la directive 'background only' afin de ne pas bloquer le thread principal. Pour une fonction qui demande un résultat le plus rapidement possible, on privilégiera la directive 'main thread'. Ce concept n'est pas sans rappeler les worklets de Reanimated, permettant l'exécution de fonctions dans le thread UI ou d'autres threads.

Conclusion

Bien que prometteur sur bien des aspects, Lynx est à l'heure actuelle encore trop jeune pour être réellement utilisé de la même manière que l'on pourrait utiliser React Native. On a ce sentiment que la librairie a été dévelopée pour être intégrée à une app existante, et non pour créer une application complète. Beaucoup d'API assez essentielles sont manquantes par rapport à ce que l'on peut trouver en React Native. J'ai personnellement été surpris de voir que Lynx ne supporte l'élément input que par l'application Lynx Explorer, sans qui on ne pourrait tout simplement pas avoir de champ de saisie.

Tous ces éléments ont déjà été remontés par la communauté et seront déployés petit à petit dans les prochaines releases de 2025. Pour le moment, il est encore trop tôt pour se lancer dans la conception d'une application, mais nous allons tout de même garder un oeil sur l'évolution de la librairie.

À découvrir également

App universelle avec Expo

29 Jan 2025

App universelle avec Expo

Au fil des années, Expo a considérablement amélioré sa plage de fonctionnalités, devenant...

par

Hugo

Retour d'expérience chez 20 Minutes : s'adapter au journalisme numérique

30 May 2024

Retour d'expérience chez 20 Minutes : s'adapter au journalisme numérique

Dans cet article, vous ne trouverez pas de snippets ou de tips croustillants sur React, seulement le récit d'une aventure plutôt ordinaire à travers le développement et l'évolution d'un projet technique.

par

Vincent

Animation 3D avec React Native

21 Jul 2023

Animation 3D avec React Native

Découverte de l'univers 3D avec Expo-GL, React-three-fiber et Reanimated

par

Laureen

Premier Octet vous accompagne dans le développement de vos projets avec react-native

En savoir plusNous contacter
18 avenue Parmentier
75011 Paris
+33 1 43 57 39 11
hello@premieroctet.com

Suivez nos aventures

GitHub
X
Flux RSS
Bluesky

Naviguez à vue