0
0
Lập trình
Thaycacac
Thaycacac thaycacac

Quản lý State trong React Native với Redux Toolkit

Đăng vào 4 ngày trước

• 12 phút đọc

Giới thiệu

Quản lý state trong React Native có thể rất phức tạp khi ứng dụng phát triển. Các thành phần như bộ đếm, trường nhập liệu, và các yêu cầu API đều làm tăng độ phức tạp của ứng dụng. Để giải quyết vấn đề này, Redux Toolkit (RTK) ra đời như một giải pháp tuyệt vời. Nó giúp loại bỏ boilerplate, thực thi các thực tiễn tốt nhất và giữ cho trạng thái ứng dụng của bạn dễ dự đoán.

Trong bài viết này, chúng ta sẽ xây dựng một ứng dụng React Native với Redux Toolkit để xử lý ba trường hợp sử dụng phổ biến:

  • Quản lý trạng thái bộ đếm
  • Quản lý trạng thái trường nhập liệu
  • Lấy dữ liệu từ API

🗂 Cấu trúc Dự án

Dưới đây là cấu trúc thư mục mà chúng ta sẽ làm việc:

Copy
src/
 ┣ screens/
 ┃ ┣ HomeScreen/
 ┃ ┃ ┗ index.js
 ┃ ┗ ProfileScreen/
 ┣ store/
 ┃ ┣ slice/
 ┃ ┃ ┣ apiSlice.js
 ┃ ┃ ┣ counterSlice.js
 ┃ ┃ ┣ InputSlice.js
 ┃ ┗ store.js
 ┗ vendor/

Cấu trúc này phân tách rõ ràng giữa UI (màn hình)quản lý trạng thái (store). Mỗi tính năng sẽ có một slice riêng trong store/slice/, giúp giữ cho logic được phân tách và dễ bảo trì.

⚡ Cấu hình Redux Store

Trong store.js, chúng ta sẽ cấu hình Redux Toolkit với ba slices:

Copy
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './slice/counterSlice';
import inputReducer from './slice/InputSlice';
import apiReducer from './slice/apiSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    input: inputReducer,
    api: apiReducer,
  },
});

Bây giờ, Redux đã biết cách xử lý các trạng thái bộ đếm, nhập liệu và API một cách độc lập.

Bộ đếm Slice

Hãy bắt đầu với một bộ đếm đơn giản:

Copy
import { createSlice } from '@reduxjs/toolkit';

const initialState = { counterValue: 0 };

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: state => { state.counterValue += 1; },
    decrement: state => { state.counterValue -= 1; },
    reset: state => { state.counterValue = 0; },
  },
});

export const { increment, decrement, reset } = counterSlice.actions;
export default counterSlice.reducer;

Với chỉ vài dòng mã, chúng ta đã có ba hành động: tăng, giảm và thiết lập lại.

Input Slice

Bây giờ hãy xử lý đầu vào của người dùng:

Copy
import { createSlice } from '@reduxjs/toolkit';

const initialState = { inputValue: "Giá trị mặc định" };

export const inputSlice = createSlice({
  name: 'input',
  initialState,
  reducers: {
    changeInputValue: (state, action) => {
      state.inputValue = action.payload;
    },
  },
});

export const { changeInputValue } = inputSlice.actions;
export default inputSlice.reducer;

Mỗi khi người dùng nhập một cái gì đó, slice này sẽ cập nhật Redux store với giá trị mới nhất.

API Slice

Cuối cùng, hãy lấy một số dữ liệu từ API:

Copy
import { createSlice } from '@reduxjs/toolkit';

const initialState = { macbook: null };

const apiSlice = createSlice({
  name: 'api',
  initialState,
  reducers: {
    setMacbook: (state, action) => {
      state.macbook = action.payload;
    },
  },
});

export const { setMacbook } = apiSlice.actions;

export const fetchMacbook = () => async dispatch => {
  const response = await fetch('https://api.restful-api.dev/objects/7');
  const data = await response.json();
  dispatch(setMacbook(data));
};

export default apiSlice.reducer;

Khi được dispatch, slice này sẽ gọi API, lấy thông tin về MacBook và lưu trữ chúng trong Redux state.

Kết nối Redux với React Native

Trong App.js, bọc component gốc bằng Provider để làm cho Redux có sẵn trên toàn ứng dụng:

