Next.js + Redux server side rendering: Has data, but doesn't render on server side

4.2k views Asked by At

I'm trying to add redux integration to my Next.js app, but I can't get serverside rendering working the way it should. I based my implementation off the official nextjs redux example.

In the end, when the page comes back from the server, the data is present as JSON data in the output, but the actual rendering based on this data did not happen. The weird thing is that before I used redux, the content DID render the way it should.

Naturally, I'm also getting React's checksum warning, indicating that the markup on the server is different.

I have no idea how to make this work properly on the server side. Is there something that I'm missing?

Here's the HTML generated by Next.js:

<h1 data-reactid="3">Test page</h1>

</div></div></div><div id="__next-error"></div></div><div><script>
          __NEXT_DATA__ = {"props":{"isServer":true,"store":{},

"initialState":{"authors":{"loading":false,"items":{"4nRpnr66B2CcQ4wsY04CIQ":… }

,"initialProps":{}},"pathname":"/test","query":{},"buildId":1504364251326,"buildStats":null,"assetPrefix":"","nextExport":false,"err":null,"chunks":[]}
          module={}
          __NEXT_LOADED_PAGES__ = []
          __NEXT_LOADED_CHUNKS__ = []

          __NEXT_REGISTER_PAGE = function (route, fn) {
            __NEXT_LOADED_PAGES__.push({ route: route, fn: fn })
          }

          __NEXT_REGISTER_CHUNK = function (chunkName, fn) {
            __NEXT_LOADED_CHUNKS__.push({ chunkName: chunkName, fn: fn })
          }
        </script><script async="" id="__NEXT_PAGE__/test" type="text/javascript" src="/_next/1504364251326/page/test"></script><script async="" id="__NEXT_PAGE__/_error" type="text/javascript" src="/_next/1504364251326/page/_error/index.js"></script><div></div><script type="text/javascript" src="/_next/1504364251326/manifest.js"></script><script type="text/javascript" src="/_next/1504364251326/commons.js"></script><script type="text/javascript" src="/_next/1504364251326/main.js"></script></div></body></html>

AS you can see, the initialState value is populated, it contains all the required data, but the DOM still shows empty!.

If I render the dom on the client side, the js picks up the initial content and rerenders the page with the loaded content in place.

Here's my test page JS file:

import React from 'react'
import map from 'lodash.map';
import { initStore } from '../lib/store';
import * as actions from '../lib/actions';
import withRedux from 'next-redux-wrapper';

class IndexPage extends React.PureComponent {
  static getInitialProps = ({ store, req }) => Promise.all([
    store.dispatch(actions.fetchAll)
  ]).then( () => ({}) )

  render() {
    const latestPlants = this.props.plants.latest || [];

    return (
      <div>
        <h1>Test page</h1>
        { map(this.props.plants.items, p => (
          <div>{p.fields.name}</div>
        ))}
      </div>
    )
  }
}

export default withRedux(initStore, data => data, null)(IndexPage)

For whatever it's worth, here's the action that I call above:

export const fetchAll = dispatch => {
  dispatch({
    type: LOADING_ALL
  })

  return axios.get('/api/frontpage')
    .then( response => {
      const data = response.data
      dispatch({
        type: RESET_AUTHORS,
        payload: data.authors
      })
      dispatch({
        type: RESET_PLANTS,
        payload: data.plants
      })
      dispatch({
        type: RESET_POSTS,
        payload: data.posts
      })
    });
}

Any help with this would be greatly appreciated, I'm at a loss on how to make this work as expected. Anyone have any leads? Please also comment if there's something I can clarify.

1

There are 1 answers

0
alejandro estrada On

I recommend to split the code in different parts. First, I'll create a store, with something like this:

import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import reducer from './reducers'

export const initStore = (initialState = {}) => {
  return createStore(reducer, initialState, applyMiddleware(thunkMiddleware))
}

Then I'll create the store with the types to handle:

const initialState = {
  authors: null,
  plants: null,
  posts: null
}

export default (state = initialState, action) => {
  switch (action.type) {
    case 'RESET':
      return Object.assign({}, state, { 
        authors: action.authors,
        plants: action.plants,
        posts: action.posts
      })
    default:
      return state
  }
}

In the actions I'll have something like this:

export const fetchAll = dispatch => {
  return axios.get('/api/frontpage')
    .then( response => {
      const data = response.data
      dispatch({
        type: 'RESET',
        authors: data.authors,
        plants: data.plants,
        posts: data.posts
      })
    });
}

The index will be something like this:

import React from 'react'
import { initStore } from '../store'
import withRedux from 'next-redux-wrapper'
import Main from '../components'

class Example extends React.Component {
  render() {
    return (
      <div>
        <Main />
      </div>
    )
  }
}

export default withRedux(initStore, null)(Example)

And the component Main:

import React, {Component} from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { fetchAll } from '../../actions'

class Data extends Component {
  componentWillMount() {
    this.props.fetchAll()
  }

  render() {
    const { state } = this.props
    return (
      <div>
        <h1>Test page</h1>
        { map(state.plants.items, p => (
          <div>{p.fields.name}</div>
        ))}
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    state
  }
}

const mapDistpatchToProps = dispatch => {
  return {
    fetchAll: bindActionCreators(fetchAll, dispatch)
  }
}

export default connect(mapStateToProps, mapDistpatchToProps)(Data)

Make the changes for what you need.

You can check some full examples here:
Form handler
Server Auth