Использование плагина Redux для захвата изменений состояния

Узнайте, как захватывать изменения состояния с помощью плагина Redux и визуализировать их в повторах сессий

Использование плагина Redux для захвата изменений состояния

Video Tutorial

Посмотрите, как отслеживать состояние с помощью Redux

Если вам не нравится читать, вы можете посмотреть это видеоруководство, которое показывает, как отслеживать состояние ваших React-приложений при использовании Redux

Если вам нужна дополнительная наглядность при воспроизведении сессий ваших пользователей, возможность заглянуть в состояние приложения может оказаться очень полезной.

В случае с Redux OpenReplay предоставляет плагин, который позволяет вам интегрироваться во внутреннюю работу store. Этот плагин даст вам возможность видеть состояние (state) store Redux и действия (actions), отправленные на протяжении записанной сессии.

После настройки вы сможете наблюдать изменения в store, как показано на следующем скриншоте:

Ожидаемый результат

Настройка Redux в проекте Next.js

Section titled Настройка Redux в проекте Next.js

Для этого руководства мы будем использовать этот репозиторий (ветка redux-store) обобщённого сайта электронной коммерции, созданного с помощью Next.js.

В этом проекте мы заменим набор рекомендуемых товаров новым набором, полученным из внешнего API.

Для этого мы добавим функцию для запроса товаров с помощью Axios, и сделаем это внутри действия (action) Redux.

Примечание: это сложное приложение на Next.js, поэтому оно может не следовать стандартной структуре, характерной для классических To-Do приложений, но, следуя этому руководству, вы должны без труда уследить за изменениями.

Помните: вы всегда можете клонировать репозиторий и просмотреть код самостоятельно.

Начнём с установки всех основных зависимостей с помощью:

npm i next-redux-wrapper redux react-redux redux-thunk redux-devtools-extension

После этого создайте папку под названием store в корневом каталоге вашего проекта и воспроизведите следующую структуру:

Структура папок

Файл types.js будет содержать определение типов для двух действий, которые мы собираемся определить:

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
  }
}

И, наконец, файл действия определит одну-единственную функцию, которая отвечает за получение списка товаров из внешнего API и отправку правильного действия и 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,
    })
  }
}

Настройка провайдера трекера

Section titled Настройка провайдера трекера

Чтобы внедрить трекер в приложение, мы будем использовать контекст, предоставленный так, как показано в руководстве по Next.js.

Этот провайдер позволит вам настроить набор плагинов; в нашем случае мы будем использовать плагин 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>
  )
}

Теперь, этот код позволяет вам настроить трекер с нужным плагином, но чтобы плагин работал, нам потребуется доступ к 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>
  )
}

Внутри действия init мы также отслеживаем значения, возвращаемые методом use, когда он вызывается с нашими плагинами. И мы храним этот словарь внутри свойства state.pluginsReturnedValue. Которое мы делаем доступным для всех дочерних элементов через переменную pluginsReturnedValues.

Эта логика позволяет вам использовать плагин при инициализации трекера, а затем получить доступ к middleware и использовать его позже.

Создание store Redux с новым middleware

Section titled Создание store Redux с новым middleware

Теперь, когда плагин работает, нам нужно инициализировать store Redux, и сделать это нужно после того, как Tracker был инициализирован, и до того, как будет вызван метод start.

Для этого я выбрал компонент ManagedUI, который используется непосредственно в файле _app.tsx. Этот компонент обёрнут нашим Tracker Provider, что означает, что он будет иметь доступ к контексту, которым мы делимся.

Компонент выглядит так:

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 из контекста.
  2. Мы вызываем первую только один раз, когда компонент монтируется (через первый useEffect).
  3. Затем мы создаём store Redux только после того, как переменная pluginsReturnedValues получит наше возвращаемое значение. Второй useEffect будет вызван дважды: один раз при загрузке страницы и затем когда метод initTracker изменит нашу переменную состояния. Во второй раз мы создадим store с middleware, хранящимся в pluginsReturnedValues.

Когда плагин настроен, а store Redux правильно создан, всё, что нам осталось сделать сейчас, — это вызвать метод start трекера.

Логика для этого будет добавлена в файл 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 и хук useSelector из Redux для захвата списка возвращённых товаров.

Хук useEffect инициирует вызов startTracking и получение нового списка косметических товаров путём отправки вызова функции getMakeUpProducts.

И на этом вы должны быть в состоянии развернуть ваше приложение (при условии, что вы настроили ключ API, как упоминается в этом руководстве)

Вы можете ознакомиться с этим репозиторием, чтобы увидеть полный исходный код работающего приложения на основе Next.js со store Redux.

Если у вас возникнут какие-либо проблемы с настройкой плагина Redux, пожалуйста, свяжитесь с нами в нашем сообществе Slack и задайте вопросы нашим разработчикам напрямую!