Copy
import React from 'react';
import HomeScreen from './src/screens/HomeScreen';
import { Provider } from 'react-redux';
import { store } from './src/store/store';

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

export default App;

🏡 Triển khai HomeScreen

Dưới đây là cách các slices kết hợp với nhau trong UI:

  • Phần bộ đếm → Tăng, giảm, thiết lập lại
  • Phần nhập liệu → Lưu và hiển thị văn bản
  • Phần API → Fetch và hiển thị thông tin về MacBook

Mã HomeScreen

Copy
import { StyleSheet, Text, TextInput, View, ScrollView, TouchableOpacity } from 'react-native';
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { decrement, increment, reset } from '../../store/slice/counterSlice';
import { changeInputValue } from '../../store/slice/InputSlice';
import { fetchMacbook } from '../../store/slice/apiSlice';

const HomeScreen = () => {
  const [value, setValue] = useState('');
  const { counterValue } = useSelector(state => state.counter);
  const { inputValue } = useSelector(state => state.input);
  const { macbook } = useSelector(state => state.api);
  const dispatch = useDispatch();

  return (
    <ScrollView style={styles.scrollContainer}>
      <View style={styles.container}>
        <Text style={styles.appTitle}>🚀 Ví dụ Redux Toolkit</Text>

        {/* Phần bộ đếm */}
        <View style={styles.smallCard}>
          <Text style={styles.smallCardTitle}>🔢 Bộ đếm: {counterValue}</Text>
          <View style={styles.buttonRow}>
            <TouchableOpacity style={[styles.smallButton, styles.incrementButton]} onPress={() => dispatch(increment())}>
              <Text style={styles.smallButtonText}>+</Text>
            </TouchableOpacity>
            <TouchableOpacity style={[styles.smallButton, styles.decrementButton]} onPress={() => dispatch(decrement())}>
              <Text style={styles.smallButtonText}>-</Text>
            </TouchableOpacity>
            <TouchableOpacity style={[styles.smallButton, styles.resetButton]} onPress={() => dispatch(reset())}>
              <Text style={styles.smallButtonText}>Reset</Text>
            </TouchableOpacity>
          </View>
        </View>

        {/* Phần nhập liệu */}
        <View style={styles.smallCard}>
          <Text style={styles.smallCardTitle}>📝 Đã lưu: {inputValue || 'Không có'}</Text>
          <View style={styles.inputRow}>
            <TextInput
              style={styles.compactInputStyle}
              value={value}
              onChangeText={setValue}
              placeholder="Nhập văn bản..."
              placeholderTextColor="#999"
            />
            <TouchableOpacity 
              style={[styles.smallButton, styles.saveButton]} 
              onPress={() => {
                dispatch(changeInputValue(value));
                setValue('');
              }}
            >
              <Text style={styles.smallButtonText}>Lưu</Text>
            </TouchableOpacity>
          </View>
        </View>

        {/* Phần API */}
        <View style={styles.card}>
          <Text style={styles.cardTitle}>🌐 Lấy dữ liệu API</Text>
          <TouchableOpacity 
            style={[styles.button, styles.apiButton]} 
            onPress={() => dispatch(fetchMacbook())}
          >
            <Text style={styles.buttonText}>📡 Lấy Dữ liệu MacBook</Text>
          </TouchableOpacity>

          {macbook && (
            <View style={styles.macbookContainer}>
              <Text style={styles.macbookTitle}>💻 {macbook.name}</Text>
              <View style={styles.specRow}>
                <Text style={styles.specLabel}>📅 Năm:</Text>
                <Text style={styles.specValue}>{macbook.data.year}</Text>
              </View>
              <View style={styles.specRow}>
                <Text style={styles.specLabel}>💰 Giá:</Text>
                <Text style={styles.specValue}>${macbook.data.price}</Text>
              </View>
              <View style={styles.specRow}>
                <Text style={styles.specLabel}>⚡ CPU:</Text>
                <Text style={styles.specValue}>{macbook.data['CPU model']}</Text>
              </View>
              <View style={styles.specRow}>
                <Text style={styles.specLabel}>💾 Bộ nhớ:</Text>
                <Text style={styles.specValue}>{macbook.data['Hard disk size']}</Text>
              </View>
            </View>
          )}
        </View>
      </View>
    </ScrollView>
  );
};

export default HomeScreen;

