markdalgleish/redial

Example for the client

Closed this issue · 8 comments

After setting everything up for using the decorators in a react-redux-router project, I am currently looking for an equivalent client side part to trigger them before the transition hooks in with @connect. Although there is a complete example for rendering on the server side, neither for getPrefetchedData or getDeferredData guidance exist. Would be useful to know how to provide the component and local properties.

I found the most suitable way, is to hook into react-router's onUpdate handler and call the getXData methods. I've found that it's best if the universal fetch is only applied after its second invocation -- as the initial page load will already contain necessary state.

const onUpdate = flow(
  after(2, function handleRouterUpdate() {
    const { components, location, params } = this.state
    getPrefetchedData(components, { store, location, params })
  }),
  function clientOnlyRouteUpdate() {
    const { components, location, params } = this.state
    getDeferredData(components, { store, location, params })
  }
)

ReactDOM.render(
  makeContent(
    <Router history={history} onUpdate={onUpdate}>
      {makeRoutes()}
    </Router>, store),
  document.getElementById('application-root')
)

@tomatau does you example imply that you are using redux-router? This would not work with redux-simple-router I assume.

I'm using redux-simple-router, it's part of a boilerplate universal app I'm making https://github.com/tomatau/breko-hub

NB. locally, I've made one tiny change to the initialisation within redux-simple-router to stop an unnecessary history.pushState on page load (only when redux dev tools are rendered). I'm expecting this to be fixed by redux-simple-router soon.

hey @tomatau, I am wondering how you are able to access this.state within onUpdate.

For me, this is undefined, because the onUpdate call is not bound to the router instance: https://github.com/rackt/react-router/blob/v1.0.1/modules/Router.js#L52

Please share your code that adds the onUpdate handler in your routing config.

The Router calls onUpdate with the correct state, but if you're using an arrow function or something, you'll be binding it out of context.

Take a look at my working code https://github.com/tomatau/breko-hub/blob/master/src/app/entry.js#L20-L37

Ouch, you're right ;-) I was using an arrow function mistakenly... Thanks alot for the hint!

So my client-side code works and looks currently like so:

//...
syncReduxAndRouter(history, store);

function onRouterUpdate() {
  const {components, location, params} = this.state;
  getPrefetchedData(components, {store, location, params});
  getDeferredData(components, {store, location, params});
}

ReactDOM.render(
  <Provider store={store}>
    <Router history={history} onUpdate={onRouterUpdate}>
      {routes}
    </Router>
  </Provider>,
  document.getElementById('main')
);

Can we have an exemple with the new library? On the client side, how can i wait for rendering the component? Or any can help me with my setup please?

Client routing:

/**
 * @file Fichier de base pour la construction du bundle avec Browserify. Ce fichier est le fichier d'entré avec Gulp.
 * @author Mikael Boutin
 * @version 0.0.1
 */
