Làm quen REDUX + REDUX-SAGA trong ứng dụng REACT NATIVE

0
Dịch vụ dạy kèm gia sư lập trình

Wow! Hôm nay chúng ta sẽ cùng nhau thảo luận về một chủ đề rất thú vị. Đó là xây dựng ứng dụng mobile với với combo React Native + Redux + Redux-Saga. Sau đây là một vài kinh nghiệm thực tế về bộ đôi Redux và Redux-saga mà mình đã từng làm việc.

Kể từ React v16.8.0 đã có rất nhiều Hooks được giới thiệu. Những hooks này giúp cho cuộc sống của chúng ta trở nên dễ dàng hơn rất nhiều.

Các hooks mới này cho phép bạn sử dụng nhiều tính năng hơn của React mà không cần tới các class component. Các hooks cung cấp một API trực tiếp hơn cho các khái niệm React mà bạn đã rất quen thuộc như là: props, state, context, refs (quyền truy cập vào đối tượng bằng tham chiếu id), và lifecycle.

🔥 Đọc thêm về React Native:

Tích hợp Google Map vào ứng dụng

Map là một tính năng rất phổ biến và cần thiết với hầu hết các ứng dụng mobile. Trong các dịch vụ map phổ biến thì Google Map là cái tên đầu tiên mà bạn sẽ nghĩ tới.

Dưới đây là các chúng ta tích hợp Google map vào ứng dụng React Native. Trong ví dụ này, mình chỉ hướng dẫn cách tích hợp với phần Android. Phần iOS làm tương tự nên bạn tự làm nhé (để làm bài biết được ngắn gọn hơn)

Bước 1: Tạo dự án React Native mới trong Intellij IDEA hoặc Visual Studio Code

npm install — save react-native-maps
Hoặc
yarn add react-native-maps

Bước 2: Đối với phiên bản Android, mình cần thêm thẻ meta vào Android.manifest

<uses-permission android:name=”android.permission.ACCESS_COARSE_LOCATION” />
<uses-permission android:name=”android.permission.ACCESS_FINE_LOCATION” />
<meta-data
android:name=”com.google.android.geo.API_KEY”
android:value=”Google Api key”/>

Ngoài ra, bạn cũng cần phải viết code để xử lý việc xin cấp quyền từ người dùng trong Activity Java. Nếu bạn chưa rõ thì có thể tham khảo bài viết trước của mình: Runtime permission trong Android

Khi các thư viện được thêm vào dự án, nó cũng cần chạy một số lệnh clean trong thư mục /android. Bạn gõ lệnh sau trong cửa sổ terminal:

gradlew clean
npm start — — reset-cache

Để minh họa, mình phải tải xuống dữ liệu từ server, cách phổ thông nhất là sử dụng thư viện axios. Ngoài ra, mình cũng cần tạo vòng lặp để cập nhật dữ liệu sau mỗi 1 phút.

//create Api endpoint request
export const getPointsListFromApi = async (): Promise<MyApi[]> => {
  const response = await axios.get<MyApi[]>('ApiGetMarkers');
  return response.data;
};

//in React component we create a simple hook with endless cycle to update data through 60 seconds
useEffect(() => {
  fetchData();
  const timer = setInterval(() => {
    fetchData();
  }, 60000);
  return function cleanup() {
    clearInterval(timer);
  };
}, []);

function fetchData() {
  getPointsListFromApi()
  .then((json) => {
     buildMarkersOnMap(json);
  })
  .catch((ex) => console.error(ex.toString()));
}

Nhìn đoạn mã trên, bạn có nhân ra vấn đề không? Có đấy, hiện code đang viết lẫn lộn business logic và View. Điều này không tốt chút nào, đặc biệt cho việc thực hiện unit test.

Một mã nguồn clean phải đảm bảo yếu tố tách biệt giữa View và xử lý logic.

Nào hãy cùng mình refactoring với sự trợ giúp của Redux và Redux-Saga.

Ứng dụng Redux và Redux-Saga

Bản thân mình thích sử dụng Redux-Saga như một luồng riêng biệt để chạy các scenario không đồng bộ ở chế độ nền và trả về kết quả cho Component View.

