Bài viết tổng hợp các câu hỏi phỏng vấn React Native phổ biến dành cho lập trình viên React Native, giúp hiểu rõ hơn về các kiến thức kỹ thuật quan trọng mà các ứng viên cần nắm vững. Từ việc trả lời các câu hỏi liên quan đến Redux, API, async/await, cho đến việc giải thích các khái niệm như Metro Bundler hay TurboModules, các câu hỏi này không chỉ kiểm tra khả năng nắm vững công nghệ mà còn đánh giá cách thức giải quyết vấn đề và tư duy logic của các lập trình viên React Native.

Đọc bài viết này để hiểu thêm về:

  • Câu hỏi phỏng vấn React Native cơ bản về React Native 
  • Câu hỏi phỏng vấn React Native về thiết kế và cấu trúc ứng dụng
  • Câu hỏi phỏng vấn React Native về các thành phần giao diện 
  • Câu hỏi phỏng vấn React Native về xử lý dữ liệu và API
  • Câu hỏi phỏng vấn React Native về tích hợp Native Modules và Native Code 
  • Câu hỏi phỏng vấn React Native về kiểm thử 
  • Câu hỏi phỏng vấn React Native về hiệu suất và tối ưu hoá
  • Câu hỏi phỏng vấn React Native về công cụ và thư viện hỗ trợ

React Native Developer là ai? Vai trò của React Native Developer là gì?

Lập trình viên React Native là những chuyên gia trong việc phát triển các ứng dụng di động đa nền tảng, cho phép tạo ra sản phẩm hoạt động mượt mà trên cả iOS và Android. Với vai trò này, lập trình viên cần có kiến thức vững vàng về JavaScript và React, hiểu biết sâu sắc về hệ sinh thái React Native, và nắm bắt các khái niệm cơ bản như state management, navigation, và API integration.

Ngoài ra, khả năng giải quyết vấn đề, tối ưu hóa hiệu suất, và hiểu biết về các nguyên tắc thiết kế giao diện người dùng cũng là những kỹ năng quan trọng.

Để thành công, lập trình viên React Native không chỉ cần kỹ năng lập trình tốt mà còn phải có khả năng làm việc nhóm, tư duy sáng tạo và khả năng giao tiếp hiệu quả trong việc giải quyết các thách thức công nghệ và phối hợp với các thành viên khác trong dự án.

Đọc thêm: React Native: Chi tiết React Native là gì và Cách sử dụng A-Z

Câu hỏi phỏng vấn React Native cơ bản

React Native là gì? Điểm khác biệt chính giữa React Native và React.js?

React Native là một framework mã nguồn mở được Facebook phát triển, cho phép các lập trình viên xây dựng ứng dụng di động cho cả hai nền tảng iOS và Android bằng JavaScript và React.

Điểm nổi bật của React Native là khả năng tạo ra ứng dụng mobile sử dụng các thành phần giao diện gốc (native components), giúp ứng dụng có hiệu suất và trải nghiệm người dùng gần giống như một ứng dụng native.

Điểm khác biệt chính giữa React Native và React.js:

Tiêu chí  React Native React.js
Mục đích sử dụng Phát triển ứng dụng di động (iOS và Android). Phát triển ứng dụng web.
Thành phần UI Sử dụng các thành phần native như View, Text, Image. Sử dụng các thành phần web như div, span, img.
Khả năng tái sử dụng mã nguồn Có thể tái sử dụng nhiều mã nguồn giữa các nền tảng di động. Chủ yếu sử dụng cho các ứng dụng web và không tương thích với ứng dụng mobile.
Giao diện người dùng (UI) Tạo giao diện native với các thành phần native UI. Tạo giao diện trên web sử dụng HTML và CSS.
Công cụ phát triển Sử dụng các công cụ như Expo, React Native CLI. Sử dụng Webpack, Babel, và các công cụ khác cho web.
Hiệu suất Hiệu suất gần tương đương với ứng dụng native. Hiệu suất phụ thuộc vào trình duyệt và JavaScript engine.
Khả năng truy cập phần cứng Có thể truy cập dễ dàng vào phần cứng như máy ảnh, GPS. Truy cập phần cứng hạn chế, cần sử dụng API của trình duyệt.

Các lợi ích của việc sử dụng React Native cho phát triển ứng dụng mobile?

Dưới đây là một số lợi ích nổi bật của React Native mà ứng viên có thể nhấn mạnh:

  • Phát triển đa nền tảng (Cross-platform): React Native cho phép phát triển ứng dụng cùng lúc cho cả iOS và Android chỉ với một mã nguồn duy nhất. Điều này giúp tiết kiệm thời gian và công sức khi không cần phải viết mã riêng cho từng nền tảng, từ đó tăng tốc độ phát triển sản phẩm.
  • Tiết kiệm chi phí phát triển: Do sử dụng chung một mã nguồn, chi phí phát triển và bảo trì ứng dụng được giảm thiểu đáng kể. Điều này rất hữu ích đối với các startup hoặc các công ty có ngân sách hạn chế.
  • Hiệu suất gần như ứng dụng gốc (Near-native performance): React Native cung cấp hiệu suất tốt và gần tương đương với ứng dụng native nhờ vào việc sử dụng các thành phần UI native của iOS và Android. Các ứng dụng xây dựng bằng React Native thường chạy mượt mà và đáp ứng tốt, đặc biệt đối với các ứng dụng không yêu cầu hiệu suất cao.
  • Cộng đồng lớn và hệ sinh thái phong phú: React Native có một cộng đồng lớn mạnh và hệ sinh thái phong phú với nhiều thư viện, công cụ và tài nguyên hỗ trợ. Điều này giúp các nhà phát triển dễ dàng tìm kiếm giải pháp cho các vấn đề họ gặp phải và học hỏi từ các kinh nghiệm thực tiễn.
  • Cập nhật dễ dàng với Hot Reloading: Tính năng Hot Reloading cho phép nhà phát triển cập nhật và xem ngay những thay đổi trong mã nguồn mà không cần phải biên dịch lại toàn bộ ứng dụng. Điều này giúp tăng hiệu suất làm việc và tiết kiệm thời gian phát triển.
  • Khả năng tích hợp với mã native: Trong trường hợp cần sử dụng các tính năng đặc thù của thiết bị hoặc hiệu suất cao hơn, React Native cho phép tích hợp mã native (Java, Swift, hoặc Objective-C) vào dự án. Điều này cung cấp sự linh hoạt cao hơn khi phát triển ứng dụng.

So sánh giữa React Native và các framework khác như Flutter, Xamarin

Bảng dưới đây phân tích sự khác nhau giữa React Native và các Framework khác:

Nền tảng React Native Flutter  Xamarin
Ngôn ngữ sử dụng JavaScript (React) Dart C#
Hiệu suất Hiệu suất tốt nhưng phụ thuộc vào bridge giữa JavaScript và native Hiệu suất cao, chạy mã native nhờ vào Dart và engine Skia Hiệu suất tốt, hỗ trợ native và Xamarin.Forms
Thời gian phát triển Nhanh, cộng đồng lớn, nhiều thư viện hỗ trợ Tương đối nhanh, nhưng học Dart cần thời gian Nhanh, tích hợp tốt với hệ sinh thái của Microsoft
UI/UX Sử dụng các thành phần native, UI thân thiện, dễ tùy chỉnh Cung cấp các widget tùy chỉnh, UI nhất quán trên các nền tảng Sử dụng Xamarin.Forms hoặc native UI tuỳ chỉnh
Tính nhất quán giữa nền tảng Tương đối tốt, nhưng cần điều chỉnh cho từng nền tảng Cao, hầu hết các tính năng giống nhau trên iOS và Android Tốt với Xamarin.Forms, nhưng có thể gặp vấn đề với native
Khả năng mở rộng Tốt, dễ dàng mở rộng và tích hợp thư viện bên ngoài Mạnh mẽ, dễ mở rộng với các gói bổ trợ (plugins) Tích hợp tốt với hệ sinh thái Microsoft, khả năng mở rộng mạnh
Chi phí duy trì ứng dụng Tương đối thấp, phụ thuộc vào các thư viện bên thứ ba Thấp, nhiều công cụ hỗ trợ từ cộng đồng và Google Có thể cao hơn nếu sử dụng các công cụ Microsoft có phí
Khả năng tái sử dụng mã Tốt, dễ tái sử dụng mã giữa các nền tảng Rất tốt, Flutter cho phép viết một lần chạy trên mọi nền tảng Tốt, đặc biệt với Xamarin.Forms
Khả năng tiếp cận code native Dễ dàng, hỗ trợ các module native thông qua bridge Khó hơn, cần viết plugin riêng cho native code Dễ dàng, sử dụng C# để viết mã native trực tiếp

Khái niệm Component trong React Native là gì?

Trong React Native, Component là thành phần cơ bản để xây dựng giao diện người dùng (UI).

Bạn có thể hiểu Component như các “khối xây dựng” của ứng dụng, mỗi Component đảm nhận một phần nhỏ của giao diện, giúp tạo ra các thành phần có thể tái sử dụng và quản lý dễ dàng hơn. Mỗi Component có thể nhận dữ liệu đầu vào (props) và trả về cấu trúc giao diện (UI) để hiển thị.

Có hai loại Component phổ biến trong React Native: Functional Component và Class Component.

Phân biệt giữa Functional Component và Class Component

Functional Component

Functional Component là dạng component được khai báo dưới dạng hàm (function). Đây là cách khai báo phổ biến hơn, dễ hiểu, và ngắn gọn. Kể từ khi React Hooks ra đời, Functional Component đã trở thành xu hướng chính vì khả năng dễ dàng quản lý state và các side effects.

Đặc điểm của Functional Component:

  • Được khai báo dưới dạng một hàm JavaScript.
  • Không có this context như Class Component.
  • Sử dụng React Hooks như useState, useEffect để quản lý state và lifecycle.
  • Nhẹ hơn và hiệu quả hơn về hiệu suất so với Class Component.

Ví dụ về Functional Component:

import React, { useState } from 'react';

const Greeting = (props) => {
  const [message, setMessage] = useState('Hello');

  return (
    <View>
      <Text>{message}, {props.name}!</Text>
    </View>
  );
};

export default Greeting;

Class Component

Class Component là kiểu component truyền thống trong React, được khai báo dưới dạng lớp (class). Trước khi React Hooks xuất hiện, Class Component là lựa chọn chính khi cần quản lý state hoặc các lifecycle methods.

Đặc điểm của Class Component:

  • Được khai báo dưới dạng một class và mở rộng từ React.Component.
  • Sử dụng this để truy cập vào các thuộc tính và phương thức của component.
  • Sử dụng các lifecycle methods như componentDidMount, componentDidUpdate, componentWillUnmount để quản lý chu kỳ sống của component.

Ví dụ về Class Component:

import React, { Component } from 'react';

class Greeting extends Component {
  constructor(props) {
    super(props);
    this.state = {
      message: 'Hello',
    };
  }

  render() {
    return (
      <View>
        <Text>{this.state.message}, {this.props.name}!</Text>
      </View>
    );
  }
}

export default Greeting;

So sánh Functional Component và Class Component

Component  Functional Component Class Component
Khai báo Sử dụng hàm Sử dụng class
Quản lý State useState, useReducer (Hooks) Sử dụng this.statesetState
Lifecycle useEffect (Hooks) componentDidMount, componentDidUpdate,…
Độ phức tạp Đơn giản, ngắn gọn Phức tạp hơn khi sử dụng lifecycle
Hiệu suất Nhanh hơn vì ít phức tạp Có thể chậm hơn nếu không được tối ưu

Functional Component đang dần thay thế Class Component trong hầu hết các trường hợp nhờ sự đơn giản, dễ đọc, và khả năng tận dụng React Hooks để quản lý state và lifecycle một cách linh hoạt.

