React-router: How to access Router's current components when rendering Server-side

Created on 2 Jan 2016  路  12Comments  路  Source: ReactTraining/react-router

I am trying to follow the Server-side rendering example, and take it a step farther by allowing Route "handler" components to by convention provide a method such as fetchData which would give them an opportunity to asynchronously load data before they are rendered.

I've got synchronous server-side rendering working just like in the example above, but I'm having a hard time figuring out how to access the _instances_ of the Components that the RoutingContext (I'm on 1.0.3 currently) creates when it's rendered.

Here's a snippet:

match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
    if (error) {
        res.status(500).send(error.message)
    } else if (redirectLocation) {
        res.redirect(302, redirectLocation.pathname + redirectLocation.search)
    } else if (renderProps) {
        let routingContext = React.createElement(RoutingContext, renderProps);
        let html = renderToString(routingContext);
        console.log(routingContext.props.components); 
        //These ^^ are the *classes* of the components which get rendered when my route is hit.

        //I want to be able to access the component *instances* so that I can do something like what follows:
        var promises = instances.map((component) => {
            if ( component.fetchData ) {
                return component.fetchData(); //fetch Data will set data on the component, allowing for complete rendering
            }
            return Promise.resolve();
        });

        Promise.all(promises).then(() => {
            let html = renderToString(routingContext); //unsure if this will work, hopefully so if components have their data set?
            res.status(200).send(html);
        })
    } else {
        res.status(404).send('Not found')
    }
})

Is there a way to access the component instances?

Most helpful comment

The RoutingContext (now RouterContext in 2.0.0) creates the elements internally.

However, this should work for you. Copying this from my own code base:

// This is inside of my server file and the callback to match()
// Note that I use redux here, but you don't have to.
Promise.all(
  prefetchData(renderProps, store)
).then(render, render);

And this is the contents of prefetchData:

export default function(renderProps, store) {
  const { components, params } = renderProps;
  const { getState, dispatch } = store;

  return components
    .filter(component => component)
    .filter(component => component.fetchData)
    .map(component => component.fetchData)
    .map(fetchData => fetchData(getState(), dispatch, params));
}

I've got a decorator to add the static fetchData prop. This works because I use redux, which stores the state in one place. I don't have to pass that state to my component instances directly, they can query it from the store themselves.

All 12 comments

The RoutingContext (now RouterContext in 2.0.0) creates the elements internally.

However, this should work for you. Copying this from my own code base:

// This is inside of my server file and the callback to match()
// Note that I use redux here, but you don't have to.
Promise.all(
  prefetchData(renderProps, store)
).then(render, render);

And this is the contents of prefetchData:

export default function(renderProps, store) {
  const { components, params } = renderProps;
  const { getState, dispatch } = store;

  return components
    .filter(component => component)
    .filter(component => component.fetchData)
    .map(component => component.fetchData)
    .map(fetchData => fetchData(getState(), dispatch, params));
}

I've got a decorator to add the static fetchData prop. This works because I use redux, which stores the state in one place. I don't have to pass that state to my component instances directly, they can query it from the store themselves.

Yes - it has to be static. See https://github.com/rackt/async-props/ though.

Ah, fantastic! Thank you both for the help, this is a big help. :+1:

I've been trying out react-router v4, how would i achieve this without renderProps?

my _half_ updated code is below... you can see the renderProps args are still there. Is there an equivalent in RRv4?

// set-router-context.js
const createMarkup = (req, context, store) => renderToString(
  <Provider store={store}>
    <ServerRouter location={req.url} context={context} >
      {makeRoutes()}
    </ServerRouter>
  </Provider>
);

const setRouterContext = (req, res, next) => {
  const store = configureStore();
  const context = createServerRenderContext();
  const markup = createMarkup(req, context);
  const result = context.getResult();
  if (result.redirect) {
    res.redirect(301, result.redirect.pathname + result.redirect.search);
  } else {
    const setContext = () => {
      res.status(result.missed ? 404 : 200);
      res.routerContext = (result.missed)
        ? createMarkup(req, context, store)
        : markup;
      next();
    };
    fetchComponentData(store.dispatch, renderProps.components, renderProps.params)
      .then(setContext)
      .catch((err) => res.render500(err));
  }
};

