Call component.loadData on server side (and client)

This commit is contained in:
Vesa Luusua 2017-01-27 00:55:01 +02:00
parent 90bc748731
commit 71ab7c33e0
5 changed files with 118 additions and 38 deletions

View file

@ -23,7 +23,8 @@
"redux-saga": "^0.14.3",
"sanitize.css": "^4.1.0",
"sharetribe-scripts": "0.8.6",
"source-map-support": "^0.4.11"
"source-map-support": "^0.4.11",
"url": "^0.11.0"
},
"devDependencies": {
"nodemon": "^1.11.0",

View file

@ -22,6 +22,7 @@ const compression = require('compression');
const path = require('path');
const fs = require('fs');
const qs = require('qs');
const url = require('url');
const _ = require('lodash');
const React = require('react');
const { createServerRenderContext } = require('react-router');
@ -33,7 +34,9 @@ const buildPath = path.resolve(__dirname, '..', 'build');
const manifestPath = path.join(buildPath, 'asset-manifest.json');
const manifest = require(manifestPath);
const mainJsPath = path.join(buildPath, manifest['main.js']);
const renderApp = require(mainJsPath).default;
const mainJs = require(mainJsPath);
const renderApp = mainJs.default;
const matchPathname = mainJs.matchPathname;
// The HTML build file is generated from the `public/index.html` file
// and used as a template for server side rendering. The application
@ -66,8 +69,21 @@ const template = _.template(indexHtml, {
escape: reNoMatch,
});
function render(url, context, preloadedState) {
const { head, body } = renderApp(url, context, preloadedState);
function render(requestUrl, context, preloadedState) {
const { head, body } = renderApp(requestUrl, context, preloadedState);
const pathname = url.parse(requestUrl).pathname;
const { matchedRoutes, params } = matchPathname(pathname);
const component = matchedRoutes[0].component;
if (component.loadData) {
component
.loadData()
.then((val) => {
console.log('Data fetch resolved', val);
});
}
// Preloaded state needs to be passed for client side too.
// For security reasons we ensure that preloaded state is considered as a string

View file

@ -1,4 +1,4 @@
import React, { PropTypes } from 'react';
import React, { Component, PropTypes } from 'react';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { includes } from 'lodash';
@ -39,41 +39,53 @@ const fakeListings = [
},
];
export const SearchPageComponent = props => {
const { tab } = props;
const selectedTab = includes(['filters','listings', 'map'], tab)
? tab
: 'listings';
export class SearchPageComponent extends Component {
const filtersClassName = classNames(css.filters, {
[css.open]: selectedTab === 'filters',
});
const listingsClassName = classNames(css.filters, {
[css.open]: selectedTab === 'listings',
});
const mapClassName = classNames(css.filters, {
[css.open]: selectedTab === 'map',
});
componentWillMount() {
/* eslint-disable no-console */
console.log('Client loads data');
SearchPageComponent.loadData().then((val) => {
console.log('Client fetched data', val);
});
/* eslint-enable no-console */
}
return (
<PageLayout title="Search page">
<div className={css.container}>
<div className={filtersClassName}>
<FilterPanel />
render() {
const { tab } = this.props;
const selectedTab = includes(['filters','listings', 'map'], tab)
? tab
: 'listings';
const filtersClassName = classNames(css.filters, {
[css.open]: selectedTab === 'filters',
});
const listingsClassName = classNames(css.filters, {
[css.open]: selectedTab === 'listings',
});
const mapClassName = classNames(css.filters, {
[css.open]: selectedTab === 'map',
});
return (
<PageLayout title="Search page">
<div className={css.container}>
<div className={filtersClassName}>
<FilterPanel />
</div>
<div className={listingsClassName}>
<SearchResultsPanel>
{fakeListings.map(l => <ListingCard key={l.id} {...l} />)}
</SearchResultsPanel>
</div>
<div className={mapClassName}>
<MapPanel>
{fakeListings.map(l => <ListingCardSmall key={l.id} {...l} />)}
</MapPanel>
</div>
</div>
<div className={listingsClassName}>
<SearchResultsPanel>
{fakeListings.map(l => <ListingCard key={l.id} {...l} />)}
</SearchResultsPanel>
</div>
<div className={mapClassName}>
<MapPanel>
{fakeListings.map(l => <ListingCardSmall key={l.id} {...l} />)}
</MapPanel>
</div>
</div>
</PageLayout>
);
</PageLayout>
);
}
};
SearchPageComponent.defaultProps = { tab: 'listings' };
@ -82,6 +94,15 @@ const { string } = PropTypes;
SearchPageComponent.propTypes = { tab: string };
SearchPageComponent.loadData = () => (
new Promise((resolve) => (
// also node has a 'setTimeout' -> it's good for testing async call.
setTimeout(() => {
resolve(fakeListings);
}, (Math.random() * 2000) + 1000)
))
);
/**
* Container functions.
* Since we add this to global store state with combineReducers, this will only get partial state

View file

@ -15,6 +15,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { ClientApp, renderApp } from './app';
import configureStore from './store';
import { matchLocation } from './routesConfiguration';
import './index.css';
@ -29,3 +30,5 @@ if (typeof window !== 'undefined') {
// Export the function for server side rendering.
export default renderApp;
export { matchLocation };

View file

@ -1,6 +1,10 @@
import React from 'react';
import { find } from 'lodash';
import { Redirect } from 'react-router';
// This will change to `matchPath` soonish
import matchPattern from 'react-router/matchPattern'
import pathToRegexp from 'path-to-regexp';
import {
AuthenticationPage,
@ -269,7 +273,42 @@ const toPathByRouteName = (nameToFind, routes) => {
const pathByRouteName = (nameToFind, routes, params = {}) =>
toPathByRouteName(nameToFind, routes)(params);
/**
* matchRoutesToLocation helps to figure out which routes are related to given location.
*/
const matchRoutesToLocation = (routes, location, matchedRoutes=[], params={}) => {
const parameters = { ...params };
routes.forEach((route) => {
const { exactly = false } = route;
const match = !route.pattern ? true : matchPattern(route.pattern, location, exactly);
if (match) {
matchedRoutes.push(route);
if (match.params) {
Object.keys(match.params).forEach(key => { parameters[key] = match.params[key]; });
}
}
if (route.routes) {
matchRoutesToLocation(route.routes, location, matchedRoutes, parameters);
}
});
return { matchedRoutes, params: parameters };
}
const matchPathnameCreator = routes => (
(location, matchedRoutes=[], params={}) =>
matchRoutesToLocation(routes, { pathname: location }, matchedRoutes, params)
);
/**
* matchRoutesToLocation helps to figure out which routes are related to given location.
*/
const matchPathname = matchPathnameCreator(routesConfiguration);
// Exported helpers
export { findRouteByName, flattenRoutes, toPathByRouteName, pathByRouteName };
export { findRouteByName, flattenRoutes, matchPathname, toPathByRouteName, pathByRouteName };
export default routesConfiguration;