Использование плагина Redux для захвата изменений состояния
Video Tutorial
Посмотрите, как отслеживать состояние с помощью Redux
Если вы не любите читать, можете посмотреть это видеоруководство, которое показывает, как отслеживать состояние ваших React-приложений при использовании Redux
Если при воспроизведении сессий ваших пользователей вам нужна дополнительная наглядность, возможность заглянуть в состояние приложения может оказаться очень полезной.
В случае с Redux OpenReplay предоставляет плагин, который позволяет интегрироваться во внутреннюю работу хранилища (store). Этот плагин позволит вам видеть состояние хранилища Redux и действия (actions), отправленные на протяжении записанной сессии.
После настройки вы сможете наблюдать за изменениями в хранилище, как показано на следующем скриншоте:

Настройка 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 будет экспортировать функцию, которая, КОГДА она вызывается, создаёт новое хранилище 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
}
Наш файл редьюсера (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 Настройка провайдера трекераЭтот провайдер позволит вам настроить набор плагинов; в нашем случае мы будем использовать плагин 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) и использовать его позже.
Создание хранилища Redux с новым промежуточным ПО
Section titled Создание хранилища Redux с новым промежуточным ПОТеперь, когда плагин работает, нам нужно инициализировать хранилище Redux, и мы должны сделать это после того, как был инициализирован Трекер, и до того, как будет вызван метод 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>
)
}
Ключевые выводы из этого файла следующие:
- Мы получаем функцию
initTrackerи атрибутpluginsReturnedValuesиз контекста. - Мы вызываем первую только один раз, когда компонент монтируется (через первый
useEffect). - Затем мы создаём хранилище Redux только после того, как переменная
pluginsReturnedValuesполучит наше возвращаемое значение. ВторойuseEffectбудет вызван дважды: один раз при загрузке страницы, а затем когда методinitTrackerизменит нашу переменную состояния. Во второй раз мы создадим хранилище с промежуточным ПО, хранящимся вpluginsReturnedValues.
Запуск трекера
Section titled Запуск трекераС настроенным плагином и правильно созданным хранилищем 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 из провайдера контекста Трекера и хук useSelector из Redux для захвата списка возвращённых товаров.
Хук useEffect инициирует вызов startTracking и получение нового списка косметических товаров путём отправки вызова функции getMakeUpProducts.
Есть вопросы?
Section titled Есть вопросы?Вы можете ознакомиться с этим репозиторием, чтобы получить полный исходный код рабочего приложения на основе Next.js с хранилищем Redux.
Если у вас возникнут какие-либо проблемы с настройкой плагина Redux, свяжитесь с нами в нашем сообществе Slack и задайте вопросы нашим разработчикам напрямую!