nytimes / redux-taxi

🚕 Component-driven asynchronous SSR in isomorphic Redux apps
Other
71 stars 8 forks source link

Example of implementation #4

Open dougsmith1000 opened 7 years ago

dougsmith1000 commented 7 years ago

Hi Jeremy, could you post a version of this with a working example? The project I'm building is using React Router v4, which doesn't seem have syncHistory as a working function. I think I almost have this hooked up, but am getting caught up on the middleware when creating my store. I've posted an example below that forces the use of your taxi middleware regardless of environment.

I'm also getting an empty value when I instantiate ReduxTaxi(). Because I handle a universal js implementation a little differently, I'm trying to pass the reduxTaxi object around to different files without completely understanding what I am passing. These errors are in both my development and production environments. Please take a look if you get a chance. Thank you!

Here's the code - used from several boilerplates that I've modified to do what I want:

ssr.js:

// Node Modules
import fs from 'fs';
import {basename, join} from 'path';

// Libraries
import React from 'react';
import {renderToString} from 'react-dom/server';

// Redux
import createStore from 'universal/redux/createStore.js';
import createHistory from 'history/createMemoryHistory'

import { ReduxTaxi } from 'redux-taxi';

// Components
import Html from './Html.js';

function renderApp(url, res, store, assets, reduxTaxi) {
  const context = {};

  const html = renderToString(
    <Html
      title='Test Site'
      store={store}
      url={url}
      context={context}
      assets={assets}
      reduxTaxi={reduxTaxi} />
  );

  res.send('<!DOCTYPE html>'+html);
}

export const renderPage = function (req, res) {
  const reduxTaxi = ReduxTaxi();
  const history = createHistory( );
  const store  = createStore(history, reduxTaxi);

  const assets = require('../../build/assets.json');

  assets.manifest.text = fs.readFileSync(
    join(__dirname, '..', '..', 'build', basename(assets.manifest.js)),
    'utf-8'
  );

  renderApp(req.url, res, store, assets, {reduxTaxi});
};

export const renderDevPage = function (req, res) {
  const reduxTaxi = ReduxTaxi();
  const history =  createHistory( );
  const store   = createStore(history, reduxTaxi);
  renderApp(req.url, res, store, {reduxTaxi});
};

export default renderPage;

Html.js:

// Libraries
import React, { Component } from 'react';
import {StaticRouter} from 'react-router';
import {renderToString} from 'react-dom/server';
import PropTypes from 'prop-types';

// Redux
import { Provider } from 'react-redux';
import {ReduxTaxi, ReduxTaxiProvider} from 'redux-taxi';

import createStore from '../universal/redux/createStore'; // Your store configurator

import promise from 'es6-promise';
promise.polyfill();

import { I18nextProvider } from 'react-i18next';
import i18n from '../i18n.server';

class Html extends Component {
  static propTypes = {
    url: PropTypes.string.isRequired,
    store: PropTypes.object.isRequired,
    title: PropTypes.string.isRequired,
    assets: PropTypes.object
  }