Tuy nhiên, hiểu biết về cả hai loại component là cần thiết để xử lý các dự án React Native một cách hiệu quả, đặc biệt là khi cần duy trì các codebase cũ sử dụng Class Component.

State và Props trong React Native là gì? Làm thế nào để quản lý chúng?

State

Trong React Native, State là một đối tượng được sử dụng để lưu trữ dữ liệu động có thể thay đổi trong suốt vòng đời của component. Khi State thay đổi, component sẽ tự động render lại để hiển thị giá trị mới. Đây là dữ liệu chỉ có thể được thay đổi bên trong chính component đó, vì vậy nó được gọi là “local” hoặc “internal” state.

Để quản lý State trong một component, bạn thường sử dụng useState hook (trong function component) hoặc this.setState() (trong class component). Dữ liệu trong State có thể được thay đổi thông qua các sự kiện hoặc các hành động từ người dùng (như bấm nút, nhập liệu, v.v.).

import React, { useState } from 'react';

const ExampleComponent = () => {
  // Khởi tạo state với useState hook
  const [count, setCount] = useState(0);

  return (
    <View>
      <Text>{'Count: ${count}`}</Text>
      {/* Cập nhật state khi bấm nút */}
     <Button onPress={() => setCount(count + 1)} title="Increase Count" />
    </View>
  );
};

Props

Props (viết tắt của “properties”) là một đối tượng truyền dữ liệu từ component cha đến component con. Đây là dữ liệu chỉ đọc, không thể thay đổi trực tiếp từ component nhận props. Props cho phép bạn truyền thông tin giữa các component, giúp chia sẻ dữ liệu và giữ cho UI đồng bộ.

Để quản lý Props, Props được truyền từ component cha thông qua JSX và có thể truy cập bằng cú pháp this.props (trong class component) hoặc truy cập trực tiếp thông qua đối số trong function component. Vì Props là chỉ đọc, nếu cần thay đổi dữ liệu, bạn cần cập nhật từ component cha.

const ChildComponent = ({ message }) => {
  return <Text>{message}</Text>;
};

const Parent Component = () => {
  const greeting = 'Hello from Parent!';
  return <ChildComponent message={greeting} />;
};

Khi nào sử dụng State và Props?

  • Sử dụng State khi dữ liệu cần thay đổi dựa trên các sự kiện hoặc tương tác từ người dùng.
  • Sử dụng Props khi cần truyền dữ liệu từ component cha đến component con mà không cần thay đổi dữ liệu trong component con.

Các kỹ thuật quản lý State nâng cao

  • React Context: Nếu cần chia sẻ State giữa nhiều component mà không muốn truyền qua nhiều cấp, React Context có thể giúp tạo một nơi lưu trữ State chung.
  • State Management Libraries: Với các ứng dụng lớn, quản lý State có thể trở nên phức tạp, vì vậy bạn có thể sử dụng các thư viện như Redux, MobX, hoặc React Query để quản lý State toàn cục một cách hiệu quả.

Câu hỏi phỏng vấn React Native về thiết kế và cấu trúc ứng dụng

Redux là gì?

Redux là một thư viện quản lý state (trạng thái) phổ biến cho các ứng dụng JavaScript, đặc biệt là các ứng dụng xây dựng bằng React hoặc React Native. Mục tiêu chính của Redux là giúp quản lý trạng thái một cách rõ ràng và dễ đoán thông qua việc sử dụng một store trung tâm, nơi chứa toàn bộ trạng thái của ứng dụng.

Khi sử dụng Redux, bạn có thể quản lý và chia sẻ dữ liệu giữa các thành phần của ứng dụng mà không cần phải truyền dữ liệu qua nhiều cấp độ khác nhau, nhờ đó giúp mã nguồn dễ bảo trì, kiểm tra, và mở rộng hơn. Redux sử dụng mô hình “Unidirectional Data Flow” (dòng dữ liệu một chiều), giúp việc theo dõi thay đổi trở nên đơn giản hơn và tránh được các lỗi liên quan đến việc cập nhật trạng thái không đồng bộ.

Đọc thêm: Redux React Native: Chi tiết sử dụng và Lỗi thường gặp

Cách tích hợp Redux vào một ứng dụng React Native

Để tích hợp Redux vào một ứng dụng React Native, bạn cần thực hiện các bước sau:

Bước 1: Cài đặt thư viện Redux và các phụ thuộc cần thiết

Trước tiên, bạn cần cài đặt reduxreact-redux để sử dụng Redux trong ứng dụng. Bạn có thể thêm chúng bằng cách sử dụng npm hoặc yarn:

npm install redux react-redux 

Bước 2: Tạo Store

Khởi tạo store bằng cách kết hợp các reducer của bạn thông qua combineReducers và sử dụng createStore.

import { createStore } from 'redux';
import rootReducer from './reducers'; // Tập hợp các reducer

const store = createStore (rootReducer);

Bước 3: Cung cấp Store cho Ứng dụng

Sử dụng <Provider> từ react-redux để cung cấp store cho toàn bộ ứng dụng.

import React from 'react';
import { Provider } from 'react-redux';
import App from './App';
import store from './store';

const Root = () =>
  <Provider store={store}>
    <App />
  </Provider>
);

export default Root;

Bước 4: Kết nối Component với Redux

Sử dụng connect hoặc useSelectoruseDispatch từ react-redux để kết nối component với store.

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';

const MyComponent() => {
  const data = useSelector((state) => state.data);
  const dispatch = useDispatch();

  const handleAction = () => {
    dispatch({ type: 'ACTION_TYPE' });
  };

  return (
    <div>
      <h1>{data}</h1>
      <button onClick={handleAction}>Dispatch Action</button>
    </div>
  );
};

export default MyComponent;

Tóm lại, việc tích hợp Redux giúp cho việc quản lý trạng thái trở nên dễ dàng hơn, đặc biệt là khi ứng dụng của bạn có cấu trúc phức tạp và yêu cầu chia sẻ dữ liệu giữa nhiều component.

Context API hoạt động như thế nào?

Context API trong React cho phép truyền dữ liệu từ một component cha đến các component con ở nhiều cấp độ khác nhau mà không cần phải sử dụng props truyền trực tiếp qua từng cấp component. Nó được xem như một cách để quản lý state ở phạm vi nhỏ hoặc cụ thể trong ứng dụng. Context API bao gồm hai phần chính: ProviderConsumer.

  • Provider: Cung cấp dữ liệu (state) cho toàn bộ cây component con của nó. Khi một giá trị trong Provider thay đổi, tất cả các component con sử dụng context này sẽ được cập nhật tự động.
  • Consumer: Là các component con có thể truy cập dữ liệu từ Provider. Consumer sử dụng useContext hook để truy xuất giá trị từ Context.

Khi nào nên sử dụng Context API thay cho Redux?

Context API thường phù hợp trong các trường hợp quản lý state không quá phức tạp hoặc chỉ cần quản lý state ở phạm vi nhỏ. Dưới đây là một số trường hợp nên cân nhắc sử dụng Context API thay vì Redux:

  1. State đơn giản hoặc không phức tạp: Nếu ứng dụng chỉ cần chia sẻ một vài state hoặc không yêu cầu quản lý state phức tạp với nhiều logic, Context API là lựa chọn tốt. Nó giúp giảm bớt sự phức tạp khi không phải thiết lập một hệ thống Redux đầy đủ.
  2. Quản lý state theo phạm vi cụ thể: Khi một phần của ứng dụng cần quản lý state riêng biệt mà không ảnh hưởng đến các phần khác, Context API giúp định rõ phạm vi của state đó, giảm thiểu rủi ro tác động không mong muốn đến các phần khác của ứng dụng.
  3. Ứng dụng nhỏ hoặc trung bình: Với các ứng dụng có quy mô nhỏ hoặc trung bình, nơi số lượng state không lớn và không có quá nhiều component con cần chia sẻ dữ liệu, Context API giúp đơn giản hóa cách quản lý và giảm sự phức tạp khi không cần thiết phải sử dụng Redux.

Tuy nhiên, nếu bạn cần quản lý state ở cấp độ lớn hơn, yêu cầu nhiều middleware cho việc xử lý như logging, debugging, hoặc muốn có một hệ thống quản lý state thống nhất cho toàn bộ ứng dụng, Redux vẫn là một lựa chọn tốt hơn.

Làm thế nào để tối ưu hóa hiệu suất của một ứng dụng React Native? 

Để tối ưu hóa hiệu suất của một ứng dụng React Native, có một số phương pháp và kỹ thuật mà bạn có thể áp dụng:

  • Sử dụng PureComponent hoặc React.memo: Nếu một component không thay đổi thường xuyên, bạn có thể sử dụng React.PureComponent hoặc React.memo để ngăn chặn việc render lại không cần thiết. Điều này sẽ giảm thiểu việc tiêu thụ tài nguyên và tăng tốc độ ứng dụng.
  • Tối ưu hóa danh sách bằng FlatList hoặc SectionList: Khi làm việc với dữ liệu danh sách lớn, hãy sử dụng FlatList hoặc SectionList thay vì ScrollView. FlatListSectionList chỉ render những item hiển thị trên màn hình và sẽ tải thêm khi người dùng cuộn, giúp tiết kiệm bộ nhớ.
  • Sử dụng các thư viện Native Module: Đối với các tác vụ nặng (như xử lý hình ảnh, mã hóa/giải mã dữ liệu,…), hãy cân nhắc sử dụng các thư viện Native Module được viết bằng Swift/Objective-C hoặc Java/Kotlin để tăng tốc độ xử lý thay vì sử dụng JavaScript thuần.
  • Tối ưu hóa hình ảnh: Hình ảnh có thể ảnh hưởng lớn đến hiệu suất của ứng dụng. Hãy tối ưu hóa hình ảnh bằng cách sử dụng các định dạng ảnh nhẹ hơn như WebP, sử dụng các thư viện như react-native-fast-image để caching hình ảnh, và đảm bảo chỉ tải những hình ảnh cần thiết cho mỗi màn hình.
  • Sử dụng Performance Monitor: React Native cung cấp Performance Monitor để giúp bạn kiểm tra hiệu suất ứng dụng trong quá trình phát triển. Sử dụng công cụ này để xác định các điểm yếu và tìm giải pháp cải thiện.
  • Chỉ sử dụng các component Animation khi cần thiết: Các animation có thể gây ảnh hưởng đến hiệu suất. Sử dụng react-native-reanimated hoặc react-native-gesture-handler để tối ưu hóa các animation phức tạp thay vì sử dụng các animation mặc định của React Native.
  • Quản lý trạng thái hiệu quả: Việc quản lý trạng thái không hợp lý có thể làm ứng dụng trở nên chậm chạp. Hãy sử dụng Redux hoặc Context API một cách tối ưu và tránh việc truyền state không cần thiết qua các component không liên quan.

Khái niệm về Navigation trong React Native và các thư viện phổ biến như React Navigation, React Native Navigation

Navigation trong React Native là một phần quan trọng của bất kỳ ứng dụng di động nào, cho phép người dùng di chuyển giữa các màn hình khác nhau trong ứng dụng. Khái niệm này bao gồm việc quản lý trạng thái, chuyển đổi màn hình, và duy trì lịch sử điều hướng để tạo ra trải nghiệm người dùng mượt mà và dễ sử dụng.

