Server-side API calling/preloading. (#224)
* make sure BACKEND_URL gets set for server in production mode * ssr api calls by path * turn off redux logger on server * massage preloaded state (BNify JSONed BNs) * make sure fetchProposal returns async/promise * make sure render works on ssr (check window refs) * linting issue
This commit is contained in:
parent
f8910b1e09
commit
a95a8ff080
|
@ -63,11 +63,15 @@ export class ProposalDetail extends React.Component<Props, State> {
|
|||
} else {
|
||||
this.checkBodyOverflow();
|
||||
}
|
||||
window.addEventListener('resize', this.checkBodyOverflow);
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('resize', this.checkBodyOverflow);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.checkBodyOverflow);
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('resize', this.checkBodyOverflow);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
|
@ -121,7 +125,7 @@ export class ProposalDetail extends React.Component<Props, State> {
|
|||
<div className="Proposal-top">
|
||||
<div className="Proposal-top-social">
|
||||
<SocialShare
|
||||
url={window.location.href}
|
||||
url={(typeof window !== 'undefined' && window.location.href) || ''}
|
||||
title={`${proposal.title} needs funding on Grant-io!`}
|
||||
text={`${
|
||||
proposal.title
|
||||
|
|
|
@ -8,10 +8,12 @@ import { BrowserRouter as Router } from 'react-router-dom';
|
|||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { configureStore } from 'store/configure';
|
||||
import { massageSerializedState } from 'utils/api';
|
||||
import Routes from './Routes';
|
||||
import i18n from './i18n';
|
||||
|
||||
const initialState = window && (window as any).__PRELOADED_STATE__;
|
||||
const initialState =
|
||||
window && massageSerializedState((window as any).__PRELOADED_STATE__);
|
||||
const { store, persistor } = configureStore(initialState);
|
||||
const i18nLanguage = window && (window as any).__PRELOADED_I18N__;
|
||||
i18n.changeLanguage(i18nLanguage.locale);
|
||||
|
|
|
@ -23,8 +23,8 @@ export function fetchProposals() {
|
|||
|
||||
export type TFetchProposal = typeof fetchProposal;
|
||||
export function fetchProposal(proposalId: ProposalWithCrowdFund['proposalId']) {
|
||||
return (dispatch: Dispatch<any>) => {
|
||||
dispatch({
|
||||
return async (dispatch: Dispatch<any>) => {
|
||||
return dispatch({
|
||||
type: types.PROPOSAL_DATA,
|
||||
payload: async () => {
|
||||
return (await getProposal(proposalId)).data;
|
||||
|
|
|
@ -13,7 +13,7 @@ const sagaMiddleware = createSagaMiddleware();
|
|||
type MiddleWare = ThunkMiddleware | SagaMiddleware<any> | any;
|
||||
|
||||
const bindMiddleware = (middleware: MiddleWare[]) => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined') {
|
||||
const { createLogger } = require('redux-logger');
|
||||
const logger = createLogger({
|
||||
collapsed: true,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import BN from 'bn.js';
|
||||
import { TeamMember, CrowdFund, ProposalWithCrowdFund } from 'types';
|
||||
import { TeamMember, CrowdFund, ProposalWithCrowdFund, UserProposal } from 'types';
|
||||
import { socialAccountsToUrls, socialUrlsToAccounts } from 'utils/social';
|
||||
import { AppState } from 'store/reducers';
|
||||
|
||||
export function formatTeamMemberForPost(user: TeamMember) {
|
||||
return {
|
||||
|
@ -28,35 +29,35 @@ export function formatTeamMemberFromGet(user: any): TeamMember {
|
|||
};
|
||||
}
|
||||
|
||||
export function formatCrowdFundFromGet(crowdFund: CrowdFund): CrowdFund {
|
||||
export function formatCrowdFundFromGet(crowdFund: CrowdFund, base = 10): CrowdFund {
|
||||
const bnKeys = ['amountVotingForRefund', 'balance', 'funded', 'target'] as Array<
|
||||
keyof CrowdFund
|
||||
>;
|
||||
bnKeys.forEach(k => {
|
||||
crowdFund[k] = new BN(crowdFund[k] as string);
|
||||
crowdFund[k] = new BN(crowdFund[k] as string, base);
|
||||
});
|
||||
crowdFund.milestones = crowdFund.milestones.map(ms => {
|
||||
ms.amount = new BN(ms.amount);
|
||||
ms.amountAgainstPayout = new BN(ms.amountAgainstPayout);
|
||||
ms.amount = new BN(ms.amount, base);
|
||||
ms.amountAgainstPayout = new BN(ms.amountAgainstPayout, base);
|
||||
return ms;
|
||||
});
|
||||
crowdFund.contributors = crowdFund.contributors.map(c => {
|
||||
c.contributionAmount = new BN(c.contributionAmount);
|
||||
c.contributionAmount = new BN(c.contributionAmount, base);
|
||||
return c;
|
||||
});
|
||||
return crowdFund;
|
||||
}
|
||||
|
||||
export function formatProposalFromGet(proposal: ProposalWithCrowdFund) {
|
||||
proposal.team = proposal.team.map(formatTeamMemberFromGet);
|
||||
proposal.proposalUrlId = generateProposalUrl(proposal.proposalId, proposal.title);
|
||||
proposal.crowdFund = formatCrowdFundFromGet(proposal.crowdFund);
|
||||
for (let i = 0; i < proposal.crowdFund.milestones.length; i++) {
|
||||
proposal.milestones[i] = {
|
||||
...proposal.milestones[i],
|
||||
...proposal.crowdFund.milestones[i],
|
||||
};
|
||||
}
|
||||
proposal.team = proposal.team.map(formatTeamMemberFromGet);
|
||||
proposal.proposalUrlId = generateProposalUrl(proposal.proposalId, proposal.title);
|
||||
proposal.crowdFund = formatCrowdFundFromGet(proposal.crowdFund);
|
||||
return proposal;
|
||||
}
|
||||
|
||||
|
@ -79,3 +80,32 @@ export function extractProposalIdFromUrl(slug: string) {
|
|||
}
|
||||
return proposalId;
|
||||
}
|
||||
|
||||
// pre-hydration massage (BNify JSONed BNs)
|
||||
export function massageSerializedState(state: AppState) {
|
||||
// proposals
|
||||
state.proposal.proposals.forEach(p => {
|
||||
formatCrowdFundFromGet(p.crowdFund, 16);
|
||||
for (let i = 0; i < p.crowdFund.milestones.length; i++) {
|
||||
p.milestones[i] = {
|
||||
...p.milestones[i],
|
||||
...p.crowdFund.milestones[i],
|
||||
};
|
||||
}
|
||||
});
|
||||
// users
|
||||
const bnUserProp = (p: UserProposal) => {
|
||||
p.funded = new BN(p.funded, 16);
|
||||
p.target = new BN(p.target, 16);
|
||||
return p;
|
||||
};
|
||||
Object.values(state.users.map).forEach(user => {
|
||||
user.createdProposals.forEach(bnUserProp);
|
||||
user.fundedProposals.forEach(bnUserProp);
|
||||
user.comments.forEach(c => {
|
||||
c.proposal = bnUserProp(c.proposal);
|
||||
});
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -50,6 +50,10 @@ envProductionRequiredHandler(
|
|||
'https://eip-712.herokuapp.com/contract/factory',
|
||||
);
|
||||
|
||||
if (!process.env.BACKEND_URL) {
|
||||
process.env.BACKEND_URL = 'http://localhost:5000';
|
||||
}
|
||||
|
||||
const appDirectory = fs.realpathSync(process.cwd());
|
||||
process.env.NODE_PATH = (process.env.NODE_PATH || '')
|
||||
.split(path.delimiter)
|
||||
|
@ -59,9 +63,9 @@ process.env.NODE_PATH = (process.env.NODE_PATH || '')
|
|||
|
||||
module.exports = () => {
|
||||
const raw = {
|
||||
PORT: process.env.PORT || 3000,
|
||||
BACKEND_URL: process.env.BACKEND_URL,
|
||||
NODE_ENV: process.env.NODE_ENV || 'development',
|
||||
BACKEND_URL: process.env.BACKEND_URL || 'http://localhost:5000',
|
||||
PORT: process.env.PORT || 3000,
|
||||
PUBLIC_HOST_URL: process.env.PUBLIC_HOST_URL,
|
||||
};
|
||||
|
||||
|
|
|
@ -4,10 +4,10 @@ import * as path from 'path';
|
|||
import chalk from 'chalk';
|
||||
import manifestHelpers from 'express-manifest-helpers';
|
||||
import * as bodyParser from 'body-parser';
|
||||
import dotenv from 'dotenv';
|
||||
import expressWinston from 'express-winston';
|
||||
import i18nMiddleware from 'i18next-express-middleware';
|
||||
|
||||
import '../config/env';
|
||||
// @ts-ignore
|
||||
import * as paths from '../config/paths';
|
||||
import log from './log';
|
||||
|
@ -17,8 +17,6 @@ import i18n from './i18n';
|
|||
process.env.SERVER_SIDE_RENDER = 'true';
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
|
||||
// log requests
|
||||
|
|
|
@ -18,6 +18,7 @@ import i18n from './i18n';
|
|||
|
||||
// @ts-ignore
|
||||
import * as paths from '../config/paths';
|
||||
import { storeActionsForPath } from './ssrAsync';
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
let cachedStats: any;
|
||||
|
@ -78,6 +79,7 @@ const chunkExtractFromLoadables = (loadableState: any) =>
|
|||
|
||||
const serverRenderer = () => async (req: Request, res: Response) => {
|
||||
const { store } = configureStore();
|
||||
await storeActionsForPath(req.url, store);
|
||||
|
||||
// i18n
|
||||
const locale = (req as any).language;
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { Store } from 'redux';
|
||||
import { fetchProposal } from 'modules/proposals/actions';
|
||||
import {
|
||||
fetchUser,
|
||||
fetchUserCreated,
|
||||
fetchUserFunded,
|
||||
fetchUserComments,
|
||||
} from 'modules/users/actions';
|
||||
import { extractProposalIdFromUrl } from 'utils/api';
|
||||
|
||||
const pathActions = [
|
||||
{
|
||||
matcher: /^\/proposals\/(.+)$/,
|
||||
action: (match: RegExpMatchArray, store: Store) => {
|
||||
const proposalId = extractProposalIdFromUrl(match[1]);
|
||||
if (proposalId) {
|
||||
return store.dispatch<any>(fetchProposal(proposalId));
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
matcher: /^\/profile\/(.+)$/,
|
||||
action: (match: RegExpMatchArray, store: Store) => {
|
||||
const userId = match[1];
|
||||
if (userId) {
|
||||
return Promise.all([
|
||||
store.dispatch<any>(fetchUser(userId)),
|
||||
store.dispatch<any>(fetchUserCreated(userId)),
|
||||
store.dispatch<any>(fetchUserFunded(userId)),
|
||||
store.dispatch<any>(fetchUserComments(userId)),
|
||||
]);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function storeActionsForPath(path: string, store: Store) {
|
||||
const pathAction = pathActions.find(pa => !!path.match(pa.matcher));
|
||||
if (pathAction) {
|
||||
const matches = path.match(pathAction.matcher);
|
||||
if (matches) {
|
||||
return pathAction.action(matches, store);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
Loading…
Reference in New Issue