  render () {
    const PROD = process.env.NODE_ENV === 'production';

    const {
      title,
      store,
      assets,
      url,
      context,
      reduxTaxi
    } = this.props;

    const {
      manifest,
      app,
      vendor
    } = assets || {};

    let state = store.getState();
    const initialState = `window.__INITIAL_STATE__ = ${JSON.stringify(state)}`;
    const Layout =  PROD ? require( '../../build/prerender.js') : () => {};

  const initialComponent = (
      <I18nextProvider i18n={ i18n }>
        <Provider store={store}>
          <ReduxTaxiProvider reduxTaxi={reduxTaxi}>
            <StaticRouter location={url} context={context}>
              <Layout />
            </StaticRouter>
          </ReduxTaxiProvider>
        </Provider>
      </I18nextProvider>
    );

    const root = PROD && renderToString(initialComponent);

//Does this have to be a different instantiation from what is passed in the props?
    const allPromises = reduxTaxi.getAllPromises();

    if (allPromises.length) {
        // If we have some promises, we need to delay server rendering
        promise
            .all(allPromises)
            .then(() => {
              return (
               <html>
                 <head>
                   <meta charSet="utf-8"/>
                   <title>{title}</title>
                   {PROD && <link rel="stylesheet" href="/static/prerender.css" type="text/css" />}
                 </head>
                 <body>
                   <script dangerouslySetInnerHTML={{__html: initialState}} />
                   {PROD ? <div id="root" dangerouslySetInnerHTML={{__html: root}}></div> : <div id="root"></div>}
                    {PROD && <script dangerouslySetInnerHTML={{__html: manifest.text}}/>}
                    {PROD && <script src={vendor.js}/>}
                   <script src={PROD ? app.js : '/static/app.js'} />
                 </body>
               </html>
              );
            })
            .catch(() => {
                return {"error": "An error occurred"}
            })
    } else {
        // otherwise, we can just respond with our rendered app
        return (
           <html>
             <head>
               <meta charSet="utf-8"/>
               <title>{title}</title>
               {PROD && <link rel="stylesheet" href="/static/prerender.css" type="text/css" />}
             </head>
             <body>
               <script dangerouslySetInnerHTML={{__html: initialState}} />
               {PROD ? <div id="root" dangerouslySetInnerHTML={{__html: root}}></div> : <div id="root"></div>}
                {PROD && <script dangerouslySetInnerHTML={{__html: manifest.text}}/>}
                {PROD && <script src={vendor.js}/>}
               <script src={PROD ? app.js : '/static/app.js'} />
             </body>
           </html>
          );
    }
}

export default Html;

createstore.js:


import {
  createStore,
  combineReducers,
  applyMiddleware,
  compose,
} from 'redux';

import {
  ConnectedRouter,
  routerReducer,
  routerMiddleware,
  syncHistory
} from 'react-router-redux';

import {
  ReduxTaxiMiddleware,
  PromiseMiddleware
} from 'redux-taxi';

import thunk from 'redux-thunk';

import {ReduxTaxi} from 'redux-taxi';

import * as Reducers from './reducers/index.js';

export default (history, instance) => {
  const middleware = [
    routerMiddleware(history),
    ReduxTaxiMiddleware(instance.reduxTaxi),
    PromiseMiddleware,
    thunk
  ];

  const store = compose(createStore(combineReducers({
    ...Reducers,
    router: routerReducer
  }), applyMiddleware(...middleware)));

  if (module.hot) {
    // Enable Webpack hot module replacement for reducers
     module.hot.accept('./reducers', () => {
       const nextReducers = require('./reducers/index.js');
       const rootReducer = combineReducers({
         ...nextReducers,
         router: routerReducer
       });

       store.replaceReducer(rootReducer);
     });
   }

  return store;
}
dougsmith1000 commented 7 years ago

I'm going to work on the md showing up and I'll repost. Sorry for the ugliness.

tizmagik commented 7 years ago

@dougsmith1000 I haven't tested this in RRv4 but happy to look at an example repo or something to see how it might work. Maybe you can push up a repo that I can pull down and try it out on?

jaredpalmer commented 7 years ago

@tizmagik It works in RR4. Was using this for a while on another project

tizmagik commented 7 years ago

Ah great, thanks @jaredpalmer !

Curious how you're finding the lib so far?

jaredpalmer commented 7 years ago

This is a great lib, especially when paired with recompose. I no longer use Redux, but when I did this was my preferred way to go about SSR data.

dougsmith1000 commented 7 years ago

Hi @tizmagik, sorry for the silence. I was working on somethiing else for awhile and never got back to this. I cleaned up the markup above and am still curious if you can help me out on it. The error I get is around syncHistory, hence my question about it being absent from RRv4 and taxi's compatibility. I'm glad to hear that jaredpalmer has it working though. This library looks exactly like what I'm looking for for my project. Thanks again!

PS, today or later this week, I'll create a sample repo of my project structure if you're able to look into it.

tizmagik commented 7 years ago

Sounds good @dougsmith1000 -- maybe @jaredpalmer could lend a hand as well 😁

jaredpalmer commented 7 years ago

Happy to review