Có hai loại điều hướng chính trong React Native:

  • Stack Navigation (chuyển đổi giữa các màn hình theo dạng stack)
  • Tab Navigation (điều hướng giữa các màn hình qua các tab
  • Một số trường hợp khác bao gồm Drawer Navigation để mở các menu ẩn từ cạnh màn hình.

Các thư viện phổ biến:

React Navigation

  • Đây là thư viện phổ biến nhất và được sử dụng rộng rãi trong cộng đồng React Native. Nó cung cấp một bộ công cụ linh hoạt để tạo ra các loại điều hướng khác nhau như Stack, Tab, Drawer, Bottom Tab và nhiều loại khác.
  • React Navigation dễ cấu hình và có một cộng đồng hỗ trợ mạnh mẽ, với tài liệu chi tiết và các ví dụ phong phú.
  • Ưu điểm: Linh hoạt, dễ tích hợp, có thể tùy chỉnh cao, hỗ trợ chuyển động mượt mà giữa các màn hình.
  • Nhược điểm: Đôi khi phức tạp khi tùy chỉnh các yêu cầu đặc biệt và có thể cần nhiều cấu hình thủ công khi ứng dụng có quy mô lớn.

React Native Navigation (do Wix phát triển)

  • Đây là một thư viện điều hướng cấp cao được thiết kế với mục tiêu cung cấp hiệu suất tốt nhất bằng cách sử dụng các thành phần native thực sự (native components) của nền tảng.
  • Thư viện này lý tưởng cho các ứng dụng phức tạp và có yêu cầu hiệu năng cao vì nó tận dụng các chức năng native để cung cấp trải nghiệm tốt hơn.
  • Ưu điểm: Hiệu suất cao hơn nhờ việc sử dụng thành phần native, chuyển động mượt mà, hỗ trợ tốt các tính năng của hệ điều hành như thanh điều hướng và thanh công cụ (Toolbar).
  • Nhược điểm: Cấu hình phức tạp hơn, yêu cầu hiểu biết sâu về các thành phần native của Android và iOS.

Giải thích về khái niệm Higher-Order Components (HOC) và sử dụng chúng trong React Native

Higher-Order Components (HOC) là một mẫu thiết kế trong React và React Native, cho phép bạn tái sử dụng logic giữa các component. HOC thực chất là một hàm nhận một component và trả về một component mới với các tính năng hoặc dữ liệu bổ sung. Nói cách khác, nó giúp chia sẻ các logic chung mà không cần lặp lại code trong nhiều component khác nhau.

HOC thường được dùng để:

  • Xử lý việc lấy dữ liệu từ server (fetch data).
  • Quản lý quyền truy cập (authorization) hoặc kiểm tra điều kiện trước khi render component.
  • Thêm các tính năng đặc biệt cho component như tracking hoặc logging.

Ví dụ về HOC trong React Native:

Giả sử bạn có một component UserList để hiển thị danh sách người dùng và bạn muốn thêm chức năng lấy dữ liệu từ server. Bạn có thể sử dụng HOC để chia sẻ logic lấy dữ liệu này mà không cần tích hợp trực tiếp vào component:

import React, { useEffect, useState } from 'react';

// Đây là HOC nhận một component và trả về một component mới
function withDataFetching(WrappedComponent, url) {
  return function () {
    const [data, setData] = useState([]);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
      fetch(url)
        .then(response => response.json())
        .then(data => {
          setData(data);
          setLoading(false);
        });
    }, [url]);
    
    // Trả về WrappedComponent với data và loading
    return <WrappedComponent data={data} loading={loading} />;
  };
}

// Component UserList nhận props data và loading
function UserList({ data, loading }) {
  if (loading) {
    return <Text>Loading...</Text>;
  }

  return (
    <FlatList
      data={data}
      keyExtractor={(item) => item.id.toString()}
      renderItem={({ item }) => <Text>{item.name}</Text>}
    />
  );
}

// Sử dụng HOC để tạo component UserListWithData
const UserListWithData = withDataFetching(UserList, 'https://api.example.com/users');

Trong ví dụ trên, withDataFetching là một HOC nhận vào component UserList và URL để fetch data. Kết quả là bạn có thể tái sử dụng logic lấy dữ liệu cho các component khác nhau mà không cần viết lại.

Câu hỏi phỏng vấn React Native về các thành phần giao diện (UI Components)

Làm thế nào để tạo một Custom Component trong React Native? 

Để tạo một Custom Component trong React Native, tôi thường sử dụng Functional Components, vì nó giúp mã nguồn ngắn gọn hơn và dễ bảo trì. Dưới đây là các bước:

Bước 1: Tạo File Component

Tạo một file JavaScript mới, ví dụ như MyButton.js, để chứa Custom Component.

Bước 2: Định nghĩa Component

Trong file MyButton.js, tôi định nghĩa một Functional Component sử dụng cú pháp ES6. Ví dụ:

import React from 'react';
import {TouchableOpacity, Text, StyleSheet } from 'react-native';

const MyButton = ({ title, onPress, color }) => {
  return (
    <TouchableOpacity
      style={[styles.button, { backgroundColor: color }]}
      onPress={onPress}
    >
      <Text style={styles.text}>{title}</Text>
    </TouchableOpacity>
  );
};

const styles StyleSheet.create({
  button: {
    padding: 10,
    borderRadius: 5,
    alignItems: 'center',
  },
  text: {
    color: '#ffffff',
    fontSize: 16,
  },
});

export default MyButton;

Bước 3: Sử dụng Props

Trong ví dụ trên, tôi sử dụng props để nhận các giá trị từ component cha như title, onPress, và color. Điều này giúp component trở nên linh hoạt và có thể tái sử dụng trong nhiều trường hợp khác nhau.

Bước 4: Sử dụng Component

Cuối cùng, để sử dụng component này trong một file khác, tôi sẽ import nó và cung cấp các props cần thiết, ví dụ:

import React from 'react';
import { View } from 'react-native';
import MyButton from './MyButton';

const App = () => {
  return (
    <View>
      <MyButton
        title="Click Me"
        onPress={() => alert('Button Pressed!')}
        color="#4CAF50"
      />
    </View>
  );
};

export default App;

Sự khác nhau giữa ScrollView và FlatList là gì?

  • ScrollView: Là một thành phần dùng để hiển thị danh sách hoặc nội dung cuộn, phù hợp khi số lượng phần tử ít và có thể tải toàn bộ vào bộ nhớ cùng lúc. Nó không tối ưu cho danh sách lớn vì mọi phần tử đều được render trước.
  • FlatList: Là thành phần được tối ưu hóa để hiển thị danh sách lớn. Nó chỉ render các phần tử hiển thị trên màn hình hoặc gần đó, giúp tiết kiệm bộ nhớ và cải thiện hiệu suất. FlatList hỗ trợ các tính năng như phân trang, kéo tải thêm dữ liệu, và tùy chỉnh hiệu quả.
Thành phần  ScrollView FlatList 
Mục đích sử dụng Sử dụng khi có một lượng nhỏ dữ liệu cần cuộn hoặc hiển thị toàn bộ dữ liệu cùng lúc. Dùng để hiển thị danh sách lớn với nhiều mục mà chỉ tải dữ liệu khi cần thiết (lazy loading).
Hiệu suất Không tối ưu khi có nhiều phần tử do tất cả dữ liệu được render một lần. Hiệu suất tốt hơn khi làm việc với danh sách lớn nhờ cơ chế virtual rendering (chỉ render các phần tử hiển thị).
Render dữ liệu Render toàn bộ dữ liệu trong danh sách, dẫn đến tốn nhiều bộ nhớ nếu dữ liệu lớn Render theo từng trang (paging) hoặc từng phần tử visible, tiết kiệm bộ nhớ hơn.
Hỗ trợ lazy loading Không có hỗ trợ lazy loading; toàn bộ dữ liệu được tải khi mở trang Có hỗ trợ lazy loading để tải dần dữ liệu khi người dùng cuộn xuống.
Tính năng phân trang Không hỗ trợ trực tiếp phân trang. Hỗ trợ phân trang dễ dàng thông qua thuộc tính onEndReachedonEndReachedThreshold
Chức năng item separator Không có hỗ trợ item separator mặc định Có hỗ trợ thuộc tính ItemSeparatorComponent để tùy chỉnh separator giữa các item.
Hiển thị header/footer Có thể thêm header/footer nhưng cần tạo custom layout Có hỗ trợ sẵn các thuộc tính ListHeaderComponentListFooterComponent để hiển thị header/footer
Key Không cần keyExtractor, không tối ưu với danh sách động hoặc thay đổi nhiều Cần có keyExtractor để định danh từng phần tử, giúp quản lý hiệu quả danh sách thay
Khả năng làm việc với dữ liệu lớn Không phù hợp với dữ liệu lớn do tất cả các phần tử được render cùng lúc Phù hợp với dữ liệu lớn do chỉ render những phần tử cần thiết

Làm sao để xử lý sự kiện touch (bấm, kéo, thả) trong React Native? 

Để xử lý sự kiện touch (bấm, kéo, thả) trong React Native, bạn có thể sử dụng các thành phần và API được cung cấp sẵn như TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback, TouchableNativeFeedback, hoặc Pressable.

Ngoài ra, bạn cũng có thể sử dụng các thư viện bên thứ ba như react-native-gesture-handler để có thể xử lý các sự kiện phức tạp hơn.

Cách 1: Sử dụng các thành phần Touchable

Các thành phần thuộc nhóm Touchable trong React Native được thiết kế để xử lý sự kiện chạm. Một ví dụ phổ biến là TouchableOpacity:

import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';

const Example = () => {
  return (
    <View>
      <TouchableOpacity onPress={() => alert('Button Pressed!')}>
        <Text>Press Me</Text>
      </TouchableOpacity>
    </View>
  );
};

Trong ví dụ này, sự kiện onPress sẽ được kích hoạt khi người dùng chạm vào vùng có chứa Text. Các thuộc tính khác như onLongPress, onPressIn, và onPressOut cũng có thể được sử dụng để bắt các sự kiện khác như giữ lâu, bắt đầu chạm, và kết thúc chạm.

Cách 2: Sử dụng Pressable

Pressable là thành phần mới hơn được giới thiệu trong các phiên bản mới của React Native, giúp bạn dễ dàng tùy chỉnh hơn trong việc xử lý các sự kiện touch:

import React from 'react';
import { View, Text, Pressable } from 'react-native';

const Example = () => {
  return (
    <View>
      <Pressable
        onPress={() => console.log('Pressed')}
        onLongPress={() => console.log('Long Pressed')}
        onPressIn={() => console.log('Press In')}
        onPressOut={() => console.log('Press Out')}
      >
        <Text>Touch Me</Text>
      </Pressable>
    </View>
  );
};

Cách 3: Sử dụng react-native-gesture-handler

Trong những trường hợp cần xử lý các cử chỉ phức tạp như kéo, vuốt, hoặc xử lý các cử chỉ cùng lúc, bạn có thể sử dụng thư viện react-native-gesture-handler. Ví dụ:

import React from 'react';
import { GestureHandlerRootView, PanGestureHandler } from 'react-native-gesture-handler';
import { View, Text } from 'react-native';

const Example = () => {
  const onGestureEvent = (event) => {
    console.log('Gesture Detected', event.nativeEvent);
  };

  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <PanGestureHandler onGestureEvent={onGestureEvent}>
        <View style={{ width: 200, height: 200, backgroundColor: 'lightblue', justifyContent: 'center', alignItems: 'center' }}>
          <Text>Drag Me</Text>
        </View>
      </PanGestureHandler>
    </GestureHandlerRootView>
  );
};

Giải thích cách tạo Animation trong React Native và các thư viện hỗ trợ như React Native Reanimated 

Trong React Native, việc tạo Animation giúp cải thiện trải nghiệm người dùng và làm cho ứng dụng trở nên sống động hơn. Để tạo Animation, React Native cung cấp một số API và thư viện hỗ trợ, phổ biến nhất là Animated APIReact Native Reanimated.

Cách tạo Animation với Animated API

Animated là API gốc của React Native, hỗ trợ việc tạo Animation đơn giản. Bạn có thể sử dụng Animated để tạo các hiệu ứng chuyển động cho các thuộc tính như vị trí, kích thước, độ mờ, hoặc màu sắc của một component.

Ví dụ: Tạo Animation đơn giản với Animated API

