React Native 0.86 en 2026 : Edge-to-Edge Android, DevTools et Questions d'Entretien

Maîtrisez React Native 0.86 avec le support edge-to-edge Android 15+, les améliorations DevTools et des questions d'entretien pratiques. Guide complet avec exemples de code.

Diagramme du flux de développement edge-to-edge Android avec React Native 0.86

React Native 0.86 introduit un support edge-to-edge complet pour Android 15+, constituant la deuxième version consécutive sans changements cassants. Publiée en juin 2026, cette version corrige les API de mise en page et de mesure critiques que l'application edge-to-edge obligatoire d'Android cassait silencieusement.

Zéro Changement Cassant

React Native 0.86 maintient une compatibilité totale avec la version 0.85. La mise à niveau ne nécessite aucune modification de code pour la plupart des applications, ce qui en fait l'une des transitions majeures les plus fluides de l'histoire de React Native.

Comprendre Edge-to-Edge sur Android 15

Les mises en page edge-to-edge permettent au contenu de s'étendre sous les éléments d'interface système comme la barre d'état et la barre de navigation. Android 15 impose ce comportement par défaut, quelle que soit la configuration de l'application. React Native 0.86 garantit que toutes les API de mesure et de mise en page fonctionnent correctement dans ces conditions.

La fonction measureInWindow retourne désormais des coordonnées précises lorsque le mode edge-to-edge est actif. Auparavant, les mesures pouvaient être décalées de la hauteur des éléments d'interface système, causant des bugs de positionnement dans les overlays, infobulles et boutons d'action flottants.

SafeAreaHandler.tsxtypescript
import React, { useRef, useCallback } from 'react';
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

interface TooltipPosition {
  x: number;
  y: number;
}

export function SafeAreaHandler() {
  const buttonRef = useRef<View>(null);
  const insets = useSafeAreaInsets();

  const showTooltip = useCallback(() => {
    // measureInWindow now returns correct coordinates on Android 15+
    buttonRef.current?.measureInWindow((x, y, width, height) => {
      const position: TooltipPosition = {
        x: x + width / 2,
        // Coordinates are accurate even with edge-to-edge enabled
        y: y + height + 8,
      };
      console.log('Tooltip position:', position);
    });
  }, []);

  return (
    <View style={[styles.container, { paddingTop: insets.top }]}>
      <View ref={buttonRef} collapsable={false}>
        <TouchableOpacity onPress={showTooltip} style={styles.button}>
          <Text style={styles.buttonText}>Show Tooltip</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
  button: { backgroundColor: '#007AFF', padding: 16, borderRadius: 8 },
  buttonText: { color: '#FFFFFF', fontWeight: '600' },
});

La prop collapsable={false} garantit que la View maintient une référence native pour les mesures. Sans cela, React Native peut optimiser la view native, causant l'échec de measureInWindow.

Corrections KeyboardAvoidingView pour Android 15

Le composant KeyboardAvoidingView calculait incorrectement l'espace disponible lorsque le mode edge-to-edge est actif. La version 0.86 corrige cela en tenant compte des insets d'interface système dans le calcul de la hauteur disponible.

