Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using redux-api with Next.js next-redux-wrapper for server-side rendering? #183

Open
tomsoderlund opened this issue Oct 25, 2017 · 14 comments
Labels

Comments

@tomsoderlund
Copy link

Anyone tried using redux-api with Next.js for server-side rendering?

I’d like to get tips on how to do the initial redux-api sync() call inside getInitialProps (Next.js specific method) instead of componentDidMount (as in the example), to get the data rendered server-side along with the first page request.

@lexich
Copy link
Owner

lexich commented Oct 25, 2017

For serverside rendering you should push initialState on backend
https://github.com/lexich/redux-api/blob/master/examples/isomorphic/app/server.jsx#L60
to frontend https://github.com/lexich/redux-api/blob/master/examples/isomorphic/app/client.jsx#L26-L28

And what problem with componentDidMount?
The best place to call sync method and other ajax calls is router.
https://github.com/lexich/redux-api/blob/master/examples/isomorphic/app/routes/routes.js#L23

And what problem with next.js integration?

@tomsoderlund
Copy link
Author

tomsoderlund commented Oct 25, 2017

Next.js works a bit differently. You don’t separate between server and client code – it runs on both. It has a special lifecycle method getInitialProps where you can set this.props e.g. based on an initial database query, which can be rendered server-side:

https://github.com/tomsoderlund/nextjs-express-mongoose-crudify-boilerplate/blob/no-redux/pages/index.js#L6

this.state is always client-side only.

@lexich
Copy link
Owner

lexich commented Oct 26, 2017

I have never used next.js but according examples https://github.com/zeit/next.js/blob/master/examples/with-redux/pages/index.js
https://github.com/tomsoderlund/nextjs-express-mongoose-crudify-boilerplate/blob/no-redux/pages/index.js
I think, you can write code like this

static async getInitialProps ({req, res, query}) {
   const { data } = await store.dispatch(rest.actions.entity.sync());
   return data;
}

@tomsoderlund
Copy link
Author

tomsoderlund commented Dec 21, 2017

I don’t even get getInitialProps to run when components are wrapped like in the redux-api examples.

There is a next-redux-wrapper NPM that adds a withRedux wrapper. Here’s from their example:

import { bindActionCreators } from 'redux'
import { initStore, startClock, addCount, serverRenderClock } from '../store'
import withRedux from 'next-redux-wrapper'

class Counter extends React.Component {
	static getInitialProps ({ store, isServer }) {
		store.dispatch(serverRenderClock(isServer))
		store.dispatch(addCount())

		return { isServer }
	}

	//...
}

const mapDispatchToProps = (dispatch) => {
	return {
		addCount: bindActionCreators(addCount, dispatch),
		startClock: bindActionCreators(startClock, dispatch)
	}
}

export default withRedux(initStore, null, mapDispatchToProps)(Counter)

Where initStore is defined as:

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

With my limited understanding of all this, I reckon I somehow need to expose reducer and the various actions (as in bindActionCreators(addCount, dispatch)) from my redux-api implementation, to accomplish something similar with withRedux.

Any tips?

Update: I guess I have reducer from const reducer = combineReducers(myReduxApi.reducers);. But I also need a createStore function to pass to withRedux.

@tomsoderlund tomsoderlund changed the title Using redux-api with Next.js for server-side rendering? Using redux-api with Next.js next-redux-wrapper for server-side rendering? Dec 21, 2017
@tomsoderlund
Copy link
Author

Progress update:

class ShowLessonPage extends React.Component {

	static async getInitialProps ({store, isServer, pathname, query}) {
		console.log(`getInitialProps`, {store, isServer, pathname, query});
		const { dispatch } = store;
		const lessonSlug = query.lessonSlug;
		// Get one Lesson
		dispatch(reduxApi.actions.oneLesson({ id: `slug=${lessonSlug}` }));
	}

	//.....

}

const createStoreWithThunkMiddleware = applyMiddleware(thunk)(createStore);
const reducer = combineReducers(reduxApi.reducers);

const makeStore = function (state, enhancer) {
	return createStoreWithThunkMiddleware(reducer, state);
}

const mapStateToProps = function (state) {
	return { oneLesson: state.oneLesson };
};

const ShowLessonPageConnected = withRedux({ createStore: makeStore, mapStateToProps: mapStateToProps })(ShowLessonPage)
export default ShowLessonPageConnected;