import React, { useRef, useEffect } from 'react';
import { Animated, View, Button } from 'react-native';

const SimpleAnimation = () => {
  const opacity = useRef(new Animated.Value(0)).current;

  const fadeIn = () => {
    Animated.timing(opacity, {
      toValue: 1,
      duration: 1000,
      useNativeDriver: true,
    }).start();
  };

  return (
    <View>
      <Animated.View style={{ opacity, width: 100, height: 100, backgroundColor: 'blue' }} />
      <Button title="Fade In" onPress={fadeIn} />
    </View>
  );
};

Trong ví dụ này, Animated.Value tạo ra một giá trị có thể thay đổi (ở đây là opacity), và Animated.timing giúp thực hiện Animation theo thời gian.

Thư viện hỗ trợ: React Native Reanimated

Trong khi Animated API có thể đáp ứng nhu cầu cơ bản, React Native Reanimated mang đến nhiều khả năng mạnh mẽ hơn khi cần xử lý Animation phức tạp hoặc tối ưu hóa hiệu năng.

React Native Reanimated cho phép thực hiện Animation mượt mà, đồng thời giảm tải cho JS thread, nhờ đó ứng dụng hoạt động ổn định và nhanh hơn, đặc biệt là với các Animation phức tạp.

Các tính năng nổi bật của React Native Reanimated

  • Mượt mà hơn: React Native Reanimated giúp Animation chạy mượt mà nhờ việc chạy trực tiếp trên UI thread thay vì phụ thuộc hoàn toàn vào JS thread.
  • Cấu trúc rõ ràng: Cung cấp một cú pháp rõ ràng, dễ sử dụng để tạo các Animation phức tạp, ví dụ như Gesture Handling và các Interaction.
  • Khả năng mở rộng: Cho phép kết hợp Animation với các hành vi phức tạp như Spring, Decay hoặc các hiệu ứng vật lý.

Ví dụ: Tạo Animation với React Native Reanimated

import React from 'react';
import { Button } from 'react-native';
import { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated';

const ReanimatedExample = () => {
  const opacity = useSharedValue(0);

  const animatedStyle = useAnimatedStyle(() => {
    return {
      opacity: opacity.value,
    };
  });

  const fadeIn = () => {
    opacity.value = withTiming(1, { duration: 1000 });
  };

  return (
    <>
      <Animated.View style={[{ width: 100, height: 100, backgroundColor: 'red' }, animatedStyle]} />
      <Button title="Fade In" onPress={fadeIn} />
    </>
  );
};

Trong ví dụ này, useSharedValue tạo ra một giá trị chia sẻ giữa các Animation, còn useAnimatedStyle giúp áp dụng hiệu ứng Animation vào style của component.

Cách sử dụng StyleSheet trong React Native và sự khác biệt giữa inline styles và StyleSheet

Trong React Native, StyleSheet giúp định nghĩa các style tái sử dụng một cách tách biệt, dễ quản lý và tối ưu hiệu suất hơn so với inline styles. StyleSheet biến các style thành mã native ngay từ lúc biên dịch, trong khi inline styles cần được tính toán lại mỗi lần render, có thể ảnh hưởng đến hiệu suất nếu sử dụng rộng rãi.

Do đó, mình thường dùng StyleSheet cho các style cố định và inline styles khi cần điều chỉnh style động.

Nội dung  StyleSheet  Inline Styles
Khái niệm Sử dụng StyleSheet.create để định nghĩa các kiểu một cách tập trung. Định nghĩa các kiểu trực tiếp trong thuộc tính style.
Cách sử dụng javascript const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: ‘white’, padding: 10, }, }); javascript <View style={styles.container}></View> javascript <View style={{ flex: 1, backgroundColor: ‘white’, padding: 10 }}></View>
Tái sử dụng Có thể tái sử dụng kiểu được định nghĩa nhiều lần trong ứng dụng. Mỗi lần sử dụng phải khai báo kiểu trực tiếp, khó tái sử dụng.
Hiệu suất Hiệu quả hơn khi tối ưu bộ nhớ vì các kiểu được quản lý dưới dạng tĩnh. Ít hiệu quả hơn vì mỗi lần khai báo tạo ra một đối tượng mới.
Tính rõ ràng và tổ chức Giúp mã nguồn rõ ràng và dễ quản lý hơn khi các kiểu được tách biệt. Có thể gây khó khăn trong quản lý nếu kiểu quá dài hoặc phức tạp.
Trường hợp sử dụng phù hợp Khi cần định nghĩa kiểu phức tạp hoặc tái sử dụng nhiều lần. Khi chỉ cần định nghĩa kiểu đơn giản, nhanh chóng.

Câu hỏi phỏng vấn React Native về xử lý dữ liệu và API

Cách sử dụng Fetch API hoặc Axios để gọi API trong React Native

Fetch API

Fetch API là một công cụ có sẵn trong JavaScript và có thể được sử dụng trực tiếp trong React Native để thực hiện các yêu cầu HTTP.

Ví dụ:

const fetchData = async () => {
  try {
    const response = await fetch('https://api.example.com/data');

    if (!response.ok) {
      throw new Error('Network response was not ok');
    }

    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Fetch error: ', error);
  }
};

Giải thích:

  • Sử dụng fetch để thực hiện yêu cầu GET tới API.
  • Sử dụng response.json() để chuyển đổi phản hồi thành đối tượng JavaScript.
  • Xử lý lỗi nếu phản hồi không thành công.

Axios

Axios là một thư viện bên thứ ba được sử dụng phổ biến vì cú pháp ngắn gọn và khả năng cấu hình cao.

Cài đặt Axios:

npm install axios

Ví dụ:

import axios from 'axios';

const fetchData = async () => {
  try {
    const response = await axios.get('https://api.example.com/data');
    console.log(response.data);
  } catch (error) {
    console.error('Axios error: ', error);
  }
};

Giải thích:

  • axios.get để thực hiện yêu cầu GET.
  • Phản hồi của Axios dễ làm việc hơn vì dữ liệu đã có sẵn trong response.data.
  • Xử lý lỗi với try…catch.

async/await trong JavaScript là gì? Cách sử dụng chúng trong React Native

Trong JavaScript, async/await là cú pháp dùng để xử lý các tác vụ bất đồng bộ (asynchronous operations) một cách dễ đọc và rõ ràng hơn so với việc sử dụng các hàm Promise hoặc callback.

  • async: Từ khóa này được sử dụng trước một hàm để đánh dấu rằng hàm đó là bất đồng bộ (asynchronous). Khi một hàm được khai báo với từ khóa async, nó sẽ tự động trả về một Promise. Điều này có nghĩa là có thể sử dụng await bên trong hàm đó.
  • await: Từ khóa này chỉ có thể được sử dụng bên trong một hàm async. Nó được sử dụng để tạm dừng việc thực thi của hàm cho đến khi một Promise được hoàn thành, giúp viết mã đồng bộ hơn và dễ đọc hơn.

Khi trả lời câu hỏi về async/await trong phỏng vấn cho vị trí React Native Developer, ứng viên nên tập trung giải thích khái niệm cơ bản và trình bày cách sử dụng nó trong bối cảnh ứng dụng React Native.

Trong React Native, async/await thường được sử dụng khi cần lấy dữ liệu từ API hoặc xử lý các tác vụ tốn thời gian như lưu trữ dữ liệu cục bộ. Ví dụ:

const fetchData = async () => {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log('Dữ liệu nhận được:', data);
  } catch (error) {
    console.error('Lỗi khi lấy dữ liệu:', error);
  }
};

Cách quản lý dữ liệu từ API trong Redux Store

Để quản lý dữ liệu từ API trong Redux Store, tôi sẽ bắt đầu bằng việc tạo một action để gọi API, ví dụ như fetchData. Sau đó, tôi sẽ sử dụng một middleware như redux-thunk hoặc redux-saga để thực hiện gọi API bất đồng bộ.

Khi API được gọi, tôi sẽ dispatch ba loại action khác nhau:

  • Một action để báo hiệu quá trình gọi API đang được tiến hành (FETCH_DATA_REQUEST).
  • Một action khi dữ liệu được tải thành công (FETCH_DATA_SUCCESS) để cập nhật dữ liệu vào Store.
  • Một action khi xảy ra lỗi trong quá trình gọi API (FETCH_DATA_FAILURE) để hiển thị thông báo lỗi.

Reducer sẽ lắng nghe các action này và cập nhật Redux Store dựa trên kết quả. Ví dụ, nếu thành công, tôi sẽ cập nhật dữ liệu vào Store, còn nếu gặp lỗi, tôi sẽ lưu thông tin lỗi vào Store để có thể hiển thị thông báo cho người dùng. Trong component, tôi sẽ sử dụng useSelector để truy xuất dữ liệu từ Store và useDispatch để dispatch các action khi cần.

Dưới đây là một số lỗi phổ biến khi gọi API trong React Native là gì?

Lỗi Network Request Failed

Nguyên nhân:

  • Thiết bị không có kết nối internet.
  • API không được bảo mật bằng HTTPS (chỉ sử dụng HTTP).
  • URL API sai hoặc không tồn tại.
  • Thiếu quyền truy cập mạng trên Android (AndroidManifest.xml).

CORS Error (Cross-Origin Resource Sharing)

Nguyên nhân:

  • Máy chủ API không cho phép yêu cầu từ nguồn gốc (origin) của ứng dụng.
  • Thiếu cấu hình header Access-Control-Allow-Origin trên server.

Lỗi không bắt được response hoặc error

Nguyên nhân:

  • Không xử lý đầy đủ các trạng thái trong try-catch.
  • Không thêm catch khi sử dụng Promise.

Lỗi Timeout hoặc API không phản hồi

Nguyên nhân:

  • Máy chủ phản hồi quá chậm.
  • Ứng dụng không thiết lập giới hạn thời gian cho yêu cầu.

Lỗi JSON Parse Error

Nguyên nhân:

  • Phản hồi từ server không phải JSON hợp lệ.
  • Đã gọi response.json() trên một phản hồi không có dữ liệu.

Lỗi 401 Unauthorized hoặc 403 Forbidden

Nguyên nhân:

  • Thiếu hoặc sai token trong header Authorization.
  • Người dùng không có quyền truy cập tài nguyên.

Lỗi 500 Internal Server Error hoặc 503 Service Unavailable

Nguyên nhân:

  • Lỗi máy chủ hoặc server đang bảo trì.

Các cách xử lý lỗi khi gọi API trong ứng dụng React Native

Khi xử lý lỗi khi gọi API trong ứng dụng React Native, nhà tuyển dụng thường muốn đánh giá cách bạn quản lý các tình huống lỗi trong quá trình giao tiếp với server, bao gồm các lỗi về mạng, lỗi từ server, hay dữ liệu không hợp lệ. Dưới đây là các bước và phương pháp thông dụng để xử lý các lỗi được đề cập ở trên:

Sử dụng try…catch với async/await

Một cách phổ biến để xử lý lỗi là sử dụng try…catch với cú pháp async/await. Điều này giúp kiểm soát lỗi xảy ra khi gọi API một cách rõ ràng và dễ hiểu: 

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      // Xử lý lỗi khi server trả về response không hợp lệ
     throw new Error(Server error: ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    // Xử lý các lỗi khác như lỗi mạng
    console.error('Fetch error:', error);
    throw error; // Có thể trả về lỗi để xử lý thêm ở nơi khác
  }
}

Kiểm tra mã trạng thái HTTP

Khi nhận được phản hồi từ server, cần kiểm tra mã trạng thái HTTP (status code). Các mã như 404, 500 thường cho thấy lỗi từ server:

if (response.status === 404) {
  console.warn('Not found: The requested resource does not exist.');
} else if (response.status === 500) {
  console.error('Server error: Something went wrong on the server.');
}

Hiển thị thông báo lỗi cho người dùng