const styles = StyleSheet.create({
  scrollContainer: {
    flex: 1,
    backgroundColor: '#f8f9fa',
  },
  container: {
    flex: 1,
    padding: 20,
    paddingTop: 60,
  },
  appTitle: {
    fontSize: 32,
    fontWeight: 'bold',
    textAlign: 'center',
    marginBottom: 30,
    color: '#2c3e50',
    textShadowColor: 'rgba(0,0,0,0.1)',
    textShadowOffset: { width: 1, height: 1 },
    textShadowRadius: 2,
  },
  card: {
    backgroundColor: 'white',
    borderRadius: 16,
    padding: 20,
    marginBottom: 20,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    elevation: 5,
  },
  smallCard: {
    backgroundColor: 'white',
    borderRadius: 12,
    padding: 15,
    marginBottom: 15,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
    elevation: 3,
  },
  cardTitle: {
    fontSize: 20,
    fontWeight: '700',
    marginBottom: 15,
    color: '#34495e',
    textAlign: 'center',
  },
  smallCardTitle: {
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 10,
    color: '#34495e',
  },
  buttonRow: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    marginTop: 10,
  },
  button: {
    paddingVertical: 12,
    paddingHorizontal: 20,
    borderRadius: 25,
    alignItems: 'center',
    justifyContent: 'center',
    minWidth: 80,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.2,
    shadowRadius: 4,
    elevation: 3,
  },
  incrementButton: {
    backgroundColor: '#27ae60',
  },
  decrementButton: {
    backgroundColor: '#e74c3c',
  },
  resetButton: {
    backgroundColor: '#f39c12',
  },
  saveButton: {
    backgroundColor: '#3498db',
    marginTop: 15,
  },
  apiButton: {
    backgroundColor: '#9b59b6',
  },
  buttonText: {
    color: 'white',
    fontWeight: '600',
    fontSize: 16,
  },
  smallButton: {
    paddingVertical: 8,
    paddingHorizontal: 15,
    borderRadius: 20,
    alignItems: 'center',
    justifyContent: 'center',
    minWidth: 60,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.15,
    shadowRadius: 2,
    elevation: 2,
  },
  smallButtonText: {
    color: 'white',
    fontWeight: '600',
    fontSize: 14,
  },
  inputRow: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 10,
  },
  compactInputStyle: {
    flex: 1,
    borderWidth: 1,
    borderColor: '#bdc3c7',
    borderRadius: 8,
    padding: 10,
    fontSize: 14,
    backgroundColor: '#fff',
  },
  macbookContainer: {
    backgroundColor: '#f8f9fa',
    borderRadius: 12,
    padding: 20,
    marginTop: 20,
    borderLeftWidth: 5,
    borderLeftColor: '#3498db',
  },
  macbookTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#2c3e50',
    marginBottom: 15,
    textAlign: 'center',
  },
  specRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingVertical: 8,
    borderBottomWidth: 1,
    borderBottomColor: '#ecf0f1',
  },
  specLabel: {
    fontSize: 16,
    color: '#7f8c8d',
    fontWeight: '500',
  },
  specValue: {
    fontSize: 16,
    color: '#2c3e50',
    fontWeight: '600',
  },
});

👉 Giao diện sẽ tự động cập nhật khi trạng thái Redux thay đổi.

🎯 Kết quả Cuối

  • Nhấn nút +/- → Bộ đếm được cập nhật
  • Nhập văn bản → Được lưu trong Redux
  • Nhấn nút Fetch → Dữ liệu MacBook xuất hiện với tên, năm, giá, CPU và bộ nhớ

✅ Tại sao nên dùng Redux Toolkit?

  • Ít boilerplate hơn so với Redux truyền thống
  • Tích hợp Immer để đảm bảo tính bất biến
  • Thunk bất đồng bộ rõ ràng cho các cuộc gọi API
  • Dễ mở rộng cho các ứng dụng lớn

🚀 Kết luận

Redux Toolkit giúp quản lý state trong React Native dễ dàng, sạch sẽ và có thể mở rộng. Chỉ với một vài slices, chúng ta đã xây dựng được một bộ đếm, quản lý nhập liệu và một trình lấy dữ liệu API — tất cả trong một cấu trúc dễ bảo trì.

Nếu bạn đang bắt đầu một dự án mới hoặc tái cấu trúc một dự án cũ, Redux Toolkit là sự lựa chọn hoàn hảo!

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào