2

I'm working off of github's erikras/react-redux-universal-hot-example repository.
I am trying to make the routes dynamic to aptly reflect a real world scenario.

  1. The client/entry.js page calls a shared module function 'getRoutes' passing along a store parameter.

    const component = (
      <Router render={(props) =>
                <ReduxAsyncConnect {...props} helpers={{client}} 
                   filter={item => !item.deferred} />
             } history={history}>
        // where the routes are populated
        {getRoutes(store)} // <-- function call
      </Router>
    

    );

  2. The server's isomorphic-routes.config.server.js also calls getRoutes(store) for route matching with client:

        match({history, routes: getRoutes(store), location: req.originalUrl},    
        (error, redirectLocation, renderProps) => {
         .....
      });
    
  3. The shared dynamic-routes.shared.js page is a reg JavaScript function 'getRoutes':

     export default (store) => {
        ......
    
      let dynRoutes= [];
      store.dispatch(loadNav()).then(result => {
      dynRoutes = result;
      })
    
    return (
     <Route path="/" component={App}>
       { /* Home (main) route */ }
       <IndexRoute component={Home}/>
        { /* Get dynamic routes from database */ }
        { dynRoutes.length > 0 ?
         <DynamicRoutes dynRoutes={dynRoutes} />
         : ''}    
        { /* Catch all route */ }
        <Route path="*" component={NotFound} status={404} />
       </Route>
       )
      };
    

While on the second 'go around', dynRoutes' array does receive the routes collection, the return statement will not re-process and the component is determined to have a zero length and the callback values are ignored.

I know this is not a React component that offers the benefit of re-loading with a new return value, but, does anyone have any insight how I can get this JavaScript function to return with the callback values?

Thank you.

1
  • In original post I had dashboard.length > 0, s/b dynRoutes.length > 0 Commented Jul 20, 2016 at 18:31

2 Answers 2

1

The short answer is I needed to make not only the dynamic-routes.shared.js asynchronous,
but also any other function calls, from both the server & client.

I used promises on the dynamic-routes.shared.js, and async/await on the server/client calls to dynamic-routes.shared.js.

I didn't bother throwing the data into a JSON, you can pretty much figure that out yourself.

1. dynamic-routes.shared.js

      function routesWithStore(store) {
       return new Promise(function(resolve, reject) {
       // you can use something like this to actually have these
       // routes in a database
       // let dynRoutes= [];
       // store.dispatch(loadNav()).then(result => {
       // dynRoutes = result;
       // }) 
       // resolve(dynRoutes.map(route => {
       //   ..... your code here .....
       // }))
         resolve(
           {
             path: '',
             component: App,
             childRoutes: [
               {path: '/', component: Home},
               {path: 'home', component: Home},
               {path: 'about', component: About},
               {path: '*', component: NotFound}
             ]
           }
         )
       });
     }

       function getRoutes(store) {      
        return(
          routesWithStore(store)
         )
       }

     exports.getRoutes = getRoutes;

2.client/entry.js

    // async call to dynamic-routes.shared.js ////////
    async function main() {
      try {
        const result = await getRoutes(store);
        processRoutes(result);
      } catch(err) {
        console.log(err.message)
      }
    }

    function processRoutes(result) {
      const component = (
        <Router render={(props) =>
            <ReduxAsyncConnect {...props} helpers={{client}} 
              filter={item => !item.deferred} />
                } history={history}>
                {result} <------------- client value from dynamic-routes.shared.js
              </Router>
            );

      ReactDOM.render(
        <Provider store={store} key="provider">
          {component}
        </Provider>,
         document.querySelector('#root');
         );

_

  if (process.env.NODE_ENV !== 'production') {
        window.React = React; // enable debugger

      if (!dest || !dest.firstChild 
                || !dest.firstChild.attributes 
                || !dest.firstChild.attributes['data-react-checksum']) {
      console.error
       ('Server-side React render was discarded. ' + 
        'Make sure that your initial render does not contain any client-side code.');
       }
     }

      if (__DEVTOOLS__ && !window.devToolsExtension) {
       const DevTools = require('shared/redux/dev-tools/dev-tools.redux.shared');
       ReactDOM.render(
        <Provider store={store} key="provider">
           <div>
            {component}
            <DevTools />
           </div>
        </Provider>,
         document.querySelector('#root');
        );
      }
    }

   main();

3. isomorphic-routes.config.server

     module.exports = (app) => {
      app.use((req, res) => {
        if (__DEVELOPMENT__) {
          // Do not cache webpack stats: the script file would change since
          // hot module replacement is enabled in the development env
          webpackIsomorphicTools.refresh();
        }
        const client = new ApiClient(req);
        const memoryHistory = createHistory(req.originalUrl);
        const store = createStore(memoryHistory, client);
        const history = syncHistoryWithStore(memoryHistory, store);

        function hydrateOnClient() {
          res.send('<!doctype html>\n' +
            ReactDOM.renderToString(

              <Html assets={webpackIsomorphicTools.assets()} 
                   store={store}/>));
        }

        if (__DISABLE_SSR__) {
          hydrateOnClient();
          return;
        }

_

        // Async call to dynamic-routes.shared.js ////////

        async function main() {
          try {
            const routesResult = await getRoutes(store);
            // pass routesResult below
            match({history, routes: routesResult, location: req.originalUrl}, 
                 (error, redirectLocation, renderProps) => {
              if (redirectLocation) {
                res.redirect(redirectLocation.pathname + redirectLocation.search);
              } else if (error) {
                console.error('ROUTER ERROR:', pretty.render(error));
                res.status(500);
                hydrateOnClient();

_

              } else if (renderProps) {
                loadOnServer({...renderProps, store, helpers: {client}}).then(() => {
                  const component = (
                    <Provider store={store} key="provider">
                      <ReduxAsyncConnect {...renderProps} />
                    </Provider>
                  );

                  res.status(200);

                  global.navigator = {userAgent: req.headers['user-agent']};

                  res.send('<!doctype html>\n' +
                    ReactDOM.renderToString(
                         <Html assets={webpackIsomorphicTools.assets()}   
                          component={component} store={store}/>));
                });
              } else {
                res.status(404).send('Iso Routes Not Found ' + routeResult);
              }
            });

          } catch(error) {
            console.error(error);
          }
        }

        main();

      });
    };

I hope this helps anyone looking to make their isomorphic routes dynamic.
Make America great again!

Sign up to request clarification or add additional context in comments.

Comments

0

The standard solution would be to set up getRoutes to be a real React component that follows the React component lifecycle, in which case you can use this.setState({ dynRoutes }) when the callback returns to trigger a re-render. In its current setup, getRoutes will never be able to force the caller/parent to re-render.

You'll need to separate the store.dispatch(loadNav()) call from the component rendering, and structure the code so that the promise handler triggers a re-render inside a component further up the hierarchy with setState, or calling the root ReactDOM.render again.

Comments