Khi gặp lỗi, nên hiển thị thông báo dễ hiểu cho người dùng thay vì chỉ log lỗi. Điều này cải thiện trải nghiệm người dùng:

catch (error) {

  Alert.alert('Lỗi', 'Không thể tải dữ liệu. Vui lòng kiểm tra kết nối mạng và thử lại.');

}

Quản lý lỗi toàn cục với Redux hoặc Context API

Để xử lý lỗi một cách nhất quán trong ứng dụng, có thể quản lý trạng thái lỗi thông qua Redux hoặc Context API, giúp lưu trữ và hiển thị lỗi ở các phần khác nhau của ứng dụng một cách đồng bộ.

Thêm cơ chế Retry (Thử lại)

Nếu xảy ra lỗi do kết nối mạng, có thể thêm tính năng thử lại (retry) sau một khoảng thời gian:

function fetchDataWithRetry (retryCount = 3) {
  let attempts = 0;
  const fetchData = async () => {
    try {
      const response = await fetch('https://api.example.com/data');
      if (!response.ok) throw new Error('Server error');
      return await response.json();
    } catch (error) {
      if (attempts < retryCount) {
        attempts++;
        console.log('Retrying... (${attempts}/${retryCount})`);
        return fetchData();
      }
      throw error;
    }
  };
  return fetchData();
}

Sử dụng các thư viện hỗ trợ

Ngoài fetch, các thư viện như Axios cung cấp nhiều tính năng tiện lợi như interceptors để xử lý lỗi tập trung, retry tự động, và dễ dàng cấu hình:

import axios from 'axios';

axios.interceptors.response.use(
  response => response,
  error => {
    console.error('API error:', error);
    return Promise. reject (error);
  }
);

Những kỹ thuật này thể hiện khả năng của ứng viên trong việc quản lý các tình huống lỗi phức tạp và cung cấp trải nghiệm người dùng tốt hơn, điều mà nhà tuyển dụng đánh giá cao ở một React Native Developer.

Sử dụng FlatList để hiển thị danh sách dữ liệu từ API

Fetch dữ liệu từ API: Đầu tiên, tôi sẽ sử dụng các phương thức như fetch hoặc axios để gửi yêu cầu đến API và lấy dữ liệu về.

Quản lý trạng thái dữ liệu: Tôi sẽ sử dụng useState để quản lý dữ liệu được nhận về từ API

const [dataList, setDataList] = useState([]);

Hiển thị dữ liệu với FlatList: Chúng ta sử dụng FlatList để hiển thị danh sách dữ liệu. FlatList là một thành phần tối ưu để hiển thị danh sách lớn vì nó chỉ render những item xuất hiện trên màn hình.

<FlatList
  data={dataList}
  keyExtractor={(item) => item.id.toString()}
  renderItem={( { item }) => (
    <View style={styles.itemContainer}>
      <Text>{item.title}</Text>
    </View>
  )}
/>

Tối ưu hóa hiệu suất: Để đảm bảo FlatList hoạt động hiệu quả với danh sách lớn, tôi sẽ cung cấp keyExtractor để định danh từng phần tử và tránh render lại không cần thiết. Ngoài ra, có thể sử dụng thuộc tính initialNumToRender để kiểm soát số lượng phần tử được render lần đầu.

Xử lý khi tải dữ liệu: Tôi có thể thêm một biến trạng thái loading để hiển thị màn hình tải khi đang fetch dữ liệu:

const [loading, setLoading] = useState(true);

useEffect(() => {
  const fetchData = async () => {
    setLoading (true);
    try {
      const response = await fetch('https://api.example.com/data');
      const data = await response.json();
      setDataList(data);
    } catch (error) {
      console.error('Error fetching data:', error);
    } finally {
      setLoading (false);
    }
  };

  fetchData();
}, []);

if (loading) {
  return <ActivityIndicator size="large" color="#0000ff" />;
}

Câu hỏi phỏng vấn React Native về tích hợp Native Modules và Native Code

Làm thế nào để tích hợp một module native vào ứng dụng React Native? 

Để tích hợp một module native vào ứng dụng React Native, quy trình bao gồm các bước cơ bản sau:

Tạo module native (Android và iOS)

Android:

  • Thêm code Java hoặc Kotlin trong thư mục android. Tạo một class mới để xử lý logic của module native.
  • Sử dụng annotation @ReactMethod để đánh dấu các phương thức sẽ được sử dụng từ React Native.
  • Đăng ký module này với ReactPackage trong code Java/Kotlin và thêm package này vào MainApplication.java.

iOS:

  • Thêm code Swift hoặc Objective-C trong thư mục ios. Tạo một class mới kế thừa từ RCTBridgeModule.
  • Implement các phương thức muốn gọi từ React Native và sử dụng RCT_EXPORT_METHOD để export các phương thức này.
  • Đảm bảo module đã được đăng ký với Bridge bằng cách thêm vào file AppDelegate.m hoặc AppDelegate.swift.

Sử dụng module trong React Native

Sử dụng module native trong JavaScript bằng cách import module vừa tạo. Có thể sử dụng NativeModules từ react-native để truy cập module đã tích hợp.

Ví dụ:

import {NativeModules} from 'react-native';
const {MyNativeModule } = NativeModules;

MyNativeModule.someMethod('data', (result) => {
  console.log('Kết quả từ module native:', result);
});

Kiểm tra và xử lý lỗi

  • Chạy ứng dụng để kiểm tra xem module native có hoạt động đúng không.
  • Debug các lỗi có thể phát sinh khi kết nối giữa React Native và module native.
  • Đảm bảo các phương thức trong module native tương thích với các yêu cầu từ phía React Native.

Khác nhau giữa Bridge và TurboModules trong React Native

Bridge và TurboModules là hai cách để React Native xử lý giao tiếp giữa mã JavaScript và mã gốc:

Bridge

  • Là cơ chế giao tiếp ban đầu của React Native. Nó hoạt động bằng cách sử dụng một lớp trung gian (bridge) để truyền dữ liệu giữa JavaScript và mã gốc thông qua JSON.
  • Điểm mạnh: Đơn giản và dễ hiểu, dễ mở rộng khi cần tích hợp các module gốc (native modules) mới.
  • Điểm yếu: Giao tiếp thông qua JSON có thể gây ra độ trễ (latency), đặc biệt khi có lượng dữ liệu lớn hoặc yêu cầu hiệu suất cao do cần chuyển đổi nhiều lần giữa các ngôn ngữ.

TurboModules

  • Là một cải tiến mới trong kiến trúc của React Native nhằm thay thế Bridge truyền thống. TurboModules giảm thiểu độ trễ bằng cách tận dụng các tính năng tối ưu hóa và giao tiếp trực tiếp hơn giữa JavaScript và mã gốc.
  • Điểm mạnh: Hiệu suất tốt hơn với độ trễ thấp, khả năng tải module một cách linh hoạt (on-demand), giúp giảm bộ nhớ sử dụng.
  • Điểm yếu: Cần kiến thức sâu hơn về cấu trúc nội bộ của React Native và đòi hỏi sự thay đổi trong cách viết mã khi so sánh với Bridge truyền thống.

So sánh Bridge vs TurboModules

Đặc điểm  Bridge  TurboModules
Cơ chế giao tiếp Sử dụng lớp trung gian (bridge) để truyền dữ liệu qua JSON giữa JavaScript và mã gốc. Giao tiếp trực tiếp hơn, giảm thiểu chuyển đổi giữa JavaScript và mã gốc.
Điểm mạnh – Đơn giản, dễ hiểu.

– Dễ mở rộng khi tích hợp module native mới.

– Hiệu suất cao, độ trễ thấp.

– Hỗ trợ tải module linh hoạt (on-demand), giảm bộ nhớ sử dụng.

Điểm yếu – Gây độ trễ do chuyển đổi JSON, đặc biệt với dữ liệu lớn hoặc yêu cầu hiệu suất cao. – Đòi hỏi kiến thức sâu về React Native.

– Cần thay đổi cách viết mã so với Bridge truyền thống.

Ứng dụng Phù hợp với các dự án không yêu cầu hiệu suất cao hoặc không xử lý dữ liệu phức tạp. Thích hợp cho các dự án yêu cầu hiệu suất tối ưu, giao tiếp nhanh giữa JavaScript và mã gốc.

Trong ứng dụng thực tế, TurboModules mang lại lợi ích về hiệu suất, đặc biệt khi xử lý các tác vụ yêu cầu tài nguyên lớn hoặc khi cần cải thiện trải nghiệm người dùng với các ứng dụng có độ phức tạp cao.

Giải thích về cách giao tiếp giữa JavaScript và Native Code trong React Native

Trong React Native, giao tiếp giữa JavaScript và Native Code được thực hiện chủ yếu thông qua Bridge hoặc TurboModules. Bridge là cách truyền thống, dùng để gửi và nhận dữ liệu giữa hai môi trường thông qua quá trình tuần tự hóa.

Tuy nhiên, nó có nhược điểm về hiệu suất. TurboModules, giải pháp mới hơn, tối ưu hóa quy trình này bằng cách cho phép truy cập trực tiếp giữa mã Native và JavaScript, giảm thiểu thời gian phản hồi và cải thiện hiệu suất của ứng dụng.

câu hỏi phỏng vấn react native - itviec blog

Khi nào cần sử dụng các thư viện native trong React Native thay vì các thư viện React Native có sẵn? 

Trong các trường hợp khi ứng dụng yêu cầu hiệu suất cao, tiếp cận các chức năng phần cứng đặc biệt, hoặc cần sử dụng các tính năng chưa được hỗ trợ đầy đủ bởi React Native, tôi sẽ xem xét sử dụng các thư viện native.

Ví dụ, với những tác vụ đòi hỏi xử lý đồ họa phức tạp như hiển thị bản đồ 3D, video streaming hoặc xử lý hình ảnh nâng cao, các thư viện native có thể cung cấp hiệu suất tốt hơn.

Ngoài ra, khi có yêu cầu về việc tích hợp với các dịch vụ hoặc SDK từ bên thứ ba chỉ hỗ trợ native, tôi cũng sẽ cân nhắc việc sử dụng các thư viện native để đảm bảo ứng dụng hoạt động đúng mong đợi. Việc sử dụng code native có thể phức tạp hơn, nhưng đôi khi đó là lựa chọn tốt nhất để đạt được chất lượng và hiệu suất cao nhất cho ứng dụng.

Khi nào sử dụng thư viện native?

  • Ứng dụng yêu cầu hiệu suất cao.
  • Tiếp cận các chức năng phần cứng đặc biệt không được hỗ trợ đầy đủ bởi React Native.
  • Sử dụng các tính năng chưa được hỗ trợ hoặc không tối ưu trong React Native.

Trường hợp cụ thể cần thư viện native

  • Xử lý đồ họa phức tạp: Hiển thị bản đồ 3D, xử lý hình ảnh nâng cao.
  • Video streaming: Phát trực tuyến video với hiệu suất cao.
  • Tích hợp dịch vụ/SDK từ bên thứ ba: Khi dịch vụ hoặc SDK chỉ hỗ trợ nền tảng native.

Lợi ích của việc sử dụng thư viện native

  • Đảm bảo hiệu suất và chất lượng cao nhất cho ứng dụng.
  • Hỗ trợ tốt hơn cho các tác vụ phức tạp hoặc yêu cầu đặc thù.

Cách xử lý các lỗi thường gặp khi tích hợp module native

Lỗi biên dịch (Compilation Errors)

Mô tả: Lỗi này xảy ra khi module native không tương thích với phiên bản React Native hoặc các thư viện native khác.

Cách xử lý:

  • Kiểm tra lại phiên bản React Native và module native để đảm bảo chúng tương thích với nhau.
  • Đọc tài liệu chính thức của module để biết các yêu cầu cụ thể về phiên bản.
  • Cập nhật hoặc hạ cấp module cho phù hợp với phiên bản React Native hiện tại của dự án.

Lỗi không tìm thấy module (Module Not Found)

Mô tả: Thông báo lỗi xuất hiện khi ứng dụng không thể tìm thấy module native được tích hợp.

Cách xử lý:

  • Kiểm tra tên module có đúng không và module đã được liên kết (linked) chính xác chưa.
  • Sử dụng lệnh react-native link <tên_module> để liên kết module native hoặc kiểm tra file cấu hình Podfile (iOS) và build.gradle (Android) để đảm bảo chúng đã được thêm vào đúng cách.
  • Nếu dùng React Native 0.60+, kiểm tra tính năng auto-linking để xem module đã được liên kết tự động hay chưa.

Lỗi thời gian chạy (Runtime Errors)

Mô tả: Lỗi này xảy ra khi ứng dụng chạy nhưng module native không hoạt động như mong đợi.

Cách xử lý:

  • Sử dụng công cụ debug như React Native Debugger hoặc Chrome Developer Tools để kiểm tra các thông tin chi tiết.
  • Sử dụng log (console.log, Log.d, NSLog) trong mã JavaScript hoặc native code (Java/Kotlin cho Android, Objective-C/Swift cho iOS) để xác định vị trí xảy ra lỗi.
  • Đảm bảo rằng các phương thức và thuộc tính trong module native đã được export và gọi chính xác trong React Native.

Lỗi liên quan đến quyền truy cập (Permission Issues)

Mô tả: Các module native yêu cầu quyền truy cập, ví dụ như camera, vị trí, hoặc bộ nhớ, có thể gây lỗi nếu quyền truy cập không được cấp.

Cách xử lý:

  • Kiểm tra và thêm các quyền cần thiết vào AndroidManifest.xml (Android) hoặc Info.plist (iOS).
  • Sử dụng thư viện như react-native-permissions để xử lý yêu cầu quyền truy cập trong thời gian thực.

Lỗi tương thích với hệ điều hành (OS Compatibility Issues)

Mô tả: Module native có thể hoạt động không như mong đợi trên một số phiên bản Android hoặc iOS cụ thể.

Cách xử lý:

  • Kiểm tra và cập nhật các thư viện native cho phù hợp với phiên bản hệ điều hành.
  • Đảm bảo rằng module được viết theo các chuẩn mới nhất của nền tảng Android/iOS.
  • Sử dụng các điều kiện kiểm tra phiên bản hệ điều hành trong code để điều chỉnh hành vi của module nếu cần thiết.

Lỗi không đồng bộ hóa (Asynchronous Issues)

Mô tả: Lỗi xảy ra khi việc gọi các phương thức native không đồng bộ hóa chính xác, gây ra lỗi trạng thái không nhất quán.

Cách xử lý:

  • Sử dụng Promises hoặc async/await trong mã JavaScript để đảm bảo việc gọi các phương thức native là không đồng bộ và chính xác.
  • Đảm bảo rằng các phương thức native trả về giá trị hoặc callback chính xác để xử lý kết quả.

Việc chuẩn bị và hiểu rõ cách xử lý các lỗi này không chỉ giúp đảm bảo quá trình tích hợp module native thành công mà còn thể hiện được kỹ năng giải quyết vấn đề và khả năng debug chuyên nghiệp của ứng viên trong mắt nhà tuyển dụng.

Câu hỏi phỏng vấn React Native về kiểm thử (Testing)

Cách thực hiện Unit Testing trong React Native 

Unit Testing trong React Native giúp đảm bảo các chức năng nhỏ của ứng dụng hoạt động chính xác. Để thực hiện Unit Testing trong React Native, các thư viện phổ biến nhất là JestReact Testing Library.

Sau đây là các bước cơ bản:

Cài đặt thư viện Jest:

  • Jest là thư viện mặc định cho Unit Testing trong các dự án React Native. Để cài đặt, có thể sử dụng lệnh: npm install –save-dev jest@testing-library/react-native
  • Sau khi cài đặt, cấu hình Jest trong file package.json hoặc tạo file cấu hình riêng jest.config.js.

Viết bài kiểm tra cho các thành phần:

Dùng @testing-library/react-native để kiểm tra các component. Ví dụ, nếu muốn kiểm tra một button hiển thị đúng, ta có thể viết như sau: 

import React from 'react';
import { render } from '@testing-library/react-native';
import MyButton from './MyButton';

test('Button renders with correct label', () => {
  const getByText } render(<MyButton label="Click me!" />);
  expect(getByText('Click me!')).toBeTruthy();
});