Redux có cách tiếp cận giống như một cách tiếp cận của mô hình MVC. Component là một View UI, Action là một Controller, Reducer là Model (giữ trạng thái hiện tại) và Redux-Saga hoạt động như một service chạy background.  Store giống như một container cho state tập hợp tất cả các reducer.

Redux nó chỉ là một state container, hỗ trợ cách tiếp cận không đồng bộ. Khi một action xảy ra, đối tượng sẽ gửi đến store, sau khi reducer đó được gọi và nó sẽ làm mới state.

Trong trường hợp tiếp cận không đồng bộ, Redux sử dụng middleware Redux-Saga layer. Nó chạy sau action, nhưng trước khi reducer được gọi.

1. Trong file main.js, plug Root Saga từ saga.js vào store bằng cách sử dụng chức năng applyMiddleware.

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import reducer from './reducers'
import rootSaga from './sagas'

const sagaMiddleware = createSagaMiddleware()
const store = createStore( // mount it on the Store
  reducer,
  applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)

2. Ở thời điểm bắt đầu chúng mình phải xác định root. Root Saga chỉ cần kết hợp các generator của chúng (sagas) với nhau:

import {all, call} from 'redux-saga/effects';
import {WatchLocations} from './location/sagas';

export const rootSaga = function* root() {
  yield all([
    call(WatchLocations),
  ]);
};

Saga đã thực hiện như một middleware kiểm soát các tác dụng phụ không đồng bộ.

Redux-saga thực hiện điều đó với sự trợ giúp của generator ES6. Các Generator là các chức năng có thể bị tạm dừng hoặc tiếp tục lại khi cần thiết. Generator sẽ thực thi mã cho đến khi nó tìm thấy từ khóa yield.

Mặt khác, nếu bạn cần hủy saga khi có điều gì đó xảy ra từ bên ngoài (thời gian chờ xảy ra hoặc có action của người dùng) thì bạn nên sử dụng từ khóa yield race.

Từ khóa yield put cho Redux-Saga biết rằng action này nên được thực hiện.

Từ khóa yield take làm cho Redux-Saga bị block cho đến khi action cụ thể được gửi đi. Lưu ý, điều này không chặn UI (giao diện người dùng) hoặc bất kỳ quá trình xử lý nào khác trên giao diện.

3. Bây giờ mình sẽ xác định loop vô tận để download dữ liệu theo thời gian mỗi phút.

//create endless loop cycle to grab new data from remote datasource
export function* getLocationsSaga(): Generator<
  StrictEffect,
  void,
  MyApi[]
> {
  while (true) { //not stop on first action
    try {
      const result: CallReturnType<typeof getPointsListFromApi> = yield call(
        getPointsListFromApi,
      );
      yield put(getListSuccess(result)); //send SUCCESS action that will be catched by reducer
      yield delay(60000);
    } catch (error) {
      const {errorResponse}: {errorResponse: AxiosResponse} = error;
      yield put(getListFailed(errorResponse));
      yield put(stopWatcher());
    }
  }
}
export function* watchLocations() {
  while (true) {
    yield take(START_WATCHER);
    yield race([call(getLocationsSaga), take(STOP_WATCHER)]);
  }
}

Làm việc với Redux.

Store của mình sẽ bao gồm các Type, Action, Reducer và có thể là Selector.

Ban đầu, hãy xác định các type sẽ được sử dụng sau này:

export const GET_LIST = 'GET_LIST';
export const GET_SUCCESS = 'GET_SUCCESS';
export const GET_FAILED = 'GET_FAILED';
export const START_WATCHER = 'START_WATCHER';
export const STOP_WATCHER = 'STOP_WATCHER';

export type LocationActionTypes = {
  type: string;
  payload: string;
};

export interface GetListAction {
  type: typeof GET_LIST;
  payload: string;
}

export interface GetSuccessAction {
  type: typeof GET_SUCCESS;
  payload: MyApi[];
}

export interface GetFailedAction {
  type: typeof GET_FAILED;
  payload: any;
}

export interface StartWatcherAction {
  type: typeof START_WATCHER;
}

export interface StopWatcherAction {
  type: typeof STOP_WATCHER;
}

export type ActionTypes =
  | LocationActionTypes
  | GetListAction
  | GetSuccessAction
  | GetFailedAction
  | StartWatcherAction;

1. Một action là một đối tượng chứa thông tin.