@peter-mouland Hi, i've stuck at the same place. Have you solved this issue in any elegant way?

@antipin I have solved this with react-router-addons-routes.

using koa v2 it looks like this:

...
import { matchRoutesToLocation } from 'react-router-addons-routes';

const createMarkup = (req, context, store) => renderToString(
  <Provider store={store}>
    <ServerRouter location={req.url} context={context} >
      {makeRoutes()}
    </ServerRouter>
  </Provider>
);

async function getContext(dispatch, req) {
  const { matchedRoutes, params } = matchRoutesToLocation(routes, { pathname: req.url });
  const needs = [];
  matchedRoutes.filter((route) => route.component.needs)
    .map((route) => route.component.needs.forEach((need) => needs.push(need)));
  return await Promise.all(needs.map((need) => dispatch(need(params))));
}

function setRouterContext() {
  return async (ctx, next) => {
    const store = configureStore();
    const context = createServerRenderContext();
    const markup = createMarkup(ctx.request, context, store);
    const result = context.getResult();
    if (result.redirect) {
      ctx.status = 301;
      ctx.redirect(result.redirect.pathname + result.redirect.search);
    } else {
      await getContext(store.dispatch, ctx.request);
      ctx.status = result.missed ? 404 : 200;
      ctx.initialState = store.getState();
      ctx.routerContext = (result.missed)
            ? createMarkup(ctx.request, context, store)
            : markup;
    }
    await next();
  };
}

checkout my react-lego app for more code around this (koa-app branch)

@peter-mouland react-router-addons-routes does not exists. I don't know how to do it with react router v4.

@albert-olive this is the latest incarnation of the code that i use:

