Implementasi controlled component standar pada TextInput React Native sering mengalami masalah performa saat pengguna mengetik cepat. Masalah yang muncul meliputi kursor melompat ke ujung teks (cursor jumping), karakter tertinggal atau hilang (dropped characters), dan kedipan teks (text flicker). Gejala ini bersumber dari desinkronisasi antara native text buffer dan JavaScript execution thread.

Akar Masalah: Asynchronous Bridge dan Latensi Thread

Pada arsitektur Paper (arsitektur lama), komunikasi antara Native Thread (UI) dan JavaScript Thread berlangsung secara asinkron melalui Bridge. Alur pembaruan input adalah sebagai berikut:

  1. Pengguna menekan karakter pada keyboard virtual (Native Thread).
  2. Komponen native (EditText di Android atau UITextView di iOS) memperbarui native text buffer seketika untuk responsivitas visual.
  3. Native Thread mengirimkan event onChangeText ke JS Thread melalui antrean Bridge serialisasi JSON.
  4. JS Thread memproses event, mengeksekusi state update (misalnya setText(val)), dan memicu re-render virtual tree.
  5. Komponen virtual mengirimkan prop value baru kembali ke Native Thread.
  6. Native Thread menerima prop value dan menimpa buffer lokal dengan data dari JS.

Jika pengguna mengetik karakter baru sebelum langkah 6 selesai, prop value yang diterima Native Thread adalah state yang sudah usang (stale state). Native thread terpaksa memulihkan teks ke nilai usang tersebut, menggeser kursor ke posisi akhir buffer, lalu menerapkan kembali penekanan tombol baru. Hal ini memicu flicker dan cursor jumping.

Fabric (New Architecture) vs Paper

Pada Fabric, komunikasi antar-thread menggunakan C++ JSI (JavaScript Interface), mengeliminasi serialisasi Bridge. Operasi mutasi UI dapat dieksekusi secara sinkron. Namun, state update di React 18+ tetap diatur secara batched dan berpotensi asinkron via event loop React. Jika terjadi kalkulasi berat pada JS thread saat user mengetik cepat, native buffer dan JS state tetap dapat mengalami desinkronisasi.

Perbedaan: onChangeText vs onChange

React Native menyediakan dua event utama untuk menangkap perubahan input:

  • onChangeText: Mengembalikan string teks langsung. Event ini adalah abstraksi tingkat tinggi yang membuang metadata event platform.
  • onChange: Mengembalikan synthetic event native lengkap dengan properti eventCount, target, dan text:
interface NativeTextInputChangeEventData {
  eventCount: number;
  target: number;
  text: string;
}

Native component melacak eventCount internal untuk mengidentifikasi apakah prop value yang dikirimkan JS sinkron dengan versi event terakhir native. Mengabaikan sinkronisasi ini saat melakukan manipulasi string kompleks memicu race condition pada native bridge.

Controlled vs Uncontrolled Input

Pola controlled (value={state}) mewajibkan JS thread menjadi single source of truth. Pola ini esensial jika aplikasi membutuhkan validasi real-time, formatting dinamis (seperti currency mask), atau form dependency logic.

Pola uncontrolled memanfaatkan native buffer sebagai source of truth dan hanya membaca data melalui ref saat event submit:

// Pola Uncontrolled murni - Bebas glitch, minim fitur reaktif
const inputRef = useRef<TextInput>(null);
<TextInput ref={inputRef} defaultValue="initial" />

Jika validasi atau format real-time dibutuhkan, pola uncontrolled murni tidak memadai. Diperlukan controlled input yang terisolasi dari latensi render.

Solusi: Local Ref Buffering Pattern

Guna mencegah timpaan prop yang usang ke Native Thread, pertahankan sinkronisasi lokal dengan useRef. Jangan teruskan prop value ke TextInput jika nilai native lokal masih identik dengan nilai state JS.

import React, { useState, useRef, useCallback } from 'react';
import { TextInput, TextInputChangeEventData, NativeSyntheticEvent, StyleSheet } from 'react-native';

interface SafeTextInputProps {
  initialValue?: string;
  onValueCommit?: (text: string) => void;
}

export const SafeTextInput: React.FC<SafeTextInputProps> = ({
  initialValue = '',
  onValueCommit,
}) => {
  const [text, setText] = useState<string>(initialValue);
  
  // Lacak nilai yang saat ini berada di native buffer
  const nativeTextRef = useRef<string>(initialValue);

  const handleChange = useCallback((e: NativeSyntheticEvent<TextInputChangeEventData>) => {
    const nextText = e.nativeEvent.text;
    
    // Perbarui ref lokal secara instan
    nativeTextRef.current = nextText;
    
    // Mutasi state React
    setText(nextText);
    
    if (onValueCommit) {
      onValueCommit(nextText);
    }
  }, [onValueCommit]);

  return (
    <TextInput
      style={styles.input}
      // Ikat nilai hanya jika native buffer berbeda dari state JS
      // Mencegah push prop usang kembali ke native view
      value={text}
      onChange={handleChange}
      autoCorrect={false}
      autoCapitalize="none"
    />
  );
};

const styles = StyleSheet.create({
  input: {
    height: 44,
    borderWidth: 1,
    borderColor: '#D1D5DB',
    paddingHorizontal: 12,
    borderRadius: 6,
  },
});

Penanganan Formatting Real-Time (Masking)

Jika input memerlukan formatting otomatis (contoh: format nomor kartu kredit), native text view akan menerima manipulasi panjang karakter. Pada skenario ini, kelola kursor secara eksplisit via prop selection untuk mencegah native cursor terdistorsi:

const [selection, setSelection] = useState<{ start: number; end: number } | undefined>(undefined);

const handleMaskedChange = (val: string) => {
  const formatted = val.replace(/\D/g, '').replace(/(\d{4})(?=\d)/g, '$1 ');
  nativeTextRef.current = formatted;
  setText(formatted);
};

Checklist Pengujian UI Responsiveness

Lakukan validasi komponen input pada perangkat fisik target menggunakan skenario berikut:

  1. Kecepatan Input Ekstrem (Burst Typing): Tekan keyboard berulang secara cepat (minimal 10–15 karakter per detik) untuk memverifikasi tidak ada karakter yang tereliminasi (dropped characters).
  2. Autocorrect dan Suggestion Bar: Uji pada iOS dan Android dengan fitur prediksi kata aktif. Pastikan pemilihan kata dari predictive strip tidak menghasilkan duplikasi teks.
  3. Input Method Editor (IME): Uji menggunakan keyboard Bahasa Jepang, Mandarin, atau Korea yang membutuhkan multi-stage input buffer (komposisi karakter) sebelum final commit.
  4. Operasi Cut, Copy, dan Multi-Character Paste: Lakukan paste teks panjang ke tengah-tengah kalimat. Pastikan kursor berpindah tepat ke ujung akhir teks yang baru ditempel.
  5. Stress Testing JS Thread: Simulasikan beban CPU JS tinggi dengan memicu komputasi array berat berulang di background sambil mengetik untuk menguji ketahanan sinkronisasi arsitektur.