Các action là nguồn thông tin duy nhất để Redux store được cập nhật. Chúng là những thứ duy nhất kích hoạt các thay đổi trong ứng dụng Redux, chúng chứa thông tin cho các thay đổi đối với store của ứng dụng. Store cập nhật Reducers dựa trên giá trị của action.type

export const getListSuccess = (
  myApi: MyApi[],
): GetSuccessAction => ({
  type: GET_SUCCESS,
  payload: myApi,
});

export const getListFailed = (
  error: AxiosResponse,
): GetFailedAction => ({
  type: GET_FAILED,
  payload: error,
});

export const startWatcher = (): StartWatcherAction => ({
  type: START_WATCHER,
});

export const stopWatcher = (): StopWatcherAction => ({
  type: STOP_WATCHER,
});

2. Reducer là một pure function để lấy state của ứng dụng nhận action làm đối số và trả về một state mới, nhưng không làm thay đổi state trước đó.

Các pure function là các function không có bất kỳ kết quả phụ nào và sẽ trả về cùng một kết quả nếu các đối số giống nhau được truyền vào.

Reducer cần khởi tạo state ban đầu của ứng dụng trong trường hợp cần tạo state mặc định hoặc null.

const initialState: MyState[] = [{}];

const locationReducer = (
  state: MyApi[] = initialState,
  action: ActionTypes,
) => {
  switch (action.type) {
    case GET_SUCCESS:
      return {
        ...state,
        plots: action.payload,
      };
    default:
      return state;
  }
};

export default locationReducer;

3. Redux-Saga có một  tham chiếu đến store của mình và nó gọi là trạng thái get, truyền state vào selector của mình. Bây giờ là lúc viết một React component để lấy dữ liệu và hiển thị các maker lên bản đồ.

Ok, nhiệm vụ của mình đến đây đã hoàn thành.

const dispatch = useDispatch();

// get result from Reducer
const resultList: MyApi[] = useSelector(
  (state: DefaultRootState) => state.location,
);

// send request to Redux-Saga
const getAvailableLocationPoints = useCallback(
  () => dispatch(startWatcher()),
  [dispatch],
);

// Component constructor first and last run
useEffect(() => {    
  getAvailableLocationPoints();

  return function cleanup() {
    //clean variables
  };
}, [dispatch]);

// redundant wait for new resultList
useEffect(() => {
  console.log('Our value changed');
  buildMarkers(resultList.plots);
}, [resultList]);

function buildMarkers(response: MyApi[]) {
  //now we have a array of location point and create a list of markers
}

Cải tiến thêm cho Reducer.

Trong trường hợp bạn cần implementation các business phức tạp, mình có thể chuyển nó ra Selector. Nó giúp cho reducer của mình trở nên nhỏ gọn và dễ đọc hơn.

Hãy tưởng tượng chúng ta cần filter dữ liệu:

function locationReducer(state = initialState, action) {
  switch (action.type) {
    case GET_SUCCESS:
      return state.set('filterKey', action.data.city);
    default:
      return state;
  }
}

import {createSelector} from 'reselect';
const selectCities = (state) => state.get('cities');
const getByKey = (state) => state.get('cities').get('filterKey');

const selectFiltered = () => createSelector(
  [selectCities, getByKey],
  (city, keyword) => city.get('cities').filter((item) => item.name.match(keyword))
);
export {selectCities, selectFiltered};

Kinh nghiệm sử dụng React Native + Redux + Redux-saga

Hãy cùng mình tìm hiểu một số cách xử lý đối tượng trong React Native nhé.

Singleton trong React:

Thỉnh thoảng rất hữu ích để chạy function một lần:

const useSingleton = (initializer) => {
    React.useState(initializer);
}
const MyFunctionalComponent = () => {
    useSingleton(() => {
    // run only once
    });
}

FC – Functional Component

FC là một stateless component và không có lifecycle. Vì vậy, bạn không thể chỉ định một hàm khởi tạo.

Bạn phải extend từ React.Component để tạo một stateful component, cái mà sẽ có constructor và bạn sẽ có thể sử dụng state.

Dưới đây là Stateless cho bạn tham khảo:

import React from 'react'const StatelessObject = ({title}) => (
  <div>{`${title}`}</div>
);
const [value, setValue] = useState(props.value || 0);

