استخدام إضافة Redux لالتقاط تغييرات الحالة
Video Tutorial
شاهد كيفية تتبع الحالة باستخدام Redux
إذا كنت لا تفضّل القراءة، يمكنك متابعة هذا الدرس المصوّر الذي يوضّح لك كيفية تتبع حالة تطبيقات React الخاصة بك عند استخدام Redux
إذا كنت بحاجة إلى رؤية إضافية عند إعادة تشغيل جلسات مستخدميك، فإن إلقاء نظرة على حالة التطبيق يمكن أن يكون مفيداً للغاية.
في حالة Redux، يوفّر OpenReplay إضافة تتيح لك الاندماج في العمل الداخلي للـ store. ستتيح لك هذه الإضافة رؤية حالة الـ store الخاص بـ Redux والإجراءات (actions) التي تم إرسالها (dispatched) طوال الجلسة المسجّلة.
بمجرد الإعداد، ينبغي أن تكون قادراً على متابعة التغييرات في الـ store كما هو موضّح في لقطة الشاشة التالية:

إعداد Redux في مشروع Next.js
Section titled إعداد Redux في مشروع Next.jsفي هذا الدرس، سنستخدم هذا المستودع (الفرع redux-store) الخاص بموقع تجارة إلكترونية عام تم بناؤه باستخدام Next.js.
في هذا المشروع، سنستبدل مجموعة من المنتجات المميّزة بمجموعة جديدة من المنتجات التي تم جلبها من واجهة برمجة تطبيقات (API) خارجية.
ولتحقيق ذلك، سنضيف دالة لطلب المنتجات باستخدام Axios، وسنقوم بذلك من داخل إجراء (action) في Redux.
ملاحظة: هذا تطبيق Next.js معقّد، لذا قد لا يتّبع البنية القياسية الموجودة في تطبيقات قوائم المهام (To-Do) الكلاسيكية، ولكن باتباع هذا الدرس ينبغي أن تكون قادراً على مواكبة التغييرات.
تذكّر: يمكنك دائماً استنساخ (clone) المستودع ومراجعة الكود بنفسك.
سنبدأ بتثبيت جميع التبعيات الرئيسية باستخدام:
npm i next-redux-wrapper redux react-redux redux-thunk redux-devtools-extension
بعد الانتهاء من ذلك، أنشئ مجلداً باسم store في الدليل الجذر لمشروعك، وأعد إنشاء البنية التالية:

سيحتوي ملف types.js على تعريف الأنواع (type) للإجرائين (actions) اللذين سنقوم بتعريفهما:
export const GET_PRODUCTS = 'GET_PRODUCTS'
export const PRODUCTS_ERROR = 'PRODUCTS_ERROR'
سيُصدّر ملف store.js دالة تقوم، عند استدعائها، بإنشاء store جديد لـ Redux. وذلك لأننا سنحتاج إلى إضافة وسيط (middleware) جديد لـ Redux تُرجعه إضافة Redux (المزيد عن ذلك بعد قليل).
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducers'
const initalState = {}
export default function createReduxStore(extraMiddleware = []) {
const middleware = [thunk, ...extraMiddleware]
const store = createStore(
rootReducer,
initalState,
composeWithDevTools(applyMiddleware(...middleware))
)
return store
}
سيقوم ملف الـ reducer الخاص بنا (makeUpReducer.js) بتحديث الحالة إما بقائمة المنتجات أو برسالة الخطأ التي يتم إرجاعها عند حدوث مشكلة.
import { GET_PRODUCTS, PRODUCTS_ERROR } from '../types'
const initialState = {
makeUpProducts: [],
loading: true,
}
export default function (state = initialState, action) {
switch (action.type) {
case GET_PRODUCTS:
return {
...state,
makeUpProducts: action.payload,
loading: false,
}
case PRODUCTS_ERROR:
return {
loading: false,
error: action.payload,
}
default:
return state
}
}
وأخيراً، سيقوم ملف الإجراء (action) بتعريف دالة واحدة، تتولّى مهمة جلب قائمة المنتجات من واجهة برمجة تطبيقات (API) خارجية وإرسال (dispatch) الإجراء (action) والحمولة (payload) الصحيحين:
import { GET_PRODUCTS, PRODUCTS_ERROR } from '../types'
import axios from 'axios'
import slugify from 'slugify'
export const getMakeUpProducts = () => async (dispatch: any) => {
console.log('Getting the makeup products')
try {
let { data } = await axios.get(
'https://makeup-api.herokuapp.com/api/v1/products.json?brand=maybelline&apiKey=123fff132'
)
const products = data
let newProds = products.map((p: any) => {
return {
id: '' + p.id,
slug: slugify(p.name),
name: p.name,
description: '',
images: [{ url: p.image_link }],
variants: [],
price: {
value: +p.price,
},
options: [],
}
})
dispatch({
type: GET_PRODUCTS,
payload: newProds,
})
} catch (e) {
dispatch({
type: PRODUCTS_ERROR,
payload: e,
})
}
}
إعداد مزوّد المتتبّع (tracker provider)
Section titled إعداد مزوّد المتتبّع (tracker provider)سيتيح لك هذا المزوّد تهيئة مجموعة من الإضافات، وفي حالتنا سنستخدم إضافة Redux على النحو التالي (من داخل ملف _app.tsx)
//...more imports here....
import TrackerProvider from '../context/trackerProvider'
import trackerRedux from '@openreplay/tracker-redux'
// ... more code here....
export default function MyApp({ Component, pageProps }: AppProps) {
const Layout = (Component as any).Layout || Noop
useEffect(() => {
document.body.classList?.remove('loading')
}, [])
let plugins = [
{
fn: trackerRedux,
name: 'redux',
config: {},
},
]
return (
<TrackerProvider config={{ plugins }}>
<Head />
<ManagedUIContext>
<Layout pageProps={pageProps}>
<Component {...pageProps} />
</Layout>
</ManagedUIContext>
</TrackerProvider>
)
}
الآن، يتيح لك هذا الكود إعداد المتتبّع (tracker) مع الإضافة الصحيحة، ولكن لكي تعمل الإضافة، سنحتاج إلى الوصول إلى الوسيط (middleware) الذي يتم إرجاعه عند استدعاء الإضافة. وهذا يعني أنه سيتعيّن علينا تتبّع القيم التي تُرجعها إضافاتنا حتى نتمكّن من استخدامها في مكان آخر. في هذه الحالة، سنحتاج إلى استخدامه عند استدعاء دالة createReduxStore المذكورة أعلاه.
ولفعل ذلك، علينا توسيع TrackerProvider للتأكّد من الاحتفاظ بالقيمة المُرجَعة داخل الحالة، على النحو التالي (يمكنك مراجعة النسخة الكاملة من هذا الملف هنا):
import { createContext, useCallback } from 'react'
import Tracker from '@openreplay/tracker'
import { v4 as uuidV4 } from 'uuid'
import { useReducer } from 'react'
export const TrackerContext = createContext()
function defaultGetUserId() {
return uuidV4()
}
function newTracker(config) {
///code here
}
function reducer(state, action) {
switch (action.type) {
case 'init': {
if (!state.tracker) {
console.log('Instantiaing the tracker for the first time...')
let t = newTracker(state.config)
let pluginsReturnedValue = {}
if (state.config.plugins) {
state.config.plugins.forEach((p) => {
console.log('Using plugin...')
pluginsReturnedValue[p.name] = t.use(p.fn(p.config)) //keep track
})
}
return {
...state,
pluginsReturnedValue: pluginsReturnedValue, //update the state
tracker: t,
}
}
return state
}
case 'start': {
console.log('Starting tracker...')
state.tracker.start()
return state
}
}
}
export default function TrackerProvider({ children, config = {} }) {
let [state, dispatch] = useReducer(reducer, {
tracker: null,
pluginsReturnedValue: {},
config,
})
let value = {
startTracking: () => dispatch({ type: 'start' }),
initTracker: () => dispatch({ type: 'init' }),
pluginsReturnedValues: { ...state.pluginsReturnedValue }, //inject the state
}
return (
<TrackerContext.Provider value={value}>{children}</TrackerContext.Provider>
)
}
داخل الإجراء (action) المسمّى init، نقوم أيضاً بتتبّع القيم التي تُرجعها الدالة use عند استدعائها مع إضافاتنا. ونحتفظ بهذا القاموس (dictionary) داخل الخاصية state.pluginsReturnedValue. والتي نتيحها لجميع المكوّنات الفرعية (children) من خلال المتغيّر pluginsReturnedValues.
يتيح لك هذا المنطق استخدام الإضافة عند تهيئة المتتبّع (tracker) ثم الوصول إلى الوسيط (middleware) واستخدامه لاحقاً.
إنشاء store الخاص بـ Redux باستخدام الوسيط الجديد
Section titled إنشاء store الخاص بـ Redux باستخدام الوسيط الجديدالآن بعد أن أصبحت الإضافة تعمل، نحتاج إلى تهيئة store الخاص بـ Redux، ويجب أن نفعل ذلك بعد تهيئة المتتبّع (Tracker) وقبل استدعاء الدالة start.
ولهذا الغرض، اخترت المكوّن ManagedUI، الذي يُستخدم مباشرة في ملف _app.tsx. هذا المكوّن مُغلّف بواسطة Tracker Provider الخاص بنا، مما يعني أنه سيكون لديه إمكانية الوصول إلى السياق (context) الذي نشاركه.
يبدو المكوّن على النحو التالي:
export const ManagedUIContext: FC = ({ children }) => {
const { initTracker, pluginsReturnedValues } = useContext(TrackerContext)
const [store, setStore] = useState<Store>()
useEffect(() => {
initTracker()
}, [])
useEffect(() => {
if (!pluginsReturnedValues['redux']) return
let middleWares = pluginsReturnedValues['redux']
? [pluginsReturnedValues['redux']]
: []
setStore(createReduxStore(middleWares))
}, [pluginsReturnedValues])
return (
<div>
{store && (
<Provider store={store}>
<UIProvider>
<ThemeProvider>{children}</ThemeProvider>
</UIProvider>
</Provider>
)}
</div>
)
}
النقاط الرئيسية المستفادة من هذا الملف هي:
- نحصل على الدالة
initTrackerوالخاصيةpluginsReturnedValuesمن السياق (context). - نستدعي الأولى مرة واحدة فقط، عند تركيب (mounting) المكوّن (من خلال أول
useEffect). - ثم ننشئ store الخاص بـ Redux فقط بمجرد أن يحتوي المتغيّر
pluginsReturnedValuesعلى قيمتنا المُرجَعة. سيتم استدعاء الـuseEffectالثاني مرتين، مرة عند تحميل الصفحة ثم عندما تقوم الدالةinitTrackerبتعديل متغيّر الحالة لدينا. في المرة الثانية، سننشئ الـ store باستخدام الوسيط (middleware) المخزَّن فيpluginsReturnedValues.
تشغيل المتتبّع (tracker)
Section titled تشغيل المتتبّع (tracker)بعد إعداد الإضافة وإنشاء store الخاص بـ Redux بشكل صحيح، كل ما علينا فعله الآن هو استدعاء الدالة start الخاصة بالمتتبّع (tracker).
سيتم إضافة المنطق الخاص بذلك إلى ملف index.tsx، ويمكنك الاطّلاع على الكود المصدري الكامل لـ هذا الملف هنا.
الجزء ذو الصلة من هذا الكود الذي سنحتاج إلى الاطّلاع عليه هو التالي:
// imports and more logic goes here...
export default function Home({
products,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const { startTracking } = useContext(TrackerContext)
const dispatch = useDispatch()
const makeUpProductsList = useSelector((state: any) => state.makeUpProducts)
const { makeUpProducts } = makeUpProductsList
useEffect(() => {
async function getProds() {
await startTracking()
dispatch(getMakeUpProducts() as any)
}
getProds()
}, [dispatch])
return (
<>
<Grid variant="filled">
{products.slice(0, 3).map((product: any, i: number) => (
<ProductCard
key={product.id}
product={product}
imgProps={{
width: i === 0 ? 1080 : 540,
height: i === 0 ? 1080 : 540,
priority: true,
}}
/>
))}
</Grid>
<Marquee variant="secondary">
{makeUpProducts.slice(0, 3).map((product: any, i: number) => (
<ProductCard key={product.id} product={product} variant="slim" />
))}
</Marquee>
<!-- more code here -->
</>
)
}
سنستخدم فقط الدالة startTracking من مزوّد سياق المتتبّع (Tracker context provider) والـ hook المسمّى useSelector من Redux لالتقاط قائمة المنتجات المُرجَعة.
سيؤدي الـ hook المسمّى useEffect إلى تشغيل استدعاء startTracking وجلب قائمة منتجات المكياج الجديدة عن طريق إرسال (dispatch) استدعاء الدالة getMakeUpProducts.
لديك أسئلة؟
Section titled لديك أسئلة؟يمكنك الاطّلاع على هذا المستودع للحصول على الكود المصدري الكامل لتطبيق عملي قائم على Next.js مع store خاص بـ Redux.
إذا واجهت أي مشكلات في إعداد إضافة Redux، يُرجى التواصل معنا عبر مجتمع Slack الخاص بنا واطرح أسئلتك على مطوّرينا مباشرة!