ChatInput.tsxtypescript
import React, { useState } from 'react';
import {
  KeyboardAvoidingView,
  TextInput,
  Platform,
  StyleSheet,
  View,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

export function ChatInput() {
  const [message, setMessage] = useState('');
  const insets = useSafeAreaInsets();

  return (
    <KeyboardAvoidingView
      // behavior="padding" works correctly on Android 15+ edge-to-edge
      behavior={Platform.OS === 'ios' ? 'padding' : 'padding'}
      style={styles.container}
      // keyboardVerticalOffset accounts for headers and safe areas
      keyboardVerticalOffset={Platform.select({ ios: 88, android: 0 })}
    >
      <View style={styles.content}>
        {/* Chat messages would render here */}
      </View>
      <View style={[styles.inputContainer, { paddingBottom: insets.bottom }]}>
        <TextInput
          value={message}
          onChangeText={setMessage}
          placeholder="Type a message..."
          style={styles.input}
          multiline
        />
      </View>
    </KeyboardAvoidingView>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
  content: { flex: 1 },
  inputContainer: { borderTopWidth: 1, borderTopColor: '#E5E5E5', padding: 8 },
  input: { minHeight: 40, padding: 12, backgroundColor: '#F5F5F5', borderRadius: 20 },
});

Sur Android 15, utiliser behavior="height" cause souvent des sauts de mise en page. Le behavior padding offre des animations plus fluides et un positionnement plus prévisible avec edge-to-edge activé.

Mises à Jour StatusBar Pendant l'Affichage des Modales

Un bug persistant empêchait les changements de style et de visibilité de la StatusBar pendant qu'une Modal était ouverte. React Native 0.86 résout cela, permettant un theming dynamique de la barre d'état dans les flux modaux.

ThemedModal.tsxtypescript
import React, { useState, useEffect } from 'react';
import { Modal, View, StatusBar, Text, Pressable, StyleSheet } from 'react-native';

interface ThemedModalProps {
  visible: boolean;
  onClose: () => void;
  darkMode?: boolean;
}

export function ThemedModal({ visible, onClose, darkMode = false }: ThemedModalProps) {
  // StatusBar changes now apply correctly while modal is visible
  useEffect(() => {
    if (visible) {
      StatusBar.setBarStyle(darkMode ? 'light-content' : 'dark-content', true);
      StatusBar.setBackgroundColor(darkMode ? '#1A1A1A' : '#FFFFFF', true);
    }
    return () => {
      // Restore original status bar when modal closes
      StatusBar.setBarStyle('dark-content', true);
      StatusBar.setBackgroundColor('#FFFFFF', true);
    };
  }, [visible, darkMode]);

  return (
    <Modal visible={visible} animationType="slide" onRequestClose={onClose}>
      <View style={[styles.modalContent, darkMode && styles.darkBackground]}>
        <StatusBar
          barStyle={darkMode ? 'light-content' : 'dark-content'}
          backgroundColor={darkMode ? '#1A1A1A' : '#FFFFFF'}
        />
        <Text style={[styles.title, darkMode && styles.lightText]}>
          Modal Content
        </Text>
        <Pressable onPress={onClose} style={styles.closeButton}>
          <Text style={styles.closeButtonText}>Close</Text>
        </Pressable>
      </View>
    </Modal>
  );
}

const styles = StyleSheet.create({
  modalContent: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#FFFFFF' },
  darkBackground: { backgroundColor: '#1A1A1A' },
  title: { fontSize: 24, fontWeight: 'bold', color: '#000000' },
  lightText: { color: '#FFFFFF' },
  closeButton: { marginTop: 20, padding: 12, backgroundColor: '#007AFF', borderRadius: 8 },
  closeButtonText: { color: '#FFFFFF', fontWeight: '600' },
});

Cette correction est particulièrement importante pour les applications implémentant des bascules de mode sombre dans les flux modaux, comme les écrans de paramètres ou les visualiseurs d'images.

Prêt à réussir tes entretiens React Native ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Émulation Mode Clair et Sombre dans DevTools

React Native DevTools introduit le support de Emulation.setEmulatedMedia, permettant un changement de thème instantané sans modification de la configuration de l'appareil ou de l'émulateur. Cette fonctionnalité est accessible via la Palette de Commandes avec Cmd+Shift+P (macOS) ou Ctrl+Shift+P (Windows/Linux).

ThemeProvider.tsxtypescript
import React, { createContext, useContext, useState, useEffect } from 'react';
import { useColorScheme, Appearance } from 'react-native';

type Theme = 'light' | 'dark';

interface ThemeContextValue {
  theme: Theme;
  toggleTheme: () => void;
  colors: typeof lightColors;
}

const lightColors = {
  background: '#FFFFFF',
  surface: '#F5F5F5',
  text: '#000000',
  textSecondary: '#666666',
  primary: '#007AFF',
  border: '#E5E5E5',
};

const darkColors = {
  background: '#1A1A1A',
  surface: '#2D2D2D',
  text: '#FFFFFF',
  textSecondary: '#A0A0A0',
  primary: '#0A84FF',
  border: '#3D3D3D',
};

const ThemeContext = createContext<ThemeContextValue | null>(null);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  // DevTools emulation triggers colorScheme changes via setEmulatedMedia
  const systemColorScheme = useColorScheme();
  const [theme, setTheme] = useState<Theme>(systemColorScheme ?? 'light');

  useEffect(() => {
    // Listen for DevTools theme emulation changes
    const subscription = Appearance.addChangeListener(({ colorScheme }) => {
      if (colorScheme) {
        setTheme(colorScheme);
      }
    });
    return () => subscription?.remove();
  }, []);

  const toggleTheme = () => setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
  const colors = theme === 'dark' ? darkColors : lightColors;

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme, colors }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme(): ThemeContextValue {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

L'état d'émulation des médias se réinitialise lorsque DevTools se déconnecte, garantissant que les builds de production ne sont pas affectés par les tests de thème effectués pendant le développement.

Questions d'Entretien : React Native 0.86 et Edge-to-Edge

Les entretiens techniques en 2026 se concentrent de plus en plus sur la compatibilité Android 15 et l'implémentation edge-to-edge. Maîtriser ces concepts démontre une compréhension des pratiques modernes de développement mobile. Pour une préparation complète aux entretiens, explorez le module questions d'entretien sur le débogage React Native.

Qu'est-ce que le mode edge-to-edge et pourquoi Android 15 l'impose-t-il ?

Le mode edge-to-edge étend le contenu de l'application sous les éléments d'interface système (barre d'état, barre de navigation, indicateurs de gestes). Android 15 impose ce comportement par défaut pour offrir des expériences immersives et une interface cohérente sur toute la plateforme. Les applications doivent gérer les insets de zone sécurisée pour éviter que le contenu soit masqué par l'interface système.

Comment le comportement de measureInWindow change-t-il avec edge-to-edge activé ?

Avant React Native 0.86, measureInWindow retournait des coordonnées relatives à la fenêtre visible, qui excluait les zones d'interface système. Avec edge-to-edge, la fenêtre s'étend jusqu'aux bords de l'écran, mais les mesures étaient incorrectement décalées. La version 0.86 garantit que les coordonnées sont précises quel que soit l'état edge-to-edge.

Quel est le behavior KeyboardAvoidingView recommandé sur Android 15 ?

Utiliser behavior="padding" sur Android 15 avec edge-to-edge activé. Le behavior height peut causer des sauts de mise en page car le calcul de la hauteur disponible change lorsque le clavier apparaît. Le behavior padding offre des transitions plus fluides et un positionnement plus prévisible.

Comment les insets de zone sécurisée diffèrent-ils entre iOS et Android edge-to-edge ?

Sur iOS, les insets de zone sécurisée existent depuis l'iPhone X pour l'encoche et l'indicateur d'accueil. Sur Android edge-to-edge, les insets incluent la hauteur de la barre d'état, la hauteur de la barre de navigation et les zones d'indicateur de gestes. La bibliothèque react-native-safe-area-context fournit des valeurs d'insets multiplateformes via useSafeAreaInsets().

Pressable android_ripple avec PlatformColor

React Native 0.86 ajoute le support de PlatformColor à la prop android_ripple, permettant des couleurs de ripple dynamiques qui respectent les paramètres de thème système.

ThemedButton.tsxtypescript
import React from 'react';
import { Pressable, Text, PlatformColor, Platform, StyleSheet } from 'react-native';

interface ThemedButtonProps {
  title: string;
  onPress: () => void;
}

export function ThemedButton({ title, onPress }: ThemedButtonProps) {
  return (
    <Pressable
      onPress={onPress}
      style={({ pressed }) => [
        styles.button,
        pressed && styles.pressed,
      ]}
      android_ripple={{
        // PlatformColor adapts to system theme automatically
        color: Platform.OS === 'android'
          ? PlatformColor('?attr/colorControlHighlight')
          : undefined,
        borderless: false,
        foreground: true,
      }}
    >
      <Text style={styles.text}>{title}</Text>
    </Pressable>
  );
}

const styles = StyleSheet.create({
  button: {
    padding: 16,
    backgroundColor: Platform.select({
      android: PlatformColor('?attr/colorPrimary'),
      default: '#007AFF',
    }),
    borderRadius: 8,
    alignItems: 'center',
  },
  pressed: { opacity: 0.8 },
  text: { color: '#FFFFFF', fontWeight: '600', fontSize: 16 },
});

Utiliser ?attr/colorControlHighlight garantit que la couleur du ripple correspond aux directives Material Design et s'adapte aux thèmes clair/sombre configurés au niveau système.

Image.getSize Retourne des Dimensions Précises

Les méthodes Image.getSize et Image.getSizeWithHeaders retournent maintenant les dimensions originales de l'image plutôt que des valeurs sous-échantillonnées. Cette correction est essentielle pour implémenter des calculs de ratio d'aspect corrects et des mises en page d'images responsives.

ResponsiveImage.tsxtypescript
import React, { useState, useEffect } from 'react';
import { Image, View, useWindowDimensions, StyleSheet } from 'react-native';

interface ResponsiveImageProps {
  uri: string;
  maxHeight?: number;
}

export function ResponsiveImage({ uri, maxHeight = 400 }: ResponsiveImageProps) {
  const { width: screenWidth } = useWindowDimensions();
  const [dimensions, setDimensions] = useState({ width: screenWidth, height: 200 });

  useEffect(() => {
    // getSize now returns true source dimensions, not downsampled values
    Image.getSize(
      uri,
      (width, height) => {
        const aspectRatio = width / height;
        let calculatedHeight = screenWidth / aspectRatio;
        
        // Constrain to maxHeight while maintaining aspect ratio
        if (calculatedHeight > maxHeight) {
          calculatedHeight = maxHeight;
        }
        
        setDimensions({
          width: screenWidth,
          height: calculatedHeight,
        });
      },
      (error) => {
        console.error('Failed to get image size:', error);
      }
    );
  }, [uri, screenWidth, maxHeight]);

  return (
    <View style={styles.container}>
      <Image
        source={{ uri }}
        style={{ width: dimensions.width, height: dimensions.height }}
        resizeMode="cover"
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { width: '100%' },
});

Cette correction élimine le besoin de solutions de contournement qui récupéraient les dimensions d'image via des modules natifs ou le traitement d'images côté serveur.

Gestion des Grandes Réponses HTTP Sans OutOfMemoryError

Le NetworkingModule lançait auparavant une OutOfMemoryError lors du traitement de grandes réponses HTTP. La version 0.86 implémente un traitement par morceaux pour les réponses dépassant les seuils de mémoire.

LargeFileDownload.tsxtypescript
import { useState, useCallback } from 'react';

interface DownloadProgress {
  loaded: number;
  total: number;
  percentage: number;
}

export function useLargeFileDownload() {
  const [progress, setProgress] = useState<DownloadProgress | null>(null);
  const [error, setError] = useState<string | null>(null);

  const downloadFile = useCallback(async (url: string): Promise<Blob | null> => {
    try {
      setError(null);
      setProgress({ loaded: 0, total: 0, percentage: 0 });

      const response = await fetch(url);
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      const contentLength = response.headers.get('Content-Length');
      const total = contentLength ? parseInt(contentLength, 10) : 0;

      // NetworkingModule now handles large responses without OutOfMemoryError
      const reader = response.body?.getReader();
      if (!reader) {
        throw new Error('Response body is not readable');
      }

      const chunks: Uint8Array[] = [];
      let loaded = 0;

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        
        chunks.push(value);
        loaded += value.length;
        
        setProgress({
          loaded,
          total,
          percentage: total > 0 ? Math.round((loaded / total) * 100) : 0,
        });
      }

      const blob = new Blob(chunks);
      setProgress({ loaded, total, percentage: 100 });
      return blob;
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Download failed';
      setError(message);
      return null;
    }
  }, []);

  return { downloadFile, progress, error };
}

Pour les téléchargements de fichiers dépassant 50 Mo, il est recommandé d'utiliser react-native-blob-util ou des bibliothèques similaires de gestion de fichiers natifs pour une meilleure gestion de la mémoire.

L'en-tête Cookie passé via le constructeur WebSocket était supprimé avant d'atteindre le serveur. Cela empêchait l'authentification basée sur les sessions pour les connexions WebSocket.

AuthenticatedWebSocket.tstypescript
type MessageHandler = (data: string) => void;
type ErrorHandler = (error: Event) => void;

interface WebSocketOptions {
  url: string;
  sessionCookie: string;
  onMessage: MessageHandler;
  onError?: ErrorHandler;
}

export function createAuthenticatedWebSocket({
  url,
  sessionCookie,
  onMessage,
  onError,
}: WebSocketOptions): WebSocket {
  // Cookie header now reaches the server correctly in 0.86
  const ws = new WebSocket(url, undefined, {
    headers: {
      Cookie: sessionCookie,
      // Additional headers as needed
      'X-Client-Version': '1.0.0',
    },
  });

  ws.onopen = () => {
    console.log('WebSocket connected with session cookie');
  };

  ws.onmessage = (event) => {
    if (typeof event.data === 'string') {
      onMessage(event.data);
    }
  };

  ws.onerror = (error) => {
    console.error('WebSocket error:', error);
    onError?.(error);
  };

  ws.onclose = (event) => {
    console.log(`WebSocket closed: ${event.code} ${event.reason}`);
  };

  return ws;
}

Cette correction est critique pour les applications utilisant la gestion de session basée sur les cookies parallèlement aux fonctionnalités en temps réel. Le module questions d'entretien sur le réseau React Native couvre en profondeur les patterns d'implémentation WebSocket.

Conclusion

  • React Native 0.86 apporte les corrections edge-to-edge obligatoires pour Android 15 sans changements cassants
  • measureInWindow, KeyboardAvoidingView et Dimensions fonctionnent correctement avec edge-to-edge activé
  • Les mises à jour de la StatusBar s'appliquent maintenant pendant que les composants Modal sont visibles
  • L'émulation de thème DevTools accélère le développement et les tests du mode sombre
  • Le support PlatformColor dans android_ripple permet un retour tactile adapté au thème
  • Image.getSize retourne des dimensions originales précises pour les mises en page responsives
  • Les grandes réponses HTTP ne causent plus d'OutOfMemoryError
  • Les en-têtes Cookie WebSocket atteignent le serveur comme prévu

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#react-native
#android
#mobile-development
#edge-to-edge
#devtools

Partager

Articles similaires