I at least get store into getInitialProps now, but I get a strange Error: only absolute urls are supported message that I didn’t have in my pre-withRedux version of the app. And this.props.oneLesson.data is of course empty.

makeStore is getting a state: undefined on the server generated calls, maybe that’s a clue.

@tomsoderlund
Copy link
Author

@lexich
Copy link
Owner

lexich commented Dec 21, 2017

@tomsoderlund
Copy link
Author

@lexich I solved the absolute URL issue, Although your solution is prettier.

Now, for 1 page reload it calls makeStore no less than 3 times, and only the first call contains the correct slug, see console output: https://stackoverflow.com/questions/47924692/how-to-get-next-js-and-redux-api-to-work-with-next-redux-wrapper

@tomsoderlund
Copy link
Author

A breakthrough: returning a promise from getInitialProps makes SSR work. Now client-side rendering is acting up, funny enough.

static async getInitialProps ({store, isServer, pathname, query}) {
	const { dispatch } = store;
	const lessonSlug = query.lessonSlug;
	const resultPromise = await dispatch(reduxApi.actions.oneLesson({ id: `slug=${lessonSlug}` }));
	return resultPromise;
}

@tomsoderlund
Copy link
Author

tomsoderlund commented Dec 21, 2017

OK, fixed client-side rendering too. Funny thing:

  • For the pages that use the simple endpoint system (reduxApi.actions.oneLesson({ id: lessonId })), the data becomes available in this.state.
  • For the pages that use the complex endpoint system (reduxApi.actions.lessons.sync()), the data becomes available in this.props.

I’m assuming it’s something in my code, but the redux(-api) code is almost identical between the pages.

@tomsoderlund
Copy link
Author

tomsoderlund commented Dec 21, 2017

Here’s a complete working example of Next.js, redux-api, and next-redux-wrapper working in harmony:

https://github.com/tomsoderlund/nextjs-express-mongoose-crudify-boilerplate

@thmohd
Copy link

thmohd commented Jul 14, 2018

You have access to the router directly

`function MapStateToProps(state, router) {

console.log(router)

}`

Assuming that you are using withRouter
export default withRouter(connect(MapStateToProps)(Whatever))

@te-online
Copy link

te-online commented Feb 13, 2019

@tomsoderlund It seems like you didn't need this, but maybe you still have an answer: Is there a way to pass the req request-object within getInitialProps to redux-api so that it'd be available in the adapter? I'd like to fetch entries directly from the database on the server-side by using a custom adapter, but my database reference sits on the req object...

@te-online
Copy link

Just to follow up my own question: I ended up creating a (kind of?) singleton class for ReduxApi that has a setter for handing in the database connection instance. This database class variable can then be used by the custom adapter, deciding whether to fetch from the API or database and using the connection instance to query the database.

Like so

// ReduxApiClient.ts

class ReduxApiClientLib {
  private rest;
  private req;

  constructor() {
    this.rest = reduxApi({
     ...
    }).use('fetch', this.adapter);
  }

  public setServerRequest = (req) => {
    this.req = req;
  };

  public getRest = () => {
    return this.rest;
  };

  private adapter = async (url, options) => {
    // Server-side behaviour
    if (this.req) {
      let data: any = [];
      switch (url) {
        case '/api/test':
          ...
          // Query database
          data = await req.database().get('test');
          break;
          ...
      }
      // Return
      return Promise.resolve(data);
    }

    // Default client-side behaviour
    try {
      const response = await fetch(
        fetchUrl,
        { ...options.fetchOptions, credentials: 'include' } || { credentials: 'include' },
      );
      if (response.ok) {
        const result = await response.json();
        return result.data;
      }
    } catch (e) {
      return [];
    }
  };

  ...
}

export const ReduxApiClient = new ReduxApiClientLib();
// _app.tsx
...
static async getInitialProps({ ctx }) {
    ...
    if (ctx.req) {
      // Get data from database
      ReduxApiClient.setServerRequest(ctx.req);
      await ctx.store.dispatch(ReduxApiClient.getRest().actions.test.sync());
      // Get current state
      const state: ReduxState = ctx.store.getState();
      // Compose props
      pageProps.test = state.apiData.test.data;
    }
    ...
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants