Как гидратировать параметры на стороне сервера с помощью React + Redux

У меня есть универсальное приложение 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(), это перестанет работать.

Что мне здесь не хватает?


person Community    schedule 18.07.2016    source источник


Ответы (2)


Недавно я написал статью об этом требовании, но оно требует использования редукс-саг. Он работает с точки зрения избыточных преобразователей и использует этот статический шаблон fetchData/need.

https://medium.com/@navgarcha7891/react-server-side-rendering-with-simple-redux-store-hydration-9f77ab66900a

Я думаю, что этот подход к саге намного чище и проще для рассуждений, но это может быть только мое мнение :)

person nav    schedule 01.08.2016

Кажется, нет более идиоматического способа сделать это, чем подход fetchData, который я включил в свой первоначальный вопрос. Хотя мне это все еще кажется неэлегантным, у него меньше проблем, чем я изначально думал:

  • .WrappedComponent — стабильный интерфейс, но референс в любом случае не нужен. Функция Redux connect автоматически поднимает любые статические методы из исходного класса в свою оболочку.
  • Любой другой компонент более высокого порядка, который обертывает контейнер с привязкой к Redux, также должен поднимать (или передавать) любые статические методы.

Могут быть другие соображения, которые я не вижу, но я остановился на вспомогательном методе, подобном этому, в моем файле server.js:

function prefetchComponentData (renderProps, store) {
  let { params, components, location } = renderProps
  components.forEach(componentClass => {
    if (componentClass && typeof componentClass.prefetchData === 'function') {
      componentClass.prefetchData({ store, params, location })
    }
  })
}
person Community    schedule 22.07.2016