کتابخانه redux-saga به ما کمک میکند تا بتوانیم توابع action creator ناهمزمان ایجاد کنیم.
برای استفاده از redux-saga ابتدا آنرا به پروژه خود اضافه میکنیم :
npm install redux react-redux redux-saga
فرض کنید ما درون پروژه خود میخواهیم از سمت سرور، لیست کاربران را دریافت کنیم. redux-saga در حقیقت یک middleware هست که اکشنهایی که به store ارسال میشوند را watch میکند و در مثال ما اگر type اکشن برابر با FETCH_USER_LIST بود، از سرور اطلاعات را بارگیری میکند و نتیجه را برای store ارسال میکند.
برای راحتی کار یک فایل بنام sagas.js ایجاد میکنیم. در این فایل ابتدا worker های مورد نیاز خود را اضافه میکنیم :
دقت کنید worker ها یک تابع سازنده یا function generator هستند.
import { call, put, takeEvery } from 'redux-saga/effects'; function fetchUsersApi(apiUrl) { return fetch(apiUrl) .then(response => response.json()) .catch(error => ({ error })) } // workers function* fetchUsers() { try { const response:Response = yield call(fetchUsersApi,'https://dummyjson.com/users'); yield put({ type: 'USER_FETCH_SUCCEEDED', users: response }); } catch (error) { yield put({ type: 'USER_FETCH_FAILED', message: error }) } }
ابتدا worker ساخته شده را بررسی میکنیم :
// workers function* fetchUsers() { try { const response:Response = yield call(fetchUsersApi,'https://dummyjson.com/users'); yield put({ type: 'USER_FETCH_SUCCEEDED', users: response }); } catch (error) { yield put({ type: 'USER_FETCH_FAILED', message: error }) } }
ابتدا بوسیله تابع call میتوانیم از api اطلاعات را بگیریم و پس از آماده شدن response اکشن USER_FETCH_SUCCEEDED را با استفاده از تابع put برای store ارسال میکنیم یا اصطلاحا dispach کنیم. ما میتوانیم تعداد زیادی worker تعریف کنیم، برای این مثال من دو worker نیاز دارم :
function* fetchUserById(action){ const id = action.payload.id; try { const response:Response = yield call(fetchUsersApi,'https://dummyjson.com/users/'+id); if(response.status){ yield put({ type: 'USER_BY_ID_FETCH_SUCCEEDED', users: response }); }else{ yield put({ type: 'USER_BY_ID_FETCH_FAILED', message: 'error' }) } } catch (error) { yield put({ type: 'USER_BY_ID_FETCH_FAILED', message: error }) } }
این worker هم همانند worker بالا ابتدا اطلاعات را از api میخواند و بر اساس نتیجه نوع response اکشن مناسب برای store می فرستد
وظیفه watcher بسیار ساده است. این تابع همواره در سطح middleware نگهبانی میدهد و با توجه به type اکشن ، worker مناسب را صدا میزند. برای درک بهتر این موضوع watcher خود را به فایل sagas اضافه میکنیم :
//watcher function* mySaga() { yield takeEvery('FETCH_USER_LIST', fetchUsers); yield takeEvery('FETCH_USER_BY_ID', fetchUserById); } export default mySaga;
تابع watcher هم همانند worker از نوع تابع سازنده میباشد.
این تابع با توجه به type اکشن، worker مورد نظر را صدا میزند. ما میتوانیم هز تعداد worker را درون تابع watcher خود اضافه کنیم. همین طور که از export مشخص است، ما در نهایت mySaga را برای استفاده در store لازم داریم. تابع takeEvery دو ورودی دریافت میکند، ورودی اول type اکشنهاست و ورودی دوم worker مرتبط با هر اکشن میباشد.
میتوان بجای takeEvery از تابع takeLatest هم استفاده کرد، اما این دوتابع دقیقا مثل یکدیگر عمل نمیکنند، تابع takeLatest همواره فقط یک درخواست fetch را بررسی میکند واگر درخواست دیگری از قبل بود، آن درخواست متوقف میکند.
فایل saga.js :
import { call, put, takeEvery } from 'redux-saga/effects'; function fetchUsersApi(apiUrl:string) { return fetch(apiUrl) .then(response => response.json()) .catch(error => ({ error })) } // workers function* fetchUsers() { try { const response:Response = yield call(fetchUsersApi,'https://dummyjson.com/users'); yield put({ type: 'USER_FETCH_SUCCEEDED', users: response }); } catch (error) { yield put({ type: 'USER_FETCH_FAILED', message: error }) } } interface Action{ type:'FETCH_USER_BY_ID' payload:number; } function* fetchUserById(action:Action){ const id = action.payload; try { const response:Response = yield call(fetchUsersApi,'https://dummyjson.com/users/'+id); if(response.status){ yield put({ type: 'USER_BY_ID_FETCH_SUCCEEDED', users: response }); }else{ yield put({ type: 'USER_BY_ID_FETCH_FAILED', message: 'error' }) } } catch (error) { yield put({ type: 'USER_BY_ID_FETCH_FAILED', message: error }) } } //watcher function* mySaga() { yield takeEvery('FETCH_USER_LIST', fetchUsers); yield takeEvery('FETCH_USER_BY_ID', fetchUserById); } export default mySaga;
پس از آماده شدن warcher ، باید saga را بعنوان یک middlewar به redux معرفی کنیم. به سراغ جایی میرویم که store خود را ایجاد کرده بودیم :
import { legacy_createStore as createStore,Store, applyMiddleware} from 'redux'; import initialState from './initialState'; import reducer from './reducer'; import createSagaMiddleware from 'redux-saga'; import mySaga from './sagas'; const sagaMiddleware = createSagaMiddleware(); const store:Store = createStore( reducer, initialState, applyMiddleware(sagaMiddleware) ); sagaMiddleware.run(mySaga); export default store;
ابتدا تابع createSagaMiddleware را از redux-sage به فایل خود import میکنیم. و بهوسیله این تابع یک sagaMiddleware ایجاد میکنیم :
import createSagaMiddleware from 'redux-saga'; const sagaMiddleware = createSagaMiddleware();
میانافزار sagaMiddleware را به store خود به عنوان یک middleware پاس میدهیم :
const store:Store = createStore( reducer, initialState, applyMiddleware(sagaMiddleware) );
پس از آن، باید watcher را که درون فایل saga.js ایجاد کرده بودیم به sagaMiddleware بدهیم :
sagaMiddleware.run(mySaga);
به سراغ فایل App.js میرویم :
import { useEffect} from 'react'; import { useSelector } from 'react-redux'; import { useDispatch } from 'react-redux/es/exports'; import {State} from './store/initialState'; function App() { const loading = useSelector((state)=>state.loading); const users = useSelector((state)=>state.users); const dispatch = useDispatch(); useEffect(()=>{ dispatch({type:'FETCH_USER_LIST'}) },[]); useEffect(()=>{ console.log(users); },[users]) if(loading){ return <h1>Loading</h1> } return <h1>Loaded!</h1> } export default App;
در این برنامه وقتی کامپوننت رندر شود اکشن زیر برای store ارسال میشود :
dispatch( {type:'FETCH_USER_LIST'} )
در سطح middleware وقتی که watcher با اکشنی که type آن مقدار FETCH_USER_LIST را دارد، روبرو میشود، worker متناظر با آن یعنی fetchUsers را صدا میزند :
//watcher function* mySaga() { yield takeEvery('FETCH_USER_LIST', fetchUsers); ... }
سپس درون این worker عملیات fetch از طریق api صورت گرفته و براساس api برگشتی، اکشن مورد نظر به store ارسال (dispatch) میشود، و در reducer بر روی state تغییرات اعمال میشود.
function* fetchUsers() { try { const response:Response = yield call(fetchUsersApi,'https://dummyjson.com/users'); yield put({ type: 'USER_FETCH_SUCCEEDED', users: response }); } catch (error) { yield put({ type: 'USER_FETCH_FAILED', message: error }) } }
امیدوارم پست مفید واقع شود.