Bài kiểm tra này kiểm tra xem button có render đúng với label “Click me!” hay không.

Mock các thành phần liên quan:

Để đơn giản hóa Unit Test, các component phức tạp hoặc các API call có thể được mock. Ví dụ:

jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');

Chạy bài kiểm tra:

  • Sau khi viết các bài kiểm tra, bạn có thể chạy chúng bằng lệnh: npm test
  • Kết quả của bài kiểm tra sẽ hiển thị thông tin chi tiết về các test case đã pass hay fail.

Lợi ích của Unit Testing:

  • Phát hiện lỗi sớm: Giúp phát hiện lỗi từ sớm khi code thay đổi.
  • Dễ bảo trì: Khi code có Unit Test tốt, việc thay đổi hoặc bảo trì code sẽ trở nên dễ dàng hơn.
  • Tăng độ tin cậy: Đảm bảo rằng các thay đổi không ảnh hưởng tiêu cực đến các chức năng hiện có.

Giới thiệu về các công cụ kiểm thử phổ biến như Jest, Detox trong React Native

Jest

Jest là một framework kiểm thử phổ biến do Facebook phát triển, thường được sử dụng để kiểm thử các ứng dụng JavaScript, bao gồm cả ứng dụng React Native. Jest được biết đến với tốc độ nhanh và khả năng dễ dàng tích hợp với các dự án React Native.

Mục đích: Được sử dụng chủ yếu cho Unit Testing và Snapshot Testing. Unit Testing giúp kiểm tra các thành phần riêng lẻ của mã, trong khi Snapshot Testing kiểm tra giao diện của ứng dụng để phát hiện những thay đổi không mong muốn.

Ưu điểm:

  • Thiết lập dễ dàng và có cấu hình mặc định tốt cho các dự án React Native.
  • Có khả năng thực hiện các bài kiểm tra không đồng bộ hiệu quả.
  • Cộng đồng lớn và tài liệu hỗ trợ phong phú.

Ví dụ: Sử dụng Jest để kiểm tra một component đơn giản, đảm bảo rằng nó render đúng với các thuộc tính đầu vào đã cho.

Detox

Detox là một công cụ kiểm thử end-to-end (E2E) được phát triển bởi Wix. Công cụ này giúp tự động hóa quá trình kiểm thử ứng dụng từ giao diện người dùng đến backend, kiểm tra toàn bộ ứng dụng từ đầu đến cuối.

Mục đích: Kiểm thử toàn bộ quá trình của ứng dụng, từ việc mở ứng dụng, điều hướng, nhập liệu, đến kiểm tra phản hồi của ứng dụng. Điều này giúp đảm bảo rằng ứng dụng hoạt động chính xác trên các thiết bị và nền tảng khác nhau.

Ưu điểm:

  • Tích hợp tốt với các ứng dụng React Native, cho phép kiểm tra giao diện thực tế của ứng dụng.
  • Hỗ trợ kiểm thử trên các thiết bị thật và giả lập.
  • Giúp phát hiện các lỗi không chỉ trong logic mã mà còn trong giao diện và trải nghiệm người dùng.

Ví dụ: Sử dụng Detox để kiểm thử chức năng đăng nhập, kiểm tra các luồng điều hướng, và xác nhận rằng các tương tác người dùng được xử lý đúng.

Làm sao để kiểm tra UI Component trong React Native? 

Kiểm thử bằng cách sử dụng Jest

Jest là một framework kiểm thử phổ biến trong cộng đồng React và React Native. Để kiểm tra UI Component, bạn có thể sử dụng Jest kết hợp với thư viện như react-test-renderer hoặc @testing-library/react-native.

Ví dụ, bạn có thể tạo một snapshot test để kiểm tra xem UI Component có thay đổi không sau mỗi lần chỉnh sửa mã. react-test-renderer sẽ tạo ra một bản ghi của UI hiện tại của component và so sánh với kết quả trước đó.

Đoạn mã mẫu sử dụng react-test-renderer:

import React from 'react';
import renderer from 'react-test-renderer';
import MyComponent from '../MyComponent';

test('renders correctly', () => {
  const tree = renderer.create(<MyComponent />).toJSON();
  expect(tree).toMatchSnapshot();
});

Kiểm thử hành vi với @testing-library/react-native

Thư viện @testing-library/react-native giúp kiểm thử hành vi của component bằng cách mô phỏng các tương tác của người dùng. Với thư viện này, bạn có thể kiểm tra sự hiện diện của các thành phần UI, các sự kiện như nhấn nút, hoặc nhập liệu.

Ví dụ để kiểm tra xem một nút có hiển thị đúng không và mô phỏng việc nhấn nút:

import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import MyComponent from '../MyComponent';

test('button press triggers event', () => {
  const getByText } render(<MyComponent />);
  const button = getByText('Click me');
  fireEvent.press (button);
  expect (someMockFunction).toHave BeenCalled();
});

Kiểm thử trên nhiều nền tảng

React Native cho phép ứng dụng chạy trên cả iOS và Android, do đó kiểm thử trên cả hai nền tảng là cần thiết. Một số công cụ như Jest hoặc @testing-library/react-native hỗ trợ việc kiểm thử đa nền tảng, nhưng cần đảm bảo rằng component tương thích và hoạt động tốt trên cả hai.

Cách mô phỏng một API response trong quá trình kiểm thử

Để mô phỏng API response trong quá trình kiểm thử, tôi sử dụng các công cụ như jest kết hợp với axios-mock-adapter hoặc msw. Với jest, tôi có thể sử dụng phương thức jest.mock() để thay thế các cuộc gọi API thực tế bằng các giá trị mô phỏng. Nếu sử dụng msw, tôi có thể dễ dàng mô phỏng các API endpoints và các phản hồi dựa trên các yêu cầu HTTP.

Ví dụ, với axios-mock-adapter, tôi có thể làm như sau:

import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { fetchData } from './api'; // Giả sử fetchData là function gọi API

// Tạo mock adapter
const mock = new MockAdapter(axios);

// Mô phỏng API response
mock.onGet('/data').reply (200, { data: 'response data' });

// Kiểm thử fetchData function
test('fetchData returns data', async () => {
  const response = await fetchData('/data');
  expect(response.data).toBe('response data');
});

Ngoài ra, nếu sử dụng msw, tôi có thể mô phỏng API với cách tiếp cận dễ dàng hơn và kiểm thử theo các kịch bản phức tạp. Việc này giúp kiểm thử ứng dụng React Native của tôi trong mọi trường hợp, từ phản hồi thành công đến các lỗi mạng, đảm bảo ứng dụng hoạt động ổn định trong mọi tình huống.

Thông qua các kỹ thuật này, tôi có thể đảm bảo ứng dụng React Native của mình sẽ xử lý API hiệu quả mà không cần phải kết nối với API thực tế trong quá trình phát triển và kiểm thử.

Làm thế nào để theo dõi và sửa lỗi trong một ứng dụng React Native?

Sử dụng Console.log và Debugging:

  • Đầu tiên, ứng viên nên đề cập đến việc sử dụng console.log() để in ra thông tin về trạng thái của ứng dụng, giá trị của các biến, và các điểm nghi ngờ trong mã nguồn. Việc này giúp phát hiện lỗi logic hoặc các vấn đề trong dòng chảy dữ liệu của ứng dụng.
  • Sử dụng công cụ React Native Debugger (hoặc Chrome DevTools) để thực hiện debug JavaScript và xem các console.log, theo dõi call stack, và xem các state trong ứng dụng.

