در
قسمت اول این سری، با مفاهیم توابع خالص و ناخالص آشنا شدیم و همچنین عنوان شد که هرگونه تعامل با یک API خارجی به عنوان یک اثر جانبی و یا side effect در نظر گرفته شده و توابع را ناخالص میکند. به علاوه Redux تنها امکان کار با توابع خالص قابل پیش بینی را دارد و همچنین dispatch تمام اکشنها توسط آن، به صورت پیشفرض synchronous است و نه asynchronous. اما در برنامههای واقعی نیاز است بتوان با یک API خارجی کارکرد و اطلاعاتی را از آن دریافت نمود و یا به آن ارسال کرد. برای رفع این مشکل، یک کتابخانهی کمکی به نام redux-thunk ایجاد شدهاست که جزئیات کار با آنرا در این قسمت بررسی میکنیم.
معرفی کتابخانهی Redux Thunk
thunk، تابعی است که خروجی تابعی دیگر است؛ مانند مثال زیر:
function definitelyNotAThunk() {
return function aThunk() {
console.log('Hello, I am a thunk.');
}
}
هدف اصلی از انجام یک چنین کاری، فراهم آوردن روشی برای اجرای به تاخیر افتادهاست. برای مثال زمانیکه برای اجرای آن مینویسیم ()definitelyNotAThunk، تابع درونی آن هنوز اجرا نشدهاست. اجرای کامل آن چنین شکلی را دارد: ()()definitelyNotAThunk. حالا فرض کنید میانافزاری پیش از اجرای reducer قرار گرفتهاست که به تمام اشیاء رسیدهی به آن (یا همان اکشنها در اینجا) گوش فرا میدهد. اگر اکشنی بجای یک شیء، یک تابع را بازگرداند، این میانافزار، آنرا اجرا میکند یا همان ()() که عنوان شد. این کل کاری است که میانافزار
14 سطری redux-thunk انجام میدهد. زمانیکه از این میانافزار استفاده میشود، تابع درونی، دو پارامتر dispatch و getState را دریافت میکند. پارامتر dispatch که در حقیقت یک متد است، امکان اجرای اعمال synchronous و ارسال آنها را به سمت reducer متناظر، میسر میکند.
برای مثال فرض کنید که نیاز است یک فراخوانی Ajax ای صورت گیرد و پس از پایان آن، جهت به روز رسانی state، یک شیء اکشن، به سمت reducer متناظری dispatch شود. مشکل اینجا است که نمیتوان به Redux، یک callback حاصل از دریافت نتیجهی عملیات Ajax ای و یا یک Promise را ارسال کرد. تمام اینها یک اثر جانبی یا side effect هستند که با توابع خالص Redux ای سازگاری ندارند. برای مدیریت یک چنین مواردی، یک میانافزار را به نام redux-thunk ایجاد کردهاند که اجازهی dispatch تابعی را میدهد (همان thunk در اینجا) که قرار است action اصلی را در زمانی دیگر dispatch کند. به این ترتیب Redux اطلاعاتی را در مورد یک عمل async نخواهد داشت؛ میانافزاری در این بین آنرا دریافت میکند و زمانیکه آنرا dispatch میکنیم، آنگاه اکشن متناظر با آن، به redux منتقل میشود. به این ترتیب امکان منتظر ماندن تا زمان رسیدن پاسخ از شبکه، میسر میشود.
فرض کنید یک action creator متداول به صورت زیر ایجاد شدهاست:
export const getAllItems = () => ({
type: UPDATE_ALL_ITEMS,
items,
});
اکنون این سؤال مطرح میشود که چگونه میتوان متوجه شد، پاسخی از سمت API دریافت شدهاست؟
برای پاسخ به این سؤال، اینبار action creator فوق را بر اساس الگوی redux-thunk به صورت زیر بازنویسی میکنیم:
export const getAllItems = () => {
return dispatch => {
Api.getAll().then(items => {
dispatch({
type: UPDATE_ALL_ITEMS,
items,
});
});
};
};
اینبار action creator ای را داریم که بجای بازگشت یک شیء، یک تابع را بازگشت دادهاست که به آن thunk گفته میشود و پارامتر dispatch را دریافت میکند. در این حالت زمانیکه یک Promise بازگشت داده میشود (امکان منتظر نتیجه شدن را فراهم میکند)، کار dispatch اکشن اصلی مدنظر (برای مثال ارسال آرایهای از اشیاء)، صورت میگیرد.
برپایی پیشنیازها
در اینجا برای افزودن کامپوننتی که اطلاعات خودش را از یک API خارجی تامین میکند، از همان برنامهی به همراه کامپوننت شمارشگر که در
قسمت قبل آنرا تکمیل کردیم، استفاده میکنیم. فقط در آن کتابخانههای Axios و همچنین redux thunk را نیز نصب میکنید. به همین جهت در ریشهی پروژهی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
> npm install --save axios redux-thunk
برنامهی backend مورد استفاده هم همان برنامهای است که از
قسمت 22 شروع به توسعهی آن کردیم و کدهای کامل آنرا از پیوستهای انتهای بحث، میتوانید دریافت کنید. این برنامه که در مسیر شروع شدهی با https://localhost:5001/api قرار میگیرد، جهت پشتیبانی از افعال مختلف HTTP مانند Get/Post/Delete/Update طراحی شدهاست. برای راه اندازی آن، به پوشهی این برنامه، مراجعه کرده و فایل dotnet_run.bat آنرا اجرا کنید، تا endpointهای REST Api آن قابل دسترسی شوند. برای مثال باید بتوان به مسیر https://localhost:5001/api/posts آن در مرورگر دسترسی یافت.
پس از نصب پیشنیازها و راه اندازی برنامهی backend، در ابتدا فایل src\config.json را جهت درج مشخصات آدرس REST Api، ایجاد میکنیم:
{
"apiUrl": "https://localhost:5001/api"
}
در ادامه میخواهیم در برنامهی React خود، لیست مطالب برنامهی backend را از سرور دریافت کرده و نمایش دهیم.
افزودن میانافزار redux-thunk به برنامه
فرض کنید در قسمتی از صفحه، در کامپوننتی مجزا، دکمهای وجود دارد و با کلیک بر روی آن، قرار است اطلاعاتی از سرور دریافت شده و در کامپوننت مجزای دیگری نمایش داده شود:
چون نیاز به عملیات async وجود دارد، باید از میانافزار مخصوص thunk برای انجام آن استفاده کرد. برای این منظور به فایل src\index.js مراجعه کرده و میانافزار thunk را توسط تابع applyMiddleware، به متد createStore، معرفی میکنیم:
import { applyMiddleware, compose, createStore } from "redux";
import thunk from "redux-thunk";
//...
const store = createStore(
reducer,
compose(
applyMiddleware(thunk),
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)
);
//...
در اینجا چون نیاز است چندین تابع را به متد createStore ارسال کرد، باید از متد compose برای اعمال دسته جمعی آنها کمک گرفت.
دریافت اطلاعات از یک API خارجی به کمک redux-thunk
پس از آشنایی با روش کلی برقراری اتصالات سیستم react-redux در
قسمت قبل، پیاده سازی دریافت اطلاعات را بر اساس همان الگو، پیاده سازی میکنیم:
1) ایجاد نام نوع اکشن متناظر با دکمهی افزودن مقدار
به فایل src\constants\ActionTypes.js، نوع جدید دریافت مطالب را اضافه میکنیم:
export const GetPostsSuccess = "GetPostsSuccess";
export const GetPostsStarted = "GetPostsStarted";
export const GetPostsFailure = "GetPostsFailure";
در حین دریافت اطلاعات از API، حداقل سه اکشن نمایش loading (و یا GetPostsStarted در اینجا)، نمایش نهایی اطلاعات دریافت شدهی از سرور (و یا GetPostsSuccess در اینجا) و یا نمایش خطاهای حاصل (با نوع GetPostsFailure در اینجا) باید مدنظر باشند. به همین جهت سه نوع مختلف در اینجا تعریف شدهاند.
2) ایجاد متد Action Creator
در فایل src\actions\index.js، متد ایجاد کنندهی شیء اکشن ارسالی به reducer متناظری را تعریف میکنیم تا بتوان بر اساس نوع آن در reducer دریافت اطلاعات، منطق نهایی را پیاده سازی کرد:
import axios from "axios";
import { apiUrl } from "../config.json";
import * as types from "../constants/ActionTypes";
export const fetchPosts = () => {
return (dispatch, getState) => {
dispatch(getPostsStarted());
axios.get(apiUrl + "/posts").then(response => {
dispatch(getPostsSuccess(response.data)).catch(err => {
dispatch(getPostsFailure(err));
});
});
};
};
export const fetchPostsAsync = () => {
return async (dispatch, getState) => {
dispatch(getPostsStarted());
try {
const { data } = await axios.get(apiUrl + "/posts");
console.log(data);
dispatch(getPostsSuccess(data));
} catch (error) {
dispatch(getPostsFailure(error));
}
};
};
const getPostsSuccess = posts => ({
type: types.GetPostsSuccess,
payload: { posts }
});
const getPostsStarted = () => ({
type: types.GetPostsStarted
});
const getPostsFailure = error => ({
type: types.GetPostsFailure,
payload: {
error
}
});
- در اینجا همان الگوی بازگشت یک تابع را بجای یک شیء، در توابع action creator، مشاهده میکنید.
- تابع fetchPosts، از همان روش قدیمی callback، برای مدیریت اطلاعات دریافتی از سرور استفاده میکند. زمانیکه اطلاعاتی دریافت شد، آنرا با فراخوانی dispatch و با قالبی که تابع getPostsSuccess ارائه میدهد، به reducer متناظر، ارسال میکند.
- تابع fetchPostsAsync، نمونهی به همراه async/await کار با کتابخانهی axios است. هر دو روش callback و یا async/await در اینجا پشتیبانی میشوند.
به صورت پیشفرض action creators کتابخانهی redux از اعمال async پشتیبانی نمیکنند. برای رفع این مشکل پس از ثبت میانافزار thunk، اینبار متدهای action creator، بجای بازگشت یک شیء، یک تابع را بازگشت میدهند که این تابع درونی در زمانی دیگر توسط میانافزار thunk و پیش از رسیدن به reducer، فراخوانی خواهد شد. این تابع درونی، دو پارامتر dispatch و getState را دریافت میکند. هر دوی اینها نیز متد هستند. برای مثال اگر نیاز به دریافت وضعیت فعلی state در اینجا وجود داشت، میتوان متد ()getState رسیده را فراخوانی کرد و حاصل آنرا بررسی نمود. برای مثال شاید تصمیم گرفته شود که بر اساس وضعیت فعلی state، نیازی نیست تا اطلاعاتی از سرور دریافت شود و بهتر است همان اطلاعات کش شدهی موجود در state را بازگشت دهیم. البته در این مثال فقط از متد dispatch ارسالی، برای بازگشت نتیجهی نهایی به reducer متناظر، استفاده شدهاست.
- در نهایت آرایهی اشیاء مطلب دریافتی از سرور، به عنوان مقدار خاصیت posts شیء منتسب به خاصیت payload شیء ارسالی به reducer، در متد getPostsSuccess تعریف شدهاست. یعنی reducer متناظر، اطلاعات را از طریق خاصیت action.payload.posts شیء رسیده، دریافت میکند.
- همچنین دو اکشن شروع به دریافت اطلاعات (getPostsStarted) و بروز خطا (getPostsFailure) نیز در ابتدا و در قسمت catch عملیات async، به سمت reducer متناظر، dispatch خواهند شد.
3) ایجاد تابع reducer مخصوص دریافت اطلاعات از سرور
اکنون در فایل جدید src\reducers\posts.js، بر اساس نوع شیء رسیده و مقدار action.payload.posts آن، کار تامین آرایهی posts موجود در state انجام میشود:
import * as types from "../constants/ActionTypes";
const initialState = { loading: false, posts: [], error: null };
export default function postsReducer(state = initialState, action) {
switch (action.type) {
case types.GetPostsStarted:
return {
loading: true,
posts: [],
error: null
};
case types.GetPostsSuccess:
return {
loading: false,
posts: action.payload.posts,
error: null
};
case types.GetPostsFailure:
return {
loading: false,
posts: [],
error: action.payload.error
};
default:
return state;
}
}
در این reducer با استفاده از یک switch، سه حالت ممکنی را که اکشنهای رسیدهی به آن میتوانند داشته باشند، مدیریت کردهایم:
- در حالت آغاز کار و یا GetPostsStarted، با تنظیم خاصیت loading به true، سبب نمایش یک div «لطفا منتظر بمانید» خواهیم شد.
- در حالت دریافت نهایی اطلاعات از سرور، خاصیت loading به false تنظیم میشود تا div «لطفا منتظر بمانید» را مخفی کند. همچنین آرایهی posts را نیز از payload رسیده استخراج کرده و به سمت کامپوننتها ارسال میکند.
- در حالت بروز خطا و یا GetPostsFailure، خاصیت error شیء action.payload استخراج شده و جهت نمایش div متناظری، بازگشت داده میشود.
پس از تعریف این reducer باید آنرا در فایل src\reducers\index.js به کمک combineReducers، با سایر reducerهای موجود، ترکیب و یکی کرد تا در نهایت این rootReducer در فایل index.js اصلی برنامه، جهت ایجاد store اصلی redux، مورد استفاده قرار گیرد:
import { combineReducers } from "redux";
import counterReducer from "./counter";
import postsReducer from "./posts";
const rootReducer = combineReducers({
counterReducer,
postsReducer
});
export default rootReducer;
تشکیل کامپوننتهای دکمهی دریافت اطلاعات و نمایش لیست مطالب
UI این قسمت از سه کامپوننت تشکیل شدهاست که کدهای کامل آنها را در ادامه مشاهده میکنید:
الف) کامپوننت src\components\FetchPosts.jsx
import React from "react";
const FetchPosts = ({ fetchPostsAsync }) => {
return (
<section className="card mt-5">
<div className="card-header text-center">
<button className="btn btn-primary" onClick={fetchPostsAsync}>
Fetch Posts
</button>
</div>
</section>
);
};
export default FetchPosts;
کار این کامپوننت، نمایش دکمهی Fetch Posts است. با کلیک بر روی آن قرار است action creator ای به نام fetchPostsAsync اجرا شود که کدهای آنرا پیشتر مرور کردیم.
همانطور که مشاهده میکنید، این کامپوننت هیچ اطلاعاتی از وجود کامپوننت دومی که قرار است لیست مطالب را نمایش دهد، ندارد. کارش تنها dispatch یک اکشن است.
بنابراین این کامپوننت از طریق props فقط یک اشارهگر به متد رویدادگردانی را دریافت میکند و اطلاعات دیگری را نیاز ندارد.
ب) کامپوننت src\components\Posts.jsx
import React from "react";
import Post from "./Post";
const Posts = ({ posts, loading, error }) => {
return (
<>
<section className="card mt-5">
<div className="card-header">
<h2>Posts</h2>
</div>
<div className="card-body">
{loading ? (
<div className="alert alert-info">Loading ...</div>
) : (
<div className="list-group list-group-flush">
{posts.map(post => (
<Post key={post.id} post={post} />
))}
</div>
)}
{error && <div className="alert alert-warning">{error.message}</div>}
</div>
</section>
</>
);
};
export default Posts;
این کامپوننت، آرایهای از اشیاء مطالب را دریافت کرده و با ایجاد حلقهای بر روی آنها، توسط کامپوننت Post، هر کدام را در صفحه درج میکند. بنابراین این کامپوننت اکشنی را dispatch نمیکند. فقط از طریق props، یک آرایهی posts، وضعیت جاری عملیات و خطاهای حاصل را باید دریافت کند.
در این کامپوننت اگر loading رسیده به true تنظیم شده باشد، یک div با عبارت loading نمایش داده میشود. در غیراینصورت، لیست مطالب را درج میکند. همچنین اگر خطایی نیز رخ داده باشد، آنرا نیز درون یک div در صفحه نمایش میدهد.
ج) کامپوننت src\components\Post.jsx
import React from "react";
const Post = ({ post }) => {
return (
<article className="list-group-item">
<header>
<h2>{post.title}</h2>
</header>
<p>{post.body}</p>
</article>
);
};
export default Post;
این کامپوننت کار نمایش جزئیات هر رکورد مطلب را به عهده دارد؛ مانند نمایش عنوان و بدنهی یک مطلب.
اتصال کامپوننتهای FetchPosts و Posts به مخزن redux
مرحلهی آخر کار، تامین state کامپوننتهای FetchPosts و Posts از طریق props است. به همین جهت باید دو دربرگیرنده را برای این دو کامپوننت ایجاد کنیم.
الف) ایجاد دربرگیرندهی کامپوننت FetchPosts
برای این منظور فایل جدید src\containers\FetchPosts.js را با محتوای زیر ایجاد میکنیم:
import { connect } from "react-redux";
import { fetchPostsAsync } from "../actions";
import FetchPosts from "../components/FetchPosts";
const mapDispatchToProps = {
fetchPostsAsync
};
export default connect(null, mapDispatchToProps)(FetchPosts);
- کار این تامین کننده، اتصال action creator ای به نام fetchPostsAsync به props کامپوننت FetchPosts است.
- چون اطلاعات state ای قرار نیست به این کامپوننت ارسال شود، تابع mapStateToProps را در اینجا مشاهده نمیکنید و با نال مقدار دهی شدهاست.
ب) ایجاد دربرگیرندهی کامپوننت Posts
برای این منظور فایل جدید src\containers\Posts.js را با محتوای زیر ایجاد میکنیم:
import { connect } from "react-redux";
import Posts from "../components/Posts";
const mapStateToProps = state => {
console.log("PostsContainer->mapStateToProps", state);
return {
...state.postsReducer
};
};
export default connect(mapStateToProps)(Posts);
- کار این تامین کننده، اتصال خاصیت posts بازگشت داده شدهی از طریق postsReducer، به props کامپوننت Posts است. البته چون کامپوننت Posts سه خاصیت { posts, loading, error } را از طریق props دریافت میکند، با استفاده از spread operator، هر سه خاصیت دریافتی از reducer را به صورت یک شیء جدید بازگشت دادهایم.
- کامپوننت Posts رویدادی را سبب نخواهد شد. به همین جهت تابع mapDispatchToProps را در اینجا تعریف و ذکر نکردهایم.
استفاده از کامپوننتهای دربرگیرنده جهت نمایش نهایی کامپوننتهای تحت کنترل Redux
اکنون به فایل src\App.js مراجعه کرده و دو تامین کنندهی فوق را درج میکنیم:
import "./App.css";
import React from "react";
import CounterContainer from "./containers/Counter";
import FetchPostsContainer from "./containers/FetchPosts";
import PostsContainer from "./containers/Posts";
function App() {
const prop1 = 123;
return (
<main className="container">
<div className="row">
<div className="col">
<CounterContainer prop1={prop1} />
</div>
<div className="col">
<FetchPostsContainer />
</div>
<div className="col">
<PostsContainer />
</div>
</div>
</main>
);
}
export default App;
در اینجا FetchPostsContainer و PostsContainer سبب خواهند شد تا اتصالات مخزن اصلی redux، به کامپوننتهایی که توسط آنها دربرگرفته شدهاند، برقرار شود و کار تامین props آنها صورت گیرد.
یک نکته: برای مثال در انتهای کامپوننت FetchPosts، سطر export default FetchPosts را داریم. اگر این سطر را حذف کنیم و بجای آن export default connect فوق را قرار دهیم، دیگر نیازی نخواهد بود تا FetchPostsContainer را از دربرگیرندهها، import کرد و سپس بجای درج المان </FetchPosts> نوشت </FetchPostsContainer>. میتوان همانند قبل از همان نام متداول </FetchPosts> استفاده کرد و import انجام شده نیز همانند سابق از همان فایل ماژول کامپوننت صورت میگیرد. یعنی میتوان پوشهی containers را حذف کرد و کدهای آن را دقیقا ذیل کلاس کامپوننت درج نمود.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید:
state-management-redux-mobx-part04-backend.zip و
state-management-redux-mobx-part04-frontend.zip