import React from 'react';
import ReactDOM from 'react-dom';
import { trigger } from 'redial';
import { Router } from 'react-router';
import { createHistory } from 'history';
import { compose, createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import { syncHistory } from 'react-router-redux';

import { thunkMiddleware } from '../middlewares/thunkMiddleware';
import routes from '../routes';
import reducers from '../reducers';
import consts from '../utils/consts';

const { APP_DOM_CONTAINER } = consts;
const history = createHistory();
const reduxRouterMiddleware = syncHistory(history);

const initialState = window.__INITIAL_STATE__;

const configureStore = compose(applyMiddleware(thunkMiddleware, reduxRouterMiddleware), window.devToolsExtension ? window.devToolsExtension() : f => f)(createStore);
const store = configureStore(reducers, initialState);
const dispatch = store.dispatch;

reduxRouterMiddleware.listenForReplays(store);

function onRouterUpdate() {
  const { components, location, params } = this.state;
  const locals = {
    path: location.pathname,
    query: location.query,
    params,
    dispatch,
  };

  trigger('defer', components, locals)
    .then(components.render);
}

ReactDOM.render((
  <Provider store={store}>
    <Router history={history} onUpdate={onRouterUpdate}>{routes}</Router>
  </Provider>
), document.getElementById(APP_DOM_CONTAINER));

It triggers 2 times the onRouterUpdate() function. My Server side router works well:

/**
 * @file Router (serverside) pour rendre les routes et les components reliés à ceux-ci.
 * @author Mikael Boutin
 * @version 0.0.1
 */
import React from 'react';
import { trigger } from 'redial';
import createMemoryHistory from 'history/lib/createMemoryHistory';
import useQueries from 'history/lib/useQueries';
import { match, RoutingContext } from 'react-router';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';

import { thunkMiddleware } from './thunkMiddleware';
import reducers from '../reducers';
import routes from '../routes';

const store = applyMiddleware(thunkMiddleware)(createStore)(reducers);
const { dispatch } = store;

/**
 * Retourne les propriétés à rendre du côté serveur
 * @function
 * @param {object} renderProps Les propriétés retournés par le router
 * @return {object} Contenant le component associé avec la route en cours et le store
 */
function getRootComponent(renderProps) {
  const state = store.getState();

  const component = (
      <Provider store={store}>
        <RoutingContext {...renderProps} />
      </Provider>
    );

  return {
    component,
    initialState: state,
  };
}

/**
 * Retourne les propriétés à rendre du côté serveur
 * @function
 * @param {object} req Objet transmi par Express en middleware
 * @param {object} res Objet transmi par Express en middleware
 * @return {promise} Un promesse contenant les éléments à rendre par React
 */
function routing(req, res) {
  const history = useQueries(createMemoryHistory)();
  const location = history.createLocation(req.url);

  return new Promise((resolve, reject) => {
    match({ routes, location }, (error, redirectLocation, renderProps) => {
      // Get array of route components:
      const components = renderProps.routes.map(route => route.component);

      // Define locals to be provided to all fetcher functions:
      const locals = {
        path: renderProps.location.pathname,
        query: renderProps.location.query,
        params: renderProps.params,

        // Allow fetcher functions to dispatch Redux actions:
        dispatch,
      };

      if (redirectLocation) {
        reject(res.status(301).redirect(redirectLocation.pathname + redirectLocation.search));
      } else if (error) {
        reject(res.status(500).send(error.message));
      } else if (renderProps === null) {
        reject(res.status(404).send('Not found'));
      }

      trigger('fetch', components, locals)
        .then(() => {
          resolve(getRootComponent(renderProps));
        })
        .catch(reject);
    });
  });
}

export default routing;

And this is my main component, the one who wrap my whole application:

/**
 * @file Component principal pour React qui contient la totalité de l'application
 * @author Mikael Boutin
 * @version 0.0.1
 */
import React from 'react';
import { connect } from 'react-redux';
import { IntlProvider } from 'react-intl';
import { provideHooks } from 'redial';

import Header from './sections/Header.react';
import Footer from './sections/Footer.react';
import MainNavigation from './sections/MainNavigation.react';

import { fetchLoginAuthToken, logout } from '../actions/login-actions';
import { languageChange } from '../actions/application-actions';

import { initialFetching } from '../utils/initialFetching';

import * as i18n from '../i18n';

/** Component principal de l'application */
class App extends React.Component {

  /**
   * @constructor
   * @param {object} props La fonction super() appelle le parent pour y transmettre ses propriétés
   */
  constructor(props) {
    super(props);

    this.handleLogout = this.handleLogout.bind(this);
    this.handleLanguageChange = this.handleLanguageChange.bind(this);
  }

  /**
   * Événement de logout - Dispatch l'action actions/login-actions logout()
   */
  handleLogout() {
    const { dispatch } = this.props;

    dispatch(logout());
  }

  handleLanguageChange(e) {
    const { dispatch } = this.props;

    e.preventDefault();

    dispatch(languageChange());
  }

  /**
   * Render le component
   * @return {JSX} Rend l'application et les autres components
   */
  render() {
    const { children, loggedIn, user, application, location } = this.props;

    const intlData = {
      locale: application.locale,
      messages: i18n[application.locale],
    };

    const className = location.pathname.substr(1).replace('/', '-');

    return (
      <IntlProvider key="intl" {...intlData}>
        <div id="application" className={className}>
          <div className="page-wrap">
            <Header loggedIn={ loggedIn } user={ user } handleLogout={ this.handleLogout } handleLanguageChange={this.handleLanguageChange} />
            <MainNavigation />
            {children}
          </div>
          <Footer />
        </div>
      </IntlProvider>
    );
  }
}

/**
 * Propriétés du component
 * @type {object}
 * @property {object} children - Components enfants.
 * @property {boolean} loggedIn - Viens du reducer reducers/login-reducers, état de login du user.
 * @property {object} user - Viens du reducer reducers/login-reducers, informations sur le user.
 * @property {function} dispatch - Permet de dispatch des actions.
 * @property {object} history - Objet history pour modifier les pages.
 */
App.propTypes = {
  children: React.PropTypes.object.isRequired,
  loggedIn: React.PropTypes.bool,
  user: React.PropTypes.object,
  dispatch: React.PropTypes.func.isRequired,
  history: React.PropTypes.object.isRequired,
  location: React.PropTypes.object.isRequired,
  application: React.PropTypes.object.isRequired,
};

const hooks = {
  fetch: ({ dispatch }) => initialFetching(dispatch),
  defer: ({ dispatch }) => {
    const userToken = global.localStorage.getItem('user_token');
    dispatch(fetchLoginAuthToken(userToken));
  },
};

export default provideHooks(hooks)(connect(state => ({ loggedIn: state.login.loggedIn, user: state.login.user, application: state.application }))(App));

I got only simple routes for now:

<Route path="/" component={App}>
    <IndexRoute component={Home}/>
    <Route path="login" component={Login}>
      <IndexRoute component={LoginForm} />
    </Route>
    <Route path="registration" component={Registration}>
      <IndexRoute component={RegistrationConnectionInfos} />
      <Route path="contact-infos" component={RegistrationContactInfos} />
      <Route path="payment" component={RegistrationPayment} />
    </Route>
    <Route path="offers" component={Offers} />
  </Route>

Thank you very much for your help! Just tell me any comments about how good or bad my code is.

Thank you very much! My app rocks 1000 times more now!!!