استخدام إضافة Redux لالتقاط تغييرات الحالة

تعلّم كيفية التقاط تغييرات الحالة باستخدام إضافة Redux وعرضها بصرياً في عمليات إعادة تشغيل الجلسات

استخدام إضافة 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>
  )
}

النقاط الرئيسية المستفادة من هذا الملف هي:

  1. نحصل على الدالة initTracker والخاصية pluginsReturnedValues من السياق (context).
  2. نستدعي الأولى مرة واحدة فقط، عند تركيب (mounting) المكوّن (من خلال أول useEffect).
  3. ثم ننشئ 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.

يمكنك الاطّلاع على هذا المستودع للحصول على الكود المصدري الكامل لتطبيق عملي قائم على Next.js مع store خاص بـ Redux.

إذا واجهت أي مشكلات في إعداد إضافة Redux، يُرجى التواصل معنا عبر مجتمع Slack الخاص بنا واطرح أسئلتك على مطوّرينا مباشرة!