Noqta
  • Accueil
  • Services
  • À propos
  • Écrits
  • Se connecter
écrits/tutorial/2026/07
● Tutorial12 juil. 2026·26 min

Créer des applications mobiles multiplateformes avec Lynx et ReactLynx : guide complet

Apprenez à créer une application mobile multiplateforme au rendu natif avec Lynx, le framework open source de ByteDance, et ReactLynx. Ce tutoriel pratique couvre la mise en place du projet avec Rspeedy, les éléments Lynx, le style en CSS, la gestion d'état, les listes performantes, la récupération de données et le scripting sur le thread principal pour des interactions ultra-fluides.

AI Bot
AI Bot
Author
·EN · FR · AR

React Native et Flutter dominent le développement mobile multiplateforme depuis des années, mais un concurrent sérieux vient d'entrer dans l'arène. Lynx est le framework open source construit par ByteDance — le moteur même qui propulse des surfaces au sein de TikTok utilisées quotidiennement par des centaines de millions de personnes. Sa promesse est audacieuse : écrire une seule fois avec les technologies web familières (CSS et React), effectuer un rendu avec une interface réellement native, et garder des interactions fluides grâce à une architecture à double thread unique.

Dans ce tutoriel, vous allez construire une application mobile fonctionnelle avec Lynx et ReactLynx (sa déclinaison officielle basée sur React) en partant de zéro : création du projet avec Rspeedy, composition de l'interface avec les éléments Lynx, style avec du vrai CSS, gestion d'état, rendu de listes performantes, récupération de données distantes et scripting sur le thread principal pour éliminer toute latence d'interaction.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 18+ installé (Node 20 LTS recommandé)
  • Des connaissances de base en React (composants, hooks, props)
  • Une familiarité avec les fondamentaux de TypeScript
  • Un smartphone (iOS ou Android) pour prévisualiser votre application avec l'app LynxExplorer, ou un simulateur
  • Un éditeur de code (VS Code recommandé)

Aucune expérience préalable en développement mobile n'est requise — c'est précisément l'argument de vente de Lynx.

Ce que vous allez construire

Vous allez construire DevPulse, une petite application mobile complète qui affiche un fil défilant d'outils développeur tendance. En chemin, vous apprendrez :

  • Comment l'architecture à double thread de Lynx garde l'interface réactive
  • Comment composer des écrans à partir des éléments <view>, <text>, <image> et <list>
  • Comment styler avec du CSS standard — dégradés, transitions et animations inclus
  • Comment gérer les taps et l'état avec les hooks React que vous connaissez déjà
  • Comment récupérer et afficher des données distantes
  • Comment déplacer le code critique pour l'interaction vers le thread principal avec la directive 'main thread'

Comprendre l'architecture de Lynx

Avant d'écrire du code, il est utile de comprendre pourquoi Lynx se distingue de React Native.

La plupart des frameworks mobiles pilotés par JavaScript exécutent tout votre code sur un seul thread JavaScript. Quand ce thread est occupé à réconcilier des composants ou à traiter des données, les taps et le défilement saccadent. Lynx répartit le travail sur deux threads :

  1. Le thread principal gère le rendu de l'interface, les gestes tactiles et les mises à jour visuelles synchrones. Il exécute un moteur léger appelé PrimJS.
  2. Le thread d'arrière-plan exécute la logique de votre application — code des composants, effets, récupération de données et règles métier.

Résultat : même si la logique de votre application est occupée, le défilement et le retour tactile restent fluides, car le thread principal n'est jamais bloqué par votre code métier. Lynx prend aussi en charge l'Instant First-Frame Rendering (IFR), qui produit un premier écran visible et non vide de manière synchrone au lancement.

ReactLynx est le modèle de programmation basé sur React au-dessus de ce moteur. Vous écrivez des composants avec JSX et des hooks, et le framework décide de ce qui s'exécute où.

Étape 1 : Créer le projet avec Rspeedy

Les projets Lynx se construisent avec Rspeedy, un outil de build basé sur Rspack offrant un bundling rapide et le rechargement à chaud. Créez un nouveau projet :

npm create rspeedy@latest

L'assistant interactif pose quelques questions. Choisissez ces réponses :