Error Boundaries:

  • Để xử lý lỗi trong React Native, ứng viên cần biết cách sử dụng Error Boundaries để ngăn ngừa ứng dụng bị crash khi gặp lỗi không lường trước trong các component. Điều này giúp giữ cho ứng dụng vẫn hoạt động mượt mà, ngay cả khi có lỗi.

Sử dụng các công cụ ghi lại lỗi:

  • Sử dụng công cụ như Sentry hoặc Bugsnag để ghi lại và theo dõi các lỗi runtime trong ứng dụng. Các công cụ này giúp phát hiện lỗi trên cả môi trường sản xuất và phát triển, cung cấp thông tin chi tiết như stack trace, thông tin về người dùng gặp lỗi và các tình huống dẫn đến lỗi.

Theo dõi hiệu suất ứng dụng:

  • Dùng các công cụ như Flipper hoặc Reactotron để theo dõi và phân tích hiệu suất của ứng dụng. Những công cụ này cho phép theo dõi các chỉ số như thời gian phản hồi, thời gian render của component, và kiểm tra các vấn đề về bộ nhớ hoặc CPU.

Sử dụng công cụ kiểm tra tự động (Unit Testing và Integration Testing):

  • Để tránh các lỗi xảy ra sau khi triển khai, ứng viên cần nêu rõ việc viết các bài test unit và integration sử dụng các công cụ như Jest hoặc Detox để đảm bảo mã nguồn không có lỗi logic hoặc giao diện bị lỗi trong quá trình phát triển.

Câu hỏi phỏng vấn React Native về hiệu suất và tối ưu hóa

Làm thế nào để tối ưu hóa FlatList trong React Native? 

Sử dụng keyExtractor

FlatList yêu cầu một giá trị key duy nhất cho mỗi phần tử trong danh sách. Nếu không có key hoặc key không thay đổi khi dữ liệu thay đổi, React Native sẽ phải render lại toàn bộ danh sách, điều này gây lãng phí tài nguyên.

Sử dụng thuộc tính keyExtractor giúp đảm bảo rằng mỗi item trong danh sách có một key duy nhất và ổn định.

<FlatList
  data={data}
  keyExtractor={(item) => item.id.toString()}
  renderItem={( { item }) => <Text>{item.name}</Text>}
/>

Sử dụng getItemLayout

Khi bạn biết trước chiều cao của mỗi phần tử trong danh sách (hoặc chiều rộng đối với danh sách ngang), bạn có thể sử dụng getItemLayout để tối ưu hóa việc tính toán vị trí của các phần tử, giúp giảm thiểu việc tính toán lại layout khi cuộn qua danh sách.

<FlatList
  data={data}
  getItemLayout={(data, index) => (
    { length: ITEM_HEIGHT, offset: ITEM_HEIGHT✶ index, index }
  )}
  renderItem={( { item }) => <Text>{item.name}</Text>}
/>

Sử dụng initialNumToRendermaxToRenderPerBatch

FlatList cho phép bạn xác định số lượng phần tử đầu tiên được render và số phần tử tối đa được render trong mỗi lần batch. Việc cấu hình các thuộc tính này giúp giảm lượng phần tử được render ban đầu và khi cuộn, từ đó tối ưu hóa hiệu suất.

<FlatList
  data={data}
  initialNumToRender={10}
  maxToRenderPerBatch={5}
  renderItem={({ item }) => <Text>{item.name}</Text>}
/>

Sử dụng windowSize

Thuộc tính windowSize xác định số lượng các phần tử FlatList sẽ được giữ trong bộ nhớ trong khi người dùng cuộn. Giá trị mặc định của windowSize là 21, nhưng bạn có thể điều chỉnh nó để tối ưu hóa hiệu suất cho ứng dụng của mình.

<FlatList
  data={data}
  windowSize={5}
  renderItem={( { item }) => <Text>{item.name}</Text>}
/>

Sử dụng removeClippedSubviews

FlatList hỗ trợ thuộc tính removeClippedSubviews giúp loại bỏ các phần tử không còn hiển thị trên màn hình khỏi bộ nhớ. Điều này giúp giảm tải cho bộ nhớ của ứng dụng khi cuộn qua danh sách.

<FlatList
  data={data}
  removeClippedSubviews={true}
  renderItem={( { item }) => <Text>{item.name}</Text>}
/>

Tránh rendering quá nhiều component phức tạp trong renderItem

Đảm bảo rằng phần tử trong renderItem là nhẹ và không thực hiện quá nhiều tác vụ tính toán phức tạp. Bạn có thể tối ưu hóa bằng cách sử dụng React.memo để ngăn chặn việc re-render lại component nếu dữ liệu không thay đổi.

const MemoizedItem = React.memo(({ item }) => {
  return <Text>{item.name}</Text>;
});

<FlatList
  data={data}
  renderItem={( { item }) => <MemoizedItem item={item} />}
/>

Một vài nguyên nhân dẫn đến xảy ra việc tăng thời gian tải ứng dụng React Native là gì?

Kích thước lớn của bundle JavaScript

  • Nguyên nhân: Quá nhiều mã nguồn, thư viện không cần thiết hoặc các dependency thừa dẫn đến kích thước bundle lớn.
  • Hậu quả: Tăng thời gian tải và parse bundle khi ứng dụng khởi chạy.

Asset không được tối ưu hóa

  • Nguyên nhân: Hình ảnh, video, và các tài nguyên tĩnh khác không được nén hoặc định dạng phù hợp.
  • Hậu quả: Gây mất thời gian tải các asset lớn trong quá trình khởi động.

Quá trình khởi tạo module native chậm

  • Nguyên nhân: Các module native hoặc third-party sử dụng hàm khởi tạo phức tạp, xử lý nhiều logic không cần thiết khi ứng dụng khởi động.
  • Hậu quả: Làm tăng thời gian khởi động ứng dụng.

Sử dụng nhiều màn hình hoặc navigation stack nặng

  • Nguyên nhân: Tải trước quá nhiều màn hình hoặc khởi tạo navigation stack lớn.
  • Hậu quả: Tiêu tốn tài nguyên và làm chậm quá trình tải.

Fetch dữ liệu đồng bộ trong quá trình khởi chạy

  • Nguyên nhân: Lấy dữ liệu từ API hoặc xử lý các yêu cầu không cần thiết trước khi hiển thị màn hình đầu tiên.
  • Hậu quả: Tăng thời gian hiển thị giao diện chính.

Thiếu tối ưu hóa ở chế độ phát hành (production)

  • Nguyên nhân: Không bật chế độ minification, obfuscation, hoặc không bật Hermes engine trong phiên bản production.
  • Hậu quả: Ứng dụng mất nhiều thời gian xử lý hơn so với ứng dụng đã được tối ưu hóa.

Xử lý logic phức tạp trong mã nguồn

  • Nguyên nhân: Thực hiện các thao tác tính toán, xử lý logic nặng hoặc chờ đợi không cần thiết trong khi khởi động.
  • Hậu quả: Ứng dụng phải mất thêm thời gian để xử lý trước khi hiển thị giao diện chính.

Các phương pháp để cải thiện thời gian tải ứng dụng React Native là gì?

Để cải thiện thời gian tải ứng dụng React Native, tôi sẽ áp dụng một số phương pháp sau:

Giảm kích thước bundle

  • Sử dụng Code Splitting để chia mã nguồn thành các phần nhỏ, giúp tải các phần cần thiết trước và các phần khác khi người dùng cần.
  • Loại bỏ mã thừa và chỉ giữ lại các thư viện và mã cần thiết. Cũng nên sử dụng các công cụ như tree-shaking để loại bỏ mã không sử dụng.

Tối ưu hóa hình ảnh và tài nguyên

  • Dùng các hình ảnh có độ phân giải phù hợp với từng loại thiết bị, giúp giảm dung lượng tải về. Các công cụ như react-native-fast-image có thể giúp tải ảnh nhanh hơn.
  • Sử dụng các định dạng ảnh nén như WebP hoặc AVIF để giảm kích thước file ảnh.

Cải thiện tốc độ khởi tạo ứng dụng

  • Lazy loading: Tải các màn hình hoặc thành phần không cần thiết ngay lập tức mà chỉ tải khi người dùng tương tác.
  • Tối ưu hóa các Native Modules để giảm độ trễ trong việc giao tiếp giữa JavaScript và Native Code.

Tối ưu hóa Redux Store

  • Sử dụng Redux Toolkit để giúp giảm thiểu việc tải lại trạng thái không cần thiết, từ đó giảm bớt số lần render lại của UI.

Tối ưu hóa JavaScript và UI rendering

  • Tối ưu hóa việc render lại các component bằng cách sử dụng React.memoPureComponent để tránh render lại các component không thay đổi.
  • Đảm bảo rằng các component chỉ render khi thực sự cần thiết, sử dụng shouldComponentUpdate hoặc React.memo để kiểm soát quá trình này.

Preload các tài nguyên quan trọng

  • Preload các tài nguyên quan trọng như font, hình ảnh, hoặc API ngay khi ứng dụng khởi động để người dùng không phải chờ đợi trong lúc sử dụng ứng dụng.

Sử dụng Hermes Engine

  • Kích hoạt Hermes, một JavaScript engine tối ưu cho React Native, giúp giảm thời gian khởi tạo ứng dụng và cải thiện hiệu suất tổng thể.
  • Bằng cách áp dụng các phương pháp này, ứng dụng React Native sẽ được tối ưu hóa để có thời gian tải nhanh hơn, mang lại trải nghiệm mượt mà cho người dùng.

Giải thích về VirtualizedList và cách sử dụng nó 

VirtualizedList là một component thay thế cho FlatListSectionList khi bạn cần làm việc với các danh sách có số lượng phần tử lớn. Thay vì render tất cả các phần tử của danh sách cùng lúc, VirtualizedList chỉ render các phần tử hiện thị trong viewport, tức là các phần tử mà người dùng có thể thấy được trên màn hình tại thời điểm đó.

Điều này giúp giảm thiểu việc sử dụng bộ nhớ và cải thiện tốc độ tải ứng dụng, đặc biệt khi danh sách chứa hàng nghìn phần tử.

Lợi ích của VirtualizedList:

  • Hiệu suất cao: Chỉ render các phần tử hiện thấy trên màn hình, giúp tiết kiệm bộ nhớ và giảm tải cho ứng dụng.
  • Dễ dàng tùy chỉnh: Cho phép bạn tùy chỉnh cách lấy dữ liệu và render các phần tử, rất hữu ích khi làm việc với các dữ liệu phức tạp hoặc cần tối ưu hóa thêm.

Cách sử dụng VirtualizedList: VirtualizedList nhận vào các prop cơ bản như data, renderItem, và getItemCount, tương tự như FlatList, nhưng cần một số thông tin bổ sung để tính toán số lượng phần tử và chỉ render những phần tử cần thiết.

Ví dụ cơ bản về cách sử dụng VirtualizedList:

import React from 'react';
import { VirtualizedList, Text, View } from 'react-native';

const data = [...Array(1000).keys()]; // Một danh sách dài 1000 phần tử

const getItemCount = (data) => data.length; // Số lượng phần tử trong danh sách
const getItem = (data, index) => ({ key: index, title: `Item ${data[index]}` });

const renderItem = ({ item }) => (
  <View style={{ padding: 20 }}>
    <Text>{item.title}</Text>
  </View>
);

const MyList = () => (
  <VirtualizedList
    data={data}
    getItemCount={getItemCount}
    getItem={getItem}
    renderItem={renderItem}
    keyExtractor={(item) => item.key.toString()}
  />
);

export default MyList;

Trong ví dụ trên:

  • getItemCount: Xác định số lượng phần tử trong danh sách.
  • getItem: Trả về một đối tượng phần tử tại một chỉ mục nhất định.
  • renderItem: Định nghĩa cách mỗi phần tử trong danh sách sẽ được render.

