#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 для этой цели.