Реагируйте на маршрутизатор SSR — потеря состояния redux при переходе на дополнительный маршрут

#reactjs #redux #react-redux #react-router #server-side-rendering

Вопрос:

Я визуализирую реакцию на сервере, все работает нормально, за исключением того, что при переходе на дополнительные маршруты состояние потерь при повторном обновлении и данные из бэкенда не отображаются. Данные отображаются в хранилище в инструментах разработки redux. Однако состояние не теряется при обновлении страницы.

Вот пример, показывающий, что я имею в виду:

 website.com/category/books to website.com/category/toys
 

App.js:

 import React from 'react'
import { Switch, Route } from 'react-router-dom'
import HomeScreen from './Screens/HomeScreen'
import CategoryScreen from './Screens/CategoryScreen'

const App = () => {
  return (
    <Switch>
      <Route path='/' component={HomeScreen} exact />
      <Route path='/category/:name' component={CategoryScreen} />
    </Switch>

  )
}

export default App;
 

index.js:

 import React from 'react'
import { Provider } from 'react-redux'
import { hydrate } from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import store from './store'
import './index.css'
import App from './App'

hydrate(
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>,
  document.getElementById('root')
)
 

store.js:

 import { createStore, combineReducers, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import { productListReducer } from './reducers/productReducers'
import { categoryListReducer } from './reducers/categoryReducers'

const reducer = combineReducers({
    productList: productListReducer,
    categoryList: categoryListReducer,
})

const initialState = {}

const middleware = [thunk]

const loadState = () => {
    try {
        const serializedState = localStorage.getItem('state')
        if (serializedState === null) {
            return undefined
        }
        return JSON.parse(serializedState)
    } catch (e) {
        return undefined
    }
}

const saveState = (state) => {
    try {
        const serializedState = JSON.stringify(state)
        localStorage.setItem('state', serializedState)
    } catch (e) {

    }
}

const persistedState = loadState()

const store = createStore(
    reducer, persistedState, composeWithDevTools(applyMiddleware(...middleware))
)

store.subscribe(() => {
    saveState(store.getState())
})

export default store
 

frontend server.js for SSR:

 import path from 'path'
import fs from 'fs'
import express from 'express'
import React from 'react'
import { StaticRouter } from 'react-router'
import ReactDOMServer from 'react-dom/server'
import { Provider } from 'react-redux'
import store from '../src/store'
import App from '../src/App'
import { createProxyMiddleware } from 'http-proxy-middleware'

const PORT = 3000
const app = express()

app.use('/api/products', createProxyMiddleware({ target: 'http://98.51.100.255:5000', changeOrigin: true }))
app.use('/api/categories', createProxyMiddleware({ target: 'http://98.51.100.255:5000', changeOrigin: true }))

const router = express.Router()

const serverRenderer = (req, res, next) => {

  app.get('/*', function (req, res) {
    res.sendFile(path.join(__dirname, '../build/index.html'), function (err) {
      if (err) {
        res.status(500).send(err)
      }
    })
  })

  const context = {}

  fs.readFile(path.resolve('./build/index.html'), 'utf8', (err, data) => {
    if (err) {
      console.error(err)
      return res.status(500).send('An error occurred')
    }
    return res.send(
      data.replace(
        '<div id="root"></div>',
        `<div id="root">
        ${ReactDOMServer.renderToString(
          <Provider store={store}>
            <StaticRouter location={req.url} context={context}>
              <App />
            </StaticRouter>
          </Provider>
        )}
        </div>`
      )
    )
  })
}

router.use('^/

categoryscreen.js:



 import React, { useEffect, useState, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import CategoryHeader from '../components/CategoryHeader'
import Product from '../components/Product'
import { listProducts } from '../actions/productActions'

function CategoryScreen({ match }) {
    let ProductMatch = match.params.name

    const dispatch = useDispatch()

    const productList = useSelector(state => state.productList)
    const { products } = productList

    useEffect(() => {
        dispatch(listProducts())
    }, [dispatch])

    return (
        <>
            <h3>{ProductMatch}</h3>
            <div>
                {products.filter(product => product.SubCategory == ProductMatch)
                    .map((product) => (
                        <Product product={product} />
                    ))
                }
            </div>

        </>
    )
}

export default CategoryScreen 

Действие списка продуктов:

 import axios from 'axios'
import {
    PRODUCT_LIST_REQUEST,
    PRODUCT_LIST_SUCCESS,
    PRODUCT_LIST_FAIL,
} from '../constants/productConstants'


export const listProducts = () => async (dispatch) => {
    try {
        dispatch({ type: PRODUCT_LIST_REQUEST })

        const { data } = await axios.get('/api/products')

        dispatch({
            type: PRODUCT_LIST_SUCCESS,
            payload: data
        })
    } catch (error) {
        dispatch({
            type: PRODUCT_LIST_FAIL,
            payload: error.response amp;amp; error.response.data.message
                ? error.response.data.message : error.message
        })
    }
} 

изменить: добавлено categoryscreen.js amp; повторное действие

Комментарии:

1. Можете ли вы поделиться CategoryScreen , так как этот компонент, похоже, не обрабатывает новый URL-адрес?

2. @DrewReese добавил экран категории

3. Спасибо. Можно ли с уверенностью предположить, что вы правильно видите name обновление параметров ( ProductMatch ) от "книг" до "игрушек" с изменением маршрута? Можете ли вы уточнить, о каком потерянном состоянии вы говорите? Это productList ?

4. @DrewReese да, productmatch обновляется с изменением маршрута. и данные отправляются из действия списка продуктов. и я добавил действие списка продуктов в сообщение

Ответ №1:

Так что на данный момент немного неясно, в чем проблема. URL-адрес изменяется, компонент видит новое ProductMatch значение, он перенаправляется, он должен отфильтровать productList состояние по ProductMatch категории и переназначить продукты.

Единственное, чего мне здесь не хватает, - это предоставление ключа реакции для сопоставления. В отсутствие ключа React в основном будет использовать индекс ребенка в качестве ключа React, и, поскольку индекс n подкатегории "книги" будет таким же, как индекс n подкатегории "игрушки", я подозреваю, что React просто отказывается от повторной настройки вашего пользовательского интерфейса.

Предоставьте ключ реакции на основе уникального свойства каждого product отображаемого, например id . Если продукты не имеют этой гарантии, я предлагаю дополнить данные о вашем продукте при его обработке в состояние redux, чтобы добавить идентификатор GUID для этой цели.

 {products.filter(product => product.SubCategory == ProductMatch)
  .map((product) => (
    <Product
      key={product.id} // <-- add React key
      product={product}
    />
  ))
}
 

, serverRenderer)

router.use(
express.static(path.resolve(__dirname, '..', 'build'))
)

app.use(router)

app.listen(PORT, () => {
console.log(`SSR running on port ${PORT}`)
})
categoryscreen.js:


Действие списка продуктов:


изменить: добавлено categoryscreen.js amp; повторное действие

Комментарии:

1. Можете ли вы поделиться CategoryScreen , так как этот компонент, похоже, не обрабатывает новый URL-адрес?

2. @DrewReese добавил экран категории

3. Спасибо. Можно ли с уверенностью предположить, что вы правильно видите name обновление параметров ( ProductMatch ) от «книг» до «игрушек» с изменением маршрута? Можете ли вы уточнить, о каком потерянном состоянии вы говорите? Это productList ?

4. @DrewReese да, productmatch обновляется с изменением маршрута. и данные отправляются из действия списка продуктов. и я добавил действие списка продуктов в сообщение

Ответ №1:

Так что на данный момент немного неясно, в чем проблема. URL-адрес изменяется, компонент видит новое ProductMatch значение, он перенаправляется, он должен отфильтровать productList состояние по ProductMatch категории и переназначить продукты.

Единственное, чего мне здесь не хватает, — это предоставление ключа реакции для сопоставления. В отсутствие ключа React в основном будет использовать индекс ребенка в качестве ключа React, и, поскольку индекс n подкатегории «книги» будет таким же, как индекс n подкатегории «игрушки», я подозреваю, что React просто отказывается от повторной настройки вашего пользовательского интерфейса.

Предоставьте ключ реакции на основе уникального свойства каждого product отображаемого, например id . Если продукты не имеют этой гарантии, я предлагаю дополнить данные о вашем продукте при его обработке в состояние redux, чтобы добавить идентификатор GUID для этой цели.