Cách tối ưu hóa ảnh và các tài nguyên trong ứng dụng React Native

  • Sử dụng ảnh với kích thước phù hợp: Ứng viên cần giải thích cách sử dụng các công cụ để giảm kích thước ảnh trước khi đưa vào ứng dụng, như sử dụng các định dạng ảnh nén (WebP, JPEG) thay vì PNG. Ngoài ra, cần giới thiệu việc sử dụng ảnh với độ phân giải phù hợp với từng loại thiết bị (xhdpi, xxhdpi, xxxhdpi), để tránh việc tải các ảnh có độ phân giải quá cao trên các thiết bị có màn hình nhỏ.
  • Lazy loading (Tải ảnh khi cần): Một cách tối ưu khác là sử dụng kỹ thuật lazy loading để chỉ tải ảnh khi người dùng thực sự cần nhìn thấy nó (ví dụ: khi ảnh xuất hiện trong phần visible của màn hình). Điều này giúp giảm bớt tải trên mạng và giúp ứng dụng khởi động nhanh hơn.
  • Sử dụng thư viện như react-native-fast-image: Đây là một thư viện nổi bật giúp tối ưu hóa việc tải và hiển thị ảnh trong React Native. Thư viện này hỗ trợ caching (bộ nhớ đệm), giảm thiểu việc tải lại ảnh khi người dùng quay lại trang cũ, giúp ứng dụng phản hồi nhanh hơn.
  • Cải thiện tốc độ tải ảnh bằng cách sử dụng cache: Việc sử dụng bộ nhớ đệm cho ảnh giúp giảm thiểu số lần tải lại và tiết kiệm băng thông. Ứng viên cần biết cách tích hợp các giải pháp cache hiệu quả, như sử dụng Image.prefetch trong React Native để tải ảnh vào bộ nhớ đệm trước khi hiển thị.
  • Tối ưu hóa tài nguyên khác (Font, Icons, SVGs): Bên cạnh ảnh, ứng viên cũng cần biết cách tối ưu hóa các tài nguyên khác như font chữ và icons. Sử dụng SVGs thay vì ảnh bitmap cho các icon có thể giúp giảm kích thước ứng dụng. Đồng thời, việc sử dụng các font chữ hệ thống thay vì font tùy chỉnh (hoặc tải chúng từ nguồn ngoài) cũng giúp giảm bớt tài nguyên tải về.
  • Sử dụng công cụ xây dựng và phân phối ứng dụng tối ưu: Trong một số trường hợp, ứng viên có thể đề xuất sử dụng các công cụ xây dựng ứng dụng như react-native-image-resizer để tự động tối ưu hóa ảnh khi xây dựng ứng dụng, cũng như giảm dung lượng của ảnh khi phân phối qua các nền tảng.

Khi nào nên sử dụng PureComponent hoặc memo trong React Native?

Trong React Native, bạn nên sử dụng PureComponent hoặc memo khi bạn muốn tối ưu hóa hiệu suất của ứng dụng bằng cách ngừng render lại component không cần thiết. Nếu bạn làm việc với các class component và các props hoặc state không thay đổi nhiều, sử dụng PureComponent là một lựa chọn tốt. Còn nếu bạn làm việc với các functional component, memo là một lựa chọn phù hợp.

Tuy nhiên, bạn cần chú ý đến các props phức tạp, vì cả hai đều chỉ so sánh shallow. Đôi khi, việc cung cấp một hàm so sánh tùy chỉnh có thể là cần thiết để đạt được tối ưu hiệu suất.

Tình huống Giải pháp đề xuất  Lưu ý 
Sử dụng class component Sử dụng PureComponent để tránh render lại khi props hoặc state không thay đổi. PureComponent chỉ thực hiện so sánh shallow, không hiệu quả với props phức tạp (như object, array lồng nhau).
Sử dụng functional component Sử dụng React.memo để tối ưu hóa render. React.memo cũng chỉ so sánh shallow, cần cung cấp hàm so sánh tùy chỉnh nếu cần thiết.

Câu hỏi phỏng vấn React Native về công cụ và thư viện hỗ trợ

Các công cụ phổ biến để debug ứng dụng React Native

  • React Native Debugger: Đây là công cụ chính thức được tích hợp với Chrome Developer Tools, giúp debug cả JavaScript và các vấn đề về giao diện người dùng. React Native Debugger cho phép theo dõi trạng thái Redux, xem log, và thực hiện các thao tác debugging mạnh mẽ.
  • Flipper: Flipper là công cụ đa năng dùng để debug ứng dụng React Native. Flipper hỗ trợ nhiều tính năng như theo dõi API, kiểm tra hiệu suất, và xem log. Nó cũng cung cấp các plugin cho việc kiểm tra cơ sở dữ liệu SQLite, kiểm tra dữ liệu và trạng thái của Redux, và thậm chí là debug native code.
  • Console.log(): Mặc dù đơn giản, console.log() vẫn là công cụ debug cơ bản nhất trong quá trình phát triển ứng dụng React Native. Việc chèn các dòng log trong mã giúp theo dõi luồng dữ liệu và phát hiện vấn đề trong ứng dụng.
  • React Developer Tools: Đây là một extension cho Chrome, giúp theo dõi và phân tích các component trong React Native, xem các props và state của chúng, cũng như kiểm tra các thay đổi trong quá trình ứng dụng chạy.
  • Xcode/Android Studio: Đối với các vấn đề native, việc sử dụng Xcode trên iOS hoặc Android Studio trên Android là rất quan trọng. Các IDE này có công cụ debug mạnh mẽ, giúp kiểm tra các vấn đề liên quan đến native code, cấu hình, và hiệu suất của ứng dụng.

Sử dụng Expo trong React Native có lợi ích gì? 

Dưới đây là một số lợi ích khi sử dụng Expo:

  • Cài đặt và cấu hình đơn giản: Expo giúp giảm thiểu các bước cài đặt và cấu hình ban đầu. Người dùng không cần phải lo lắng về việc thiết lập môi trường phát triển hoặc cấu hình các công cụ như Android Studio, Xcode, vì Expo cung cấp sẵn tất cả các công cụ này trong một package dễ sử dụng.
  • Chạy ứng dụng ngay lập tức trên điện thoại: Với Expo Go, bạn có thể chạy ứng dụng trực tiếp trên thiết bị di động mà không cần phải biên dịch lại. Điều này giúp tiết kiệm thời gian phát triển và thử nghiệm.
  • Hỗ trợ nhiều tính năng sẵn có: Expo cung cấp rất nhiều API và thư viện sẵn có như camera, location, notifications, và nhiều tính năng khác mà bạn không cần phải cài đặt riêng biệt hoặc xử lý native code. Điều này giúp giảm thiểu khối lượng công việc và tăng tốc độ phát triển.
  • Cộng đồng mạnh mẽ và tài liệu đầy đủ: Expo có một cộng đồng hỗ trợ lớn và tài liệu chi tiết, dễ tiếp cận. Điều này giúp lập trình viên dễ dàng tìm kiếm và giải quyết vấn đề trong quá trình phát triển.
  • Khả năng xuất bản nhanh chóng: Expo giúp bạn dễ dàng xuất bản ứng dụng lên App Store và Google Play mà không cần phải làm việc với native code hoặc công cụ như Xcode và Android Studio.

Các thư viện hỗ trợ cho việc xây dựng UI trong React Native như NativeBase, React Native Paper

NativeBaseReact Native Paper. Đây là các thư viện phổ biến giúp tăng tốc quá trình phát triển ứng dụng, cung cấp các thành phần UI đã được tối ưu hóa và thiết kế sẵn.

  • NativeBase là một thư viện UI được thiết kế để hoạt động trên cả Android và iOS, với mục tiêu giúp lập trình viên xây dựng giao diện người dùng đẹp và nhất quán mà không cần phải tự tay tạo ra các thành phần như buttons, forms, modals, tabs, v.v. NativeBase hỗ trợ cả theme tùy chỉnh, giúp dễ dàng thay đổi giao diện ứng dụng một cách linh hoạt.
  • React Native Paper là một thư viện UI được xây dựng dựa trên các nguyên lý của Material Design, giúp ứng dụng của bạn có một giao diện hiện đại, đẹp mắt và dễ sử dụng. React Native Paper cung cấp các thành phần UI như buttons, dialogs, cards, lists, và nhiều thành phần khác, đồng thời hỗ trợ chủ động tùy chỉnh giao diện theo yêu cầu của dự án.

Làm thế nào để sử dụng React Native CLI để tạo và quản lý dự án? 

Để tạo và quản lý dự án React Native, tôi sẽ sử dụng React Native CLI, một công cụ dòng lệnh mạnh mẽ hỗ trợ tạo và phát triển các ứng dụng React Native. Các bước cơ bản như sau:

Bước 1: Cài đặt React Native CLI

Trước tiên, tôi cần cài đặt React Native CLI nếu chưa có. Điều này có thể thực hiện bằng cách sử dụng npm (hoặc yarn):

npm install -g react-native-cli

Bước 2: Tạo dự án mới

Sau khi cài đặt xong, tôi có thể tạo một dự án mới bằng lệnh:

npx react-native init MyProject

Lệnh này sẽ tạo một thư mục dự án với cấu trúc mặc định, bao gồm các tệp cần thiết để bắt đầu phát triển ứng dụng React Native.

Bước 3: Chạy dự án trên thiết bị mô phỏng hoặc thiết bị thật

Sau khi tạo xong dự án, tôi có thể sử dụng các lệnh để chạy ứng dụng trên thiết bị mô phỏng (iOS hoặc Android):

  • Đối với iOS:
npx react-native run-ios
  • Đối với Android:
npx react-native run-android

Bước 4: Quản lý các phụ thuộc và cấu hình dự án

React Native CLI hỗ trợ quản lý các phụ thuộc (dependencies) thông qua npm hoặc yarn. Tôi có thể thêm các thư viện cần thiết cho dự án bằng lệnh:

npm install <package-name>

Bước 5: Xử lý các vấn đề khi phát triển

Khi phát triển, tôi sẽ thường xuyên sử dụng các công cụ như react-native start để khởi động server Metro (bundler của React Native) và theo dõi các thay đổi của ứng dụng.

Ngoài ra, React Native CLI còn hỗ trợ các lệnh hữu ích khác như link (để liên kết các thư viện native), clean (để làm sạch bộ nhớ cache), và upgrade (để nâng cấp React Native và các phụ thuộc).

Giới thiệu về Metro Bundler và vai trò của nó trong React Native

Metro Bundler là công cụ đóng gói (bundling) chính trong React Native, chịu trách nhiệm biên dịch và tối ưu hóa mã JavaScript của ứng dụng, chuyển chúng thành các tệp mà ứng dụng có thể sử dụng.

Vai trò của Metro Bundler trong phát triển React Native là cực kỳ quan trọng, vì nó giúp tiết kiệm thời gian phát triển nhờ vào tính năng hot reload, giúp lập trình viên thấy các thay đổi trong mã nguồn ngay lập tức mà không phải tải lại ứng dụng hoàn toàn.

Tổng kết câu hỏi phỏng vấn React Native

Bằng việc chuẩn bị kỹ càng các câu hỏi phỏng vấn React Native kể trên, ứng viên sẽ có thể tự tin hơn khi đối mặt với những câu hỏi phỏng vấn về React Native, thể hiện được kiến thức sâu rộng và khả năng áp dụng các công cụ, thư viện để phát triển ứng dụng hiệu quả.

Ngoài ra, việc hiểu rõ các khái niệm và công cụ quan trọng sẽ giúp lập trình viên React Native có thể phát triển ứng dụng mượt mà và tối ưu hóa quá trình phát triển, mang lại trải nghiệm người dùng tốt nhất.