diff --git a/templates/ReactReduxSpa/ClientApp/boot-client.tsx b/templates/ReactReduxSpa/ClientApp/boot-client.tsx index 4dc12af..fd65c0c 100644 --- a/templates/ReactReduxSpa/ClientApp/boot-client.tsx +++ b/templates/ReactReduxSpa/ClientApp/boot-client.tsx @@ -8,6 +8,15 @@ import { syncHistoryWithStore } from 'react-router-redux'; import routes from './routes'; import configureStore from './configureStore'; import { ApplicationState } from './store'; +import cookie from 'react-cookie'; + +// If the server supplied any edits to cookies, apply them on the client +const cookieDataFromServer = window['cookieData']; +if (cookieDataFromServer) { + Object.getOwnPropertyNames(cookieDataFromServer).forEach(name => { + cookie.save(name, cookieDataFromServer[name]); + }); +} // Get the application-wide store instance, prepopulating with state from the server where available. const initialState = (window as any).initialReduxState as ApplicationState; diff --git a/templates/ReactReduxSpa/ClientApp/boot-server.tsx b/templates/ReactReduxSpa/ClientApp/boot-server.tsx index 1bb8842..49a08a6 100644 --- a/templates/ReactReduxSpa/ClientApp/boot-server.tsx +++ b/templates/ReactReduxSpa/ClientApp/boot-server.tsx @@ -6,9 +6,28 @@ import createMemoryHistory from 'history/lib/createMemoryHistory'; import { createServerRenderer, RenderResult } from 'aspnet-prerendering'; import routes from './routes'; import configureStore from './configureStore'; +import cookieUtil from 'cookie'; +import cookie from 'react-cookie'; + +function plugInCookiesFromDotNet(cookieData: { key: string, value: string }[], res) { + const formattedData = {}; + cookieData.forEach(keyValuePair => { + formattedData[keyValuePair.key] = keyValuePair.value; + }); + cookie.plugToRequest({ cookies: formattedData }, res); +} export default createServerRenderer(params => { return new Promise((resolve, reject) => { + const cookiesModifiedOnServer = {}; + if (params.data.cookies) { + // If we received some cookie data, use that to prepopulate 'react-cookie' + plugInCookiesFromDotNet(params.data.cookies, { + // Also track any cookies written on the server + cookie: (name, val) => { cookiesModifiedOnServer[name] = val; } + }) + } + // Match the incoming request against the list of client-side routes match({ routes, location: params.location }, (error, redirectLocation, renderProps: any) => { if (error) { @@ -42,7 +61,17 @@ export default createServerRenderer(params => { params.domainTasks.then(() => { resolve({ html: renderToString(app), - globals: { initialReduxState: store.getState() } + globals: { + initialReduxState: store.getState(), + + // Send any cookies written during server-side prerendering to the client. + // WARNING: Do not pass any security-sensitive cookies this way, because they will become + // readable in the HTML source. If your goal is to use this approach to manage authentication + // cookies, then be sure *not* to use 'globals' to send them to the client - instead, invoke + // Microsoft.AspNetCore.SpaServices.Prerendering.Prerender directly from your .NET code and + // only send the 'html' part back to the client (or at least, not all of the 'globals'). + cookieData: cookiesModifiedOnServer + } }); }, reject); // Also propagate any errors back into the host application }); diff --git a/templates/ReactReduxSpa/ClientApp/store/Counter.ts b/templates/ReactReduxSpa/ClientApp/store/Counter.ts index 18284c0..2ddbb1a 100644 --- a/templates/ReactReduxSpa/ClientApp/store/Counter.ts +++ b/templates/ReactReduxSpa/ClientApp/store/Counter.ts @@ -1,4 +1,5 @@ import { Action, Reducer, ThunkAction } from 'redux'; +import cookie from 'react-cookie'; // ----------------- // STATE - This defines the type of data maintained in the Redux store. @@ -30,13 +31,20 @@ export const actionCreators = { // ---------------- // REDUCER - For a given state and action, returns the new state. To support time travel, this must not mutate the old state. +const cookieKey = 'counterValue'; + +function modifiedCount(state: CounterState, delta: number): CounterState { + const newCount = state.count + delta; + cookie.save(cookieKey, newCount); // Ideally, don't do this here: have something that watches the store instead of having a side-effect from a reducer + return { count: newCount }; +} export const reducer: Reducer = (state: CounterState, action: KnownAction) => { switch (action.type) { case 'INCREMENT_COUNT': - return { count: state.count + 1 }; + return modifiedCount(state, +1); case 'DECREMENT_COUNT': - return { count: state.count - 1 }; + return modifiedCount(state, -1); default: // The following line guarantees that every action in the KnownAction union has been covered by a case above const exhaustiveCheck: never = action; @@ -44,5 +52,6 @@ export const reducer: Reducer = (state: CounterState, action: Know // For unrecognized actions (or in cases where actions have no effect), must return the existing state // (or default initial state if none was supplied) - return state || { count: 0 }; + const prevCountFromCookie = parseInt(cookie.load(cookieKey) || '0'); + return state || { count: prevCountFromCookie }; }; diff --git a/templates/ReactReduxSpa/Views/Home/Index.cshtml b/templates/ReactReduxSpa/Views/Home/Index.cshtml index a53a97e..617a749 100644 --- a/templates/ReactReduxSpa/Views/Home/Index.cshtml +++ b/templates/ReactReduxSpa/Views/Home/Index.cshtml @@ -2,7 +2,8 @@ ViewData["Title"] = "Home Page"; } -
Loading...
+
Loading...
@section scripts { diff --git a/templates/ReactReduxSpa/package.json b/templates/ReactReduxSpa/package.json index 551c8d9..c85ec77 100644 --- a/templates/ReactReduxSpa/package.json +++ b/templates/ReactReduxSpa/package.json @@ -32,6 +32,7 @@ "json-loader": "^0.5.4", "node-noop": "^1.0.0", "react": "^15.3.2", + "react-cookie": "^1.0.4", "react-dom": "^15.3.2", "react-redux": "^4.4.5", "react-router": "^2.8.1",