Để mô phỏng constructor trong FC, hãy sử dụng useEffect.

useEffect(() => {
// call some useful functions
}, []); 

useEffect hook kết hợp các phương thức React lifecycle cũ với nhau: componentDidMount, componentDidUpdate và componentWillUnmount.

Làm thế nào để notify cho component khi một biến đã thay đổi?

Không phải ai cũng biết thủ thuật này đâu nha.

const myList: MyDataSource[] = [];
useEffect(() => {
console.log(‘value has been changed’);
if (myList?.data) treatData(myList.data);
}, [myList]); //it is a way to use it to only re-run if variable changes

Hãy cẩn thận với useEffect khi không có depedency Array. Theo mặc định, useEffect luôn chạy sau khi render đã chạy. Điều này có nghĩa là nếu bạn không include một dependency array, và bạn đang sử dụng useEffect, bạn có thể kết thúc trong một loop vô hạn. Lúc này, ít nhất hãy sử dụng mảng trống [].

Đôi khi, dữ liệu array không phải lúc nào cũng được cập nhật, vậy nên chúng ta có thể sử dụng thủ thuật này:

Thay vì sử dụng dữ liệu trong mảng đối số thứ hai của useEffect(), chúng ta cũng có thể sử dụng [JSON.stringify (data)]:

useEffect(() => {
console.log(‘value has been changed’);
}, [JSON.stringify(data)]); // Changes will be caught :) !

Tiếp theo đến phần khá khó để xử lý hành vi của maker trên bản đồ. Maker có state có thể là active hoặc inactive. Hãy thay đổi màu sắc khi nhấp chuột trên bản đồ.

// first we have to declare the storage state
const [selectedMarkerIndex, setSelectedMarkerIndex] = useState('');

//store the selected index to use it when marker clicked
function handleMarkerClick(id: string, event: MapEvent<{action:'marker-press'; id:string;}>, index: number): void {
    setSelectedMarkerIndex(id);
}

  return (
    <View style={style.container}>
      <MapView>
        {markers.map((marker) => (
          <Marker
            pinColor={selectedMarkerIndex === marker.id ? 'yellow' : 'red'}
            key={`${marker.id}-${selectedMarkerIndex === marker.id ? 'active' : 'inactive'}`}
            coordinate={marker.coordinate}
            description={marker.description}
            onPress={(event) => handleMarkerClick(marker.id, event)}>
          </Marker>
        ))}
      </MapView>
    </View>
  );

Mình tin rằng đọc đến đây bạn đã hiểu được phần nào về React Native, Redux và Redux-Saga rồi đúng không nào? Nhưng đừng vội tắt máy, hãy đọc nốt phần kết luận dưới đây để có thể hiểu về chúng một cách khái quát nhất.

Kết luận

React Native cung cấp một cách tiếp cận khác để tạo ứng dụng di động thay vì sử dụng Flutter, Ionic, Android Java/Kotlin hoặc iOS Swift. Redux chia logic thành các types, actions, reducer và selector để làm cho mã nguồn clean hơn và có thể thực hiện unit test được. Redux-Saga giúp bạn thực hiện các logic dưới dạng chạy ngầm (backgroud service)

Vậy thôi bài viết về bộ ba React Native + Redux + Redux-Saga này xin kết thúc tại đây. Mình xin trân thành cảm ơn bạn đã dành thời gian đọc hết bài viết này. Nếu bạn có bất kì ý kiến gì, hãy để lại comment dưới này nhé.

Dịch vụ phát triển ứng dụng mobile giá rẻ - chất lượng
Bài trướcTại sao lại chọn Fastify framework thay vì ExpressJS?
Bài tiếp theo[Javascript] Các cách xóa phần tử trong mảng (Array)
Tên đầy đủ là Dương Anh Sơn. Tốt nghiệp ĐH Bách Khoa Hà Nội. Mình bắt đầu nghiệp coder khi mà ra trường chẳng xin được việc đúng chuyên ngành. Mình tin rằng chỉ có chia sẻ kiến thức mới là cách học tập nhanh nhất. Các bạn góp ý bài viết của mình bằng cách comment bên dưới nhé !

Bình luận. Cùng nhau thảo luận nhé!

avatar
  Theo dõi bình luận  
Thông báo