У меня есть универсальное приложение React, использующее Redux и React Router. Некоторые из моих маршрутов включают параметры, которые на клиенте инициируют запрос AJAX для гидратации данных для отображения. На сервере эти запросы могут выполняться синхронно и отображаться при первом запросе.
Проблема, с которой я сталкиваюсь, заключается в следующем: к тому времени, когда какой-либо метод жизненного цикла (например, componentWillMount
) вызывается для маршрутизируемого компонента, уже слишком поздно отправлять действие Redux, которое будет отражено в первом рендеринге.
Вот упрощенный вид моего кода рендеринга на стороне сервера:
routes.js
export default getRoutes (store) {
return (
<Route path='/' component={App}>
<Route path='foo' component={FooLayout}>
<Route path='view/:id' component={FooViewContainer} />
</Route>
</Route>
)
}
сервер.js
let store = configureStore()
let routes = getRoutes()
let history = createMemoryHistory(req.path)
let location = req.originalUrl
match({ history, routes, location }, (err, redirectLocation, renderProps) => {
if (redirectLocation) {
// redirect
} else if (err) {
// 500
} else if (!renderProps) {
// 404
} else {
let bodyMarkup = ReactDOMServer.renderToString(
<Provider store={store}>
<RouterContext {...renderProps} />
</Provider>)
res.status(200).send('<!DOCTYPE html>' +
ReactDOMServer.renderToStaticMarkup(<Html body={bodyMarkup} />))
}
})
Когда компонент FooViewContainer
будет построен на сервере, его пропсы для первого рендера уже будут исправлены. Любое действие, которое я отправляю в магазин, не будет отражено в первом вызове render()
, что означает, что оно не будет отражено в том, что доставлено в запросе страницы.
Параметр id
, который передает React Router, сам по себе бесполезен для первого рендеринга. Мне нужно синхронно преобразовать это значение в правильный объект. Куда я должен положить эту гидратацию?
Одним из решений было бы поместить его внутри метода render()
для случаев, когда он вызывается на сервере. Это кажется мне явно неверным, потому что 1) это семантически не имеет смысла и 2) любые данные, которые он собирает, не будут должным образом отправлены в хранилище.
Другое решение, которое я видел, — добавить статический метод fetchData
к каждому из компонентов контейнера в цепочке Router. например что-то вроде этого:
FooViewContainer.js
class FooViewContainer extends React.Component {
static fetchData (query, params, store, history) {
store.dispatch(hydrateFoo(loadFooByIdSync(params.id)))
}
...
}
сервер.js
let { query, params } = renderProps
renderProps.components.forEach(comp =>
if (comp.WrappedComponent && comp.WrappedComponent.fetchData) {
comp.WrappedComponent.fetchData(query, params, store, history)
}
})
Я чувствую, что должен быть лучший подход, чем этот. Он не только кажется довольно неэлегантным (является ли .WrappedComponent
надежным интерфейсом?), но и не работает с компонентами более высокого порядка. Если какой-либо из классов маршрутизируемых компонентов обернут чем-то другим, кроме connect()
, это перестанет работать.
Что мне здесь не хватает?