(the addons was replace in RR4 with react-router-dom/matchPath.

//set-router-context.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import StaticRouter from 'react-router-dom/StaticRouter';
import { Provider } from 'react-redux';
import cookie from 'react-cookie';
import matchPath from 'react-router-dom/matchPath';

import configureStore from '../../app/store/configure-store';
import { makeRoutes, getRoutesConfig } from '../../app/routes';

function getMatch(routesArray, url) {
  return routesArray
    .find((route) => matchPath(url, { path: route.path, exact: route.exact, strict: false }));
}

const Markup = ({ req, store, context }) => (
  <Provider store={store}>
    <StaticRouter location={req.url} context={ context } >
      {makeRoutes()}
    </StaticRouter>
  </Provider>
);

function setRouterContext() {
  const routesArray = getRoutesConfig();
  return async (ctx, next) => {
    const routerContext = {};
    const store = configureStore();
    cookie.plugToRequest(ctx.request, ctx.response); // essential for universal cookies
    const markup = renderToString(Markup({ req: ctx.request, store, context: routerContext }));
    const match = getMatch(routesArray, ctx.request.url);
    if (routerContext.url) {
      ctx.status = 301;
      ctx.redirect(routerContext.location.pathname + routerContext.location.search);
    } else {
      ctx.status = match ? 200 : 404;
      ctx.initialState = store.getState();
      ctx.markup = markup;
    }
    await next();
  };
}

export default setRouterContext;
 // app/routes.js
import React from 'react';
import Route from 'react-router-dom/Route';
import Switch from 'react-router-dom/Switch';

import MainLayout from './Layouts/MainLayout';
import Homepage from './containers/Homepage/Homepage';
import AdminPage from './containers/AdminPage/AdminPage';
import NotFound from './containers/NotFound/NotFound';

import RouteWithAuthCheck from './authentication/components/RouteWithAuthCheck/RouteWithAuthCheck';

const baseMetaData = {
  title: 'demo',
  description: '',
  meta: {
    charset: 'utf-8',
    name: {
      keywords: 'react,example'
    }
  }
};

export function getRoutesConfig() {
  return [
    {
      name: 'homepage',
      exact: true,
      path: '/',
      meta: {
        ...baseMetaData,
        title: 'Homepage'
      },
      label: 'Homepage',
      component: Homepage
    },
    {
      name: 'admin',
      path: '/admin/',
      meta: {
        ...baseMetaData,
        title: 'Admin'
      },
      label: 'Admin',
      requiresAuthentication: true,
      component: AdminPage,
    }
  ];
}

export function makeRoutes() {
  return (
    <MainLayout>
      <Switch>
        {getRoutesConfig().map((route) => <RouteWithAuthCheck {...route} key={ route.name } />)}
        <Route title={'Page Not Found'} component={ NotFound }/>
      </Switch>
    </MainLayout>
  );
}

Nice, but what I need is to call a static fetchData placed in my components. That's why I needed prefetchData:

export const prefetchData = (renderProps, store) => {
  const { components, params } = renderProps;
  const { dispatch } = store;
  const state = store.getState();
  const extractWrappedComponent = (component) => {
    if (component.WrappedComponent) {
      return extractWrappedComponent(component.WrappedComponent);
    }
    return component;
  };

  return components
    .filter(component => component !== undefined)
    // Get component, be aware if they are wrapped by redux connect or intl.
    .map(component => extractWrappedComponent(component))
    // Get the fetchData method of each component
    .map(component => component.fetchData)
    .filter(fetchData => fetchData !== undefined)
    // Invoke each fetchData to initialize the redux state.
    .map(fetchData => fetchData(state, dispatch, params));
};

I can access to a single component but not the array of components as match method did before and needed for prefecthData method.

routes.some(route => {
      // use `matchPath` here
      const match = matchPath(req.url, route)
      if (match) {
        prefetchData(match, store);
      }
      return match;
    })

    Promise.all(promises).then(data => {
      // Do something
    })

ah ok, then to complete the story, here is how you might do that. The following code presumes you have a static needs = [fetchDataAction] array. Also note the use ofstrict:false` and trying to match exact if set on the route, i think these are important.

async function getRouteData(routesArray, url, dispatch) {
  const needs = [];
  routesArray
    .filter((route) => route.component.needs)
    .forEach((route) => {
      const match = matchPath(url, { path: route.path, exact: route.exact, strict: false });
      if (match) {
        route.component.needs.forEach((need) => {
          const result = need(Object.keys(match.params).length > 0 ? match.params : undefined);
          needs.push(dispatch(result));
        });
      }
    });
  await Promise.all(needs);
}

then you call this just before you renderToString:

    await getRouteData(routesArray, ctx.request.url, store.dispatch);

If I'm understanding this correctly, doesn't the solution by @peter-mouland defeat one of the biggest benefits of V4, being able to mix Routes with other React components?

Ex:

let routesWithWrapper = (
<MyGreatWrapper>
      <Switch>
       <Route exact path="/new" component={NewThingPanel} />
       <Route exact path="/edit/:thingId" component={EditThingPanel} /> 
     </Switch>
</MyGreatWrapper>
)

My need is something like:

extractMatchingComponentsFromMixedRouteObject(routesWithWrapper, url)

which would recursively iterate a mixed route object to find the matching routes.
Then you could use the getRouteData on the resulting components.

(I'll comment again if this ends up being what I do)

@livemixlove you're right. My solution only solves 'top-level' routes from a router config. It still allows you to nest routes, but it won't prefetch any data from nested routes. I have yet to find this a problem in what I've built but I can see how others might need more complete solution. I'd love to see what you come up with to solve it.

If it helps, there has been more discussion here, https://github.com/peter-mouland/react-lego/issues/39 , I haven't got round to exploring these techniques either.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Waquo picture Waquo  路  3Comments

winkler1 picture winkler1  路  3Comments

davetgreen picture davetgreen  路  3Comments

andrewpillar picture andrewpillar  路  3Comments

wzup picture wzup  路  3Comments