? Project name: dev-pulse
? Select framework: ReactLynx
? Select language: TypeScript
? Select additional tools: (rien pour l'instant)

Installez ensuite les dépendances et lancez le serveur de développement :

cd dev-pulse
npm install
npm run dev

Le terminal affiche un QR code. Pour prévisualiser l'application sur un appareil réel :

  1. Installez l'application LynxExplorer sur votre téléphone (disponible pour les simulateurs iOS et Android sous forme de binaires précompilés sur le site officiel de Lynx).
  2. Scannez le QR code avec LynxExplorer.
  3. Votre application apparaît sur l'appareil, avec le rechargement à chaud actif — modifiez le code et regardez la mise à jour en direct.

Étape 2 : Explorer la structure du projet

Ouvrez le projet dans votre éditeur. Les fichiers importants :

dev-pulse/
├── lynx.config.ts        # Configuration de build Rspeedy
├── src/
│   ├── index.tsx         # Point d'entrée, monte le composant racine
│   ├── App.tsx           # Composant racine
│   └── App.css           # Styles CSS standard
├── package.json
└── tsconfig.json

Le point d'entrée est d'une simplicité rafraîchissante :

// src/index.tsx
import { root } from '@lynx-js/react'
import { App } from './App.js'
 
root.render(<App />)

Remarquez que l'import provient de @lynx-js/react, et non de react. ReactLynx fournit la même API de hooks (useState, useEffect, useCallback et compagnie), adaptée au runtime à double thread.

Étape 3 : Composer l'interface avec les éléments Lynx

Lynx ne rend pas du HTML. Il embarque à la place un ensemble d'éléments intégrés qui correspondent à des vues natives sur chaque plateforme :

Élément LynxÉquivalent web le plus procheRendu natif
<view><div>UIView / ViewGroup
<text><span> / <p>UILabel / TextView
<image><img>Vue image native
<scroll-view><div> défilantConteneur de défilement natif
<list>liste virtualiséeListe native avec recyclage

Une règle à retenir : tout texte doit vivre à l'intérieur d'un élément <text>. Placer du texte brut directement dans une <view> ne produira aucun rendu.

Remplacez src/App.tsx par une première version de l'en-tête de DevPulse :

// src/App.tsx
import './App.css'
 
export function App() {
  return (
    <view className="page">
      <view className="header">
        <text className="header-title">DevPulse</text>
        <text className="header-subtitle">
          Trending developer tools, refreshed daily
        </text>
      </view>
    </view>
  )
}

Étape 4 : Styler avec du vrai CSS

Contrairement aux objets de style JavaScript de React Native, Lynx prend en charge de véritables fichiers CSS — sélecteurs, propriétés personnalisées, dégradés, transitions et animations keyframes inclus. Remplacez src/App.css :

/* src/App.css */
.page {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background-color: #0b1020;
}
 
.header {
  padding: 48px 24px 24px;
  background: linear-gradient(135deg, #1e2a5a 0%, #0b1020 100%);
}
 
.header-title {
  font-size: 32px;
  font-weight: 700;
  color: #ffffff;
}
 
.header-subtitle {
  margin-top: 6px;
  font-size: 14px;
  color: #8b93b5;
}

Enregistrez : le rechargement à chaud met l'appareil à jour instantanément. Flexbox est le système de mise en page par défaut, et display: flex se comporte exactement comme sur le web.

Une note sur les unités : Lynx prend en charge px ainsi que des unités responsives comme rpx (pixel responsif, relatif à la largeur de l'écran), pratique pour des tailles cohérentes entre appareils.

Étape 5 : État et événements

Les gestionnaires d'événements dans Lynx utilisent le préfixe bind au lieu du préfixe on de React DOM. L'événement tap — l'équivalent mobile du clic — se câble avec bindtap. L'état fonctionne exactement comme dans React standard.

Ajoutez un filtre de catégories interactif dans App.tsx :

import { useState } from '@lynx-js/react'
import './App.css'
 
const CATEGORIES = ['All', 'AI', 'Frontend', 'DevOps'] as const
 
export function App() {
  const [active, setActive] = useState('All')
 
  return (
    <view className="page">
      <view className="header">
        <text className="header-title">DevPulse</text>
        <text className="header-subtitle">
          Trending developer tools, refreshed daily
        </text>
      </view>
 
      <view className="filters">
        {CATEGORIES.map((cat) => (
          <view
            key={cat}
            className={active === cat ? 'chip chip-active' : 'chip'}
            bindtap={() => setActive(cat)}
          >
            <text className="chip-label">{cat}</text>
          </view>
        ))}
      </view>
    </view>
  )
}

Puis stylez les puces :

.filters {
  display: flex;
  flex-direction: row;
  gap: 8px;
  padding: 16px 24px;
}
 
.chip {
  padding: 8px 16px;
  border-radius: 999px;
  background-color: #1a2340;
  transition: background-color 0.2s ease;
}
 
.chip-active {
  background-color: #4f6ef7;
}
 
.chip-label {
  font-size: 13px;
  color: #ffffff;
}

Tapez sur une puce sur votre appareil : la surbrillance se déplace instantanément. Sous le capot, votre gestionnaire s'exécute sur le thread d'arrière-plan, mais Lynx garde le retour visuel vif — et à l'étape 8, vous verrez comment rendre le retour critique littéralement instantané.

Étape 6 : Afficher un fil performant avec l'élément list

Pour les fils longs, <scroll-view> rend tout de manière anticipée — acceptable pour un écran de réglages, gaspilleur pour un fil de contenu. L'élément <list> vous offre un recyclage natif des cellules, comme FlatList dans React Native ou RecyclerView sur Android.

Définissez d'abord le modèle de données et quelques données initiales. Créez src/data.ts :

// src/data.ts
export interface Tool {
  id: number
  name: string
  tagline: string
  category: 'AI' | 'Frontend' | 'DevOps'
  stars: number
}
 
export const SEED_TOOLS: Tool[] = [
  { id: 1, name: 'Lynx', tagline: 'Native UI from web tech', category: 'Frontend', stars: 12400 },
  { id: 2, name: 'Rspeedy', tagline: 'Rspack-powered Lynx builds', category: 'Frontend', stars: 3800 },
  { id: 3, name: 'Claude Agent SDK', tagline: 'Build production AI agents', category: 'AI', stars: 9600 },
  { id: 4, name: 'OpenTUI', tagline: 'React for the terminal', category: 'Frontend', stars: 5100 },
  { id: 5, name: 'Kamal', tagline: 'Zero-downtime VPS deploys', category: 'DevOps', stars: 8700 },
  { id: 6, name: 'uv', tagline: 'Blazing-fast Python packaging', category: 'DevOps', stars: 31000 },
]

Créez maintenant le composant du fil dans src/Feed.tsx :

// src/Feed.tsx
import type { Tool } from './data.js'
 
interface FeedProps {
  tools: Tool[]
}
 
export function Feed({ tools }: FeedProps) {
  return (
    <list
      className="feed"
      list-type="single"
      span-count={1}
      scroll-orientation="vertical"
    >
      {tools.map((tool) => (
        <list-item
          item-key={`tool-${tool.id}`}
          key={`tool-${tool.id}`}
        >
          <view className="card">
            <view className="card-row">
              <text className="card-name">{tool.name}</text>
              <text className="card-stars">★ {tool.stars}</text>
            </view>
            <text className="card-tagline">{tool.tagline}</text>
            <view className="card-badge">
              <text className="card-badge-text">{tool.category}</text>
            </view>
          </view>
        </list-item>
      ))}
    </list>
  )
}

Détails clés :

  • Chaque enfant de <list> doit être un <list-item> avec un item-key unique — c'est ce qui permet le recyclage.
  • list-type="single" rend une liste à une colonne ; les grilles et les mises en page en cascade sont également prises en charge.

Branchez-le dans App.tsx en appliquant le filtre de catégorie :

import { useState } from '@lynx-js/react'
import { Feed } from './Feed.js'
import { SEED_TOOLS } from './data.js'
import './App.css'
 
const CATEGORIES = ['All', 'AI', 'Frontend', 'DevOps'] as const
 
export function App() {
  const [active, setActive] = useState('All')
 
  const visible =
    active === 'All'
      ? SEED_TOOLS
      : SEED_TOOLS.filter((t) => t.category === active)
 
  return (
    <view className="page">
      {/* en-tête et filtres inchangés */}
      <view className="filters">
        {CATEGORIES.map((cat) => (
          <view
            key={cat}
            className={active === cat ? 'chip chip-active' : 'chip'}
            bindtap={() => setActive(cat)}
          >
            <text className="chip-label">{cat}</text>
          </view>
        ))}
      </view>
 
      <Feed tools={visible} />
    </view>
  )
}

Ajoutez les styles des cartes :

.feed {
  flex: 1;
  padding: 0 16px;
}
 
.card {
  margin-bottom: 12px;
  padding: 18px;
  border-radius: 16px;
  background-color: #141c36;
}
 
.card-row {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
}
 
.card-name {
  font-size: 18px;
  font-weight: 600;
  color: #ffffff;
}
 
.card-stars {
  font-size: 13px;
  color: #f7c948;
}
 
.card-tagline {
  margin-top: 4px;
  font-size: 14px;
  color: #8b93b5;
}
 
.card-badge {
  margin-top: 12px;
  align-self: flex-start;
  padding: 4px 10px;
  border-radius: 6px;
  background-color: #1e2a5a;
}
 
.card-badge-text {
  font-size: 11px;
  color: #9db1ff;
}

Faites défiler le fil sur votre appareil. Grâce au recyclage natif des cellules de <list>, la fluidité tient même avec des milliers d'éléments.

Étape 7 : Récupérer des données distantes

Les vraies applications chargent leurs données depuis une API. Lynx fournit une API fetch sur le thread d'arrière-plan, et comme le code de vos composants s'y exécute déjà, le motif ressemble au React de tous les jours :

// src/App.tsx (partie chargement des données)
import { useEffect, useState } from '@lynx-js/react'
import type { Tool } from './data.js'
import { SEED_TOOLS } from './data.js'
 
export function useTools() {
  const [tools, setTools] = useState<Tool[]>(SEED_TOOLS)
  const [loading, setLoading] = useState(false)
 
  useEffect(() => {
    setLoading(true)
    fetch('https://api.example.com/trending-tools')
      .then((res) => res.json())
      .then((data: Tool[]) => setTools(data))
      .catch(() => {
        // On garde les données initiales comme repli élégant
      })
      .finally(() => setLoading(false))
  }, [])
 
  return { tools, loading }
}

Remplacez SEED_TOOLS dans App par la valeur retournée par le hook et affichez un état de chargement léger :

const { tools, loading } = useTools()
 
// dans le JSX :
{loading ? (
  <view className="loading">
    <text className="loading-text">Loading tools…</text>
  </view>
) : (
  <Feed tools={visible} />
)}

Comme la récupération se produit sur le thread d'arrière-plan, une réponse réseau lente ne gèle jamais le défilement ni les taps.

Étape 8 : Le scripting sur le thread principal pour un retour instantané

C'est ici que Lynx se distingue réellement de ses concurrents. Les gestionnaires définis normalement s'exécutent sur le thread d'arrière-plan, ce qui implique un saut asynchrone pour chaque événement. Pour la plupart des interactions, c'est imperceptible, mais pour les interfaces pilotées par les gestes (glisser, balayer, retour à la pression), vous voulez une réponse sans aucune latence.

Lynx vous permet de marquer une fonction pour qu'elle s'exécute directement sur le thread principal avec la directive 'main thread', et de la lier avec le préfixe main-thread: :

import { useMainThreadRef } from '@lynx-js/react'
import type { MainThread } from '@lynx-js/types'
 
export function PressableCard(props: { children: React.ReactNode }) {
  const cardRef = useMainThreadRef<MainThread.Element>(null)
 
  function onPressDown(e: MainThread.TouchEvent) {
    'main thread'
    cardRef.current?.setStyleProperty('transform', 'scale(0.97)')
  }
 
  function onPressUp(e: MainThread.TouchEvent) {
    'main thread'
    cardRef.current?.setStyleProperty('transform', 'scale(1)')
  }
 
  return (
    <view
      main-thread:ref={cardRef}
      main-thread:bindtouchstart={onPressDown}
      main-thread:bindtouchend={onPressUp}
      className="card"
    >
      {props.children}
    </view>
  )
}

L'effet de réduction à la pression s'exécute de manière synchrone avec l'événement tactile — pas de saut entre threads, pas d'images perdues, même si le thread d'arrière-plan est occupé à analyser une grosse réponse API. C'est la technique derrière la sensation « instantanée » des applications de production propulsées par Lynx.

Deux règles pour les fonctions du thread principal :

  • Elles doivent commencer par la directive 'main thread' comme première instruction.
  • Elles ne peuvent toucher que des valeurs du thread principal (les refs créées avec useMainThreadRef, les objets d'événement et d'autres fonctions du thread principal) — pas directement votre état React.

Tester votre implémentation

Vérifiez chaque capacité avant de livrer :

  1. Rechargement à chaud : modifiez le texte header-title et confirmez que l'appareil se met à jour sans redémarrage.
  2. Filtrage : tapez sur chaque puce de catégorie et confirmez que le fil se met à jour et que la surbrillance active se déplace.
  3. Défilement : faites défiler la liste rapidement — la cadence d'images doit rester fluide grâce au recyclage natif.
  4. Données de repli : activez le mode avion et relancez ; les données initiales doivent s'afficher au lieu d'un écran vide.
  5. Retour à la pression : maintenez une carte pressée — la réduction d'échelle doit être instantanée, sans délai perceptible.

Pour produire un bundle de release, exécutez :

npm run build

Rspeedy génère un bundle Lynx optimisé, prêt à être chargé par une coquille hôte native (LynxExplorer pendant le développement, ou votre propre application coquille iOS/Android en production).

Dépannage

Le texte n'apparaît pas. Les chaînes brutes à l'intérieur d'une <view> ne sont pas rendues. Enveloppez chaque chaîne dans un élément <text>.

Le scan du QR code échoue. Assurez-vous que votre téléphone et votre ordinateur sont sur le même réseau. Si l'adresse du serveur de développement est inaccessible, passez un hôte explicite : npm run dev -- --host 192.168.1.x.

Les éléments de liste se ressemblent tous ou se recyclent mal. Vérifiez que chaque <list-item> possède un item-key unique et stable. Réutiliser des clés casse le recycleur.

La fonction 'main thread' lève une erreur d'accès aux valeurs. Les fonctions du thread principal ne peuvent pas capturer l'état du thread d'arrière-plan. Passez les données via des valeurs main-thread:ref ou les paramètres d'événement.

Des styles sont silencieusement ignorés. Lynx prend en charge un sous-ensemble large mais incomplet de CSS. Privilégiez les propriétés de mise en page Flexbox ; consultez la référence CSS officielle quand une propriété reste sans effet.

Prochaines étapes

  • Ajoutez un écran de détail et explorez les options de routage de Lynx
  • Remplacez le point de terminaison fictif par une vraie API et ajoutez le tirer-pour-rafraîchir avec les événements de <scroll-view>
  • Explorez l'intégration d'une vue Lynx dans une application native iOS/Android existante — Lynx est conçu pour une adoption incrémentale
  • Comparez avec notre tutoriel Expo React Native et notre tutoriel Tauri 2 pour le bureau pour choisir le bon outil multiplateforme selon le projet
  • Vous intégrez des fonctionnalités IA dans votre application mobile ? Consultez notre guide Claude Agent SDK en TypeScript

Conclusion

Vous avez construit une application mobile multiplateforme complète avec Lynx et ReactLynx : créée avec Rspeedy, composée d'éléments au rendu natif, stylée avec du vrai CSS, animée par les hooks React familiers, et optimisée avec une liste à recyclage plus le scripting sur le thread principal pour un retour tactile instantané.

L'architecture à double thread de Lynx est une remise en question significative de la pile mobile pilotée par JavaScript — le thread d'interface n'est jamais otage de votre logique métier. Avec ByteDance qui l'éprouve à l'échelle de TikTok et un outillage désormais open source, Lynx a gagné sa place sur votre liste restreinte aux côtés de React Native et Flutter pour votre prochain projet mobile.

● Tags
#lynx#react#mobile#cross-platform#typescript#intermediate#26 min de lecture
● Partage
● Une question ?

Discutez de cet article avec un agent Noqta.

AI Bot
AI Bot
Author · noqta
Suivre ↗

● À lire ensuite

Protocole A2A en TypeScript : construire des agents IA interopérables avec le standard Agent2Agent (2026)
● Tutorial

Protocole A2A en TypeScript : construire des agents IA interopérables avec le standard Agent2Agent (2026)

6 juil. 2026
Créer un agent ACP en TypeScript : connecter n'importe quel éditeur à votre agent IA (2026)
● Tutorial

Créer un agent ACP en TypeScript : connecter n'importe quel éditeur à votre agent IA (2026)

30 juin 2026
Protocole AG-UI avec Next.js : diffuser vos agents IA vers le frontend avec des événements typés (2026)
● Tutorial

Protocole AG-UI avec Next.js : diffuser vos agents IA vers le frontend avec des événements typés (2026)

9 juil. 2026
Noqta
Conditions générales · Politique de Confidentialité
Services
  • Automatisation IA
  • Agents IA
  • Automatisation CX
  • Vibe Coding
  • Gestion de Projet
  • Assurance Qualité
  • Développement Web
  • Intégration API
  • Applications Métier
  • Maintenance
  • Low-Code/No-Code
Liens
  • À propos de nous
  • Comment ça marche?
  • Actualités
  • Tutoriels
  • Blog
  • Contact
  • FAQ
  • Ressources
Régions
  • Arabie Saoudite
  • Émirats Arabes Unis
  • Qatar
  • Bahreïn
  • Oman
  • Libye
  • Tunisie
  • Algérie
  • Maroc
Entreprise
  • Noqta, Tunisie, Tunis, téléphone +216 40 385 594
© Noqta. Tous droits réservés.