Hello, world!
Welcome to your new single-page application, built with:
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Layout.js b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Layout.js
deleted file mode 100644
index 216fca9296..0000000000
--- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Layout.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-import { Container } from 'reactstrap';
-import NavMenu from './NavMenu';
-
-export default props => (
-
-
-
- {props.children}
-
-
-);
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Layout.tsx b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Layout.tsx
new file mode 100644
index 0000000000..80ddb46adb
--- /dev/null
+++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Layout.tsx
@@ -0,0 +1,12 @@
+import * as React from 'react';
+import { Container } from 'reactstrap';
+import NavMenu from './NavMenu';
+
+export default (props: { children?: React.ReactNode }) => (
+
+
+
+ {props.children}
+
+
+);
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/NavMenu.js b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/NavMenu.js
deleted file mode 100644
index 9eb976ab8a..0000000000
--- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/NavMenu.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from 'react';
-import { Collapse, Container, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
-import { Link } from 'react-router-dom';
-import './NavMenu.css';
-
-export default class NavMenu extends React.Component {
- constructor (props) {
- super(props);
-
- this.toggle = this.toggle.bind(this);
- this.state = {
- isOpen: false
- };
- }
- toggle () {
- this.setState({
- isOpen: !this.state.isOpen
- });
- }
- render () {
- return (
-
-
-
- Company.WebApplication1
-
-
-
-
- Home
-
-
- Counter
-
-
- Fetch data
-
-
-
-
-
-
- );
- }
-}
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/NavMenu.tsx b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/NavMenu.tsx
new file mode 100644
index 0000000000..9d13ae352c
--- /dev/null
+++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/NavMenu.tsx
@@ -0,0 +1,42 @@
+import * as React from 'react';
+import { Collapse, Container, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
+import { Link } from 'react-router-dom';
+import './NavMenu.css';
+
+export default class NavMenu extends React.PureComponent<{}, { isOpen: boolean }> {
+ public state = {
+ isOpen: false
+ };
+
+ public render() {
+ return (
+
+
+
+ Company.WebApplication1
+
+
+
+
+ Home
+
+
+ Counter
+
+
+ Fetch data
+
+
+
+
+
+
+ );
+ }
+
+ private toggle = () => {
+ this.setState({
+ isOpen: !this.state.isOpen
+ });
+ }
+}
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/index.js b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/index.tsx
similarity index 58%
rename from src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/index.js
rename to src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/index.tsx
index 5faeb680ba..d0f11bd4f6 100644
--- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/index.js
+++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/index.tsx
@@ -1,29 +1,27 @@
import 'bootstrap/dist/css/bootstrap.css';
-import React from 'react';
-import ReactDOM from 'react-dom';
+
+import * as React from 'react';
+import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
-import { ConnectedRouter } from 'react-router-redux';
+import { ConnectedRouter } from 'connected-react-router';
import { createBrowserHistory } from 'history';
import configureStore from './store/configureStore';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
// Create browser history to use in the Redux store
-const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href');
+const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href') as string;
const history = createBrowserHistory({ basename: baseUrl });
// Get the application-wide store instance, prepopulating with state from the server where available.
-const initialState = window.initialReduxState;
-const store = configureStore(history, initialState);
-
-const rootElement = document.getElementById('root');
+const store = configureStore(history);
ReactDOM.render(
-
-
-
-
- ,
- rootElement);
+
+
+
+
+ ,
+ document.getElementById('root'));
registerServiceWorker();
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/react-app-env.d.ts b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/react-app-env.d.ts
new file mode 100644
index 0000000000..6431bc5fc6
--- /dev/null
+++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/registerServiceWorker.js b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/registerServiceWorker.js
deleted file mode 100644
index 12542ba229..0000000000
--- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/registerServiceWorker.js
+++ /dev/null
@@ -1,108 +0,0 @@
-// In production, we register a service worker to serve assets from local cache.
-
-// This lets the app load faster on subsequent visits in production, and gives
-// it offline capabilities. However, it also means that developers (and users)
-// will only see deployed updates on the "N+1" visit to a page, since previously
-// cached resources are updated in the background.
-
-// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
-// This link also includes instructions on opting out of this behavior.
-
-const isLocalhost = Boolean(
- window.location.hostname === 'localhost' ||
- // [::1] is the IPv6 localhost address.
- window.location.hostname === '[::1]' ||
- // 127.0.0.1/8 is considered localhost for IPv4.
- window.location.hostname.match(
- /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
- )
-);
-
-export default function register() {
- if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
- // The URL constructor is available in all browsers that support SW.
- const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
- if (publicUrl.origin !== window.location.origin) {
- // Our service worker won't work if PUBLIC_URL is on a different origin
- // from what our page is served on. This might happen if a CDN is used to
- // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
- return;
- }
-
- window.addEventListener('load', () => {
- const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
-
- if (isLocalhost) {
- // This is running on localhost. Lets check if a service worker still exists or not.
- checkValidServiceWorker(swUrl);
- } else {
- // Is not local host. Just register service worker
- registerValidSW(swUrl);
- }
- });
- }
-}
-
-function registerValidSW(swUrl) {
- navigator.serviceWorker
- .register(swUrl)
- .then(registration => {
- registration.onupdatefound = () => {
- const installingWorker = registration.installing;
- installingWorker.onstatechange = () => {
- if (installingWorker.state === 'installed') {
- if (navigator.serviceWorker.controller) {
- // At this point, the old content will have been purged and
- // the fresh content will have been added to the cache.
- // It's the perfect time to display a "New content is
- // available; please refresh." message in your web app.
- console.log('New content is available; please refresh.');
- } else {
- // At this point, everything has been precached.
- // It's the perfect time to display a
- // "Content is cached for offline use." message.
- console.log('Content is cached for offline use.');
- }
- }
- };
- };
- })
- .catch(error => {
- console.error('Error during service worker registration:', error);
- });
-}
-
-function checkValidServiceWorker(swUrl) {
- // Check if the service worker can be found. If it can't reload the page.
- fetch(swUrl)
- .then(response => {
- // Ensure service worker exists, and that we really are getting a JS file.
- if (
- response.status === 404 ||
- response.headers.get('content-type').indexOf('javascript') === -1
- ) {
- // No service worker found. Probably a different app. Reload the page.
- navigator.serviceWorker.ready.then(registration => {
- registration.unregister().then(() => {
- window.location.reload();
- });
- });
- } else {
- // Service worker found. Proceed as normal.
- registerValidSW(swUrl);
- }
- })
- .catch(() => {
- console.log(
- 'No internet connection found. App is running in offline mode.'
- );
- });
-}
-
-export function unregister() {
- if ('serviceWorker' in navigator) {
- navigator.serviceWorker.ready.then(registration => {
- registration.unregister();
- });
- }
-}
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/registerServiceWorker.ts b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/registerServiceWorker.ts
new file mode 100644
index 0000000000..c0452d7086
--- /dev/null
+++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/registerServiceWorker.ts
@@ -0,0 +1,105 @@
+// In production, we register a service worker to serve assets from local cache.
+
+// This lets the app load faster on subsequent visits in production, and gives
+// it offline capabilities. However, it also means that developers (and users)
+// will only see deployed updates on the "N+1" visit to a page, since previously
+// cached resources are updated in the background.
+
+// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
+// This link also includes instructions on opting out of this behavior.
+
+const isLocalhost = Boolean(
+ window.location.hostname === 'localhost' ||
+ // [::1] is the IPv6 localhost address.
+ window.location.hostname === '[::1]' ||
+ // 127.0.0.1/8 is considered localhost for IPv4.
+ window.location.hostname.match(
+ /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
+ )
+);
+
+export default function register() {
+ if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
+ // The URL constructor is available in all browsers that support SW.
+ const url = process.env.PUBLIC_URL as string;
+ const publicUrl = new URL(url, window.location.toString());
+ if (publicUrl.origin !== window.location.origin) {
+ // Our service worker won't work if PUBLIC_URL is on a different origin
+ // from what our page is served on. This might happen if a CDN is used to
+ // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
+ return;
+ }
+
+ window.addEventListener('load', () => {
+ const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
+
+ if (isLocalhost) {
+ // This is running on localhost. Lets check if a service worker still exists or not.
+ checkValidServiceWorker(swUrl);
+ } else {
+ // Is not local host. Just register service worker
+ registerValidSW(swUrl);
+ }
+ });
+ }
+}
+
+function registerValidSW(swUrl: string) {
+ navigator.serviceWorker
+ .register(swUrl)
+ .then(registration => {
+ registration.onupdatefound = () => {
+ const installingWorker = registration.installing as ServiceWorker;
+ installingWorker.onstatechange = () => {
+ if (installingWorker.state === 'installed') {
+ if (navigator.serviceWorker.controller) {
+ // At this point, the old content will have been purged and
+ // the fresh content will have been added to the cache.
+ // It's the perfect time to display a "New content is
+ // available; please refresh." message in your web app.
+ console.log('New content is available; please refresh.');
+ } else {
+ // At this point, everything has been precached.
+ // It's the perfect time to display a
+ // "Content is cached for offline use." message.
+ console.log('Content is cached for offline use.');
+ }
+ }
+ };
+ };
+ })
+ .catch(error => {
+ console.error('Error during service worker registration:', error);
+ });
+}
+
+function checkValidServiceWorker(swUrl: string) {
+ // Check if the service worker can be found. If it can't reload the page.
+ fetch(swUrl)
+ .then(response => {
+ // Ensure service worker exists, and that we really are getting a JS file.
+ const contentType = response.headers.get('content-type');
+ if (response.status === 404 || (contentType && contentType.indexOf('javascript') === -1)) {
+ // No service worker found. Probably a different app. Reload the page.
+ navigator.serviceWorker.ready.then(registration => {
+ registration.unregister().then(() => {
+ window.location.reload();
+ });
+ });
+ } else {
+ // Service worker found. Proceed as normal.
+ registerValidSW(swUrl);
+ }
+ })
+ .catch(() => {
+ console.log('No internet connection found. App is running in offline mode.');
+ });
+}
+
+export function unregister() {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.ready.then(registration => {
+ registration.unregister();
+ });
+ }
+}
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/Counter.js b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/Counter.js
deleted file mode 100644
index c3b7b101a7..0000000000
--- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/Counter.js
+++ /dev/null
@@ -1,22 +0,0 @@
-const incrementCountType = 'INCREMENT_COUNT';
-const decrementCountType = 'DECREMENT_COUNT';
-const initialState = { count: 0 };
-
-export const actionCreators = {
- increment: () => ({ type: incrementCountType }),
- decrement: () => ({ type: decrementCountType })
-};
-
-export const reducer = (state, action) => {
- state = state || initialState;
-
- if (action.type === incrementCountType) {
- return { ...state, count: state.count + 1 };
- }
-
- if (action.type === decrementCountType) {
- return { ...state, count: state.count - 1 };
- }
-
- return state;
-};
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/Counter.ts b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/Counter.ts
new file mode 100644
index 0000000000..8b9a29989d
--- /dev/null
+++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/Counter.ts
@@ -0,0 +1,48 @@
+import { Action, Reducer } from 'redux';
+
+// -----------------
+// STATE - This defines the type of data maintained in the Redux store.
+
+export interface CounterState {
+ count: number;
+}
+
+// -----------------
+// ACTIONS - These are serializable (hence replayable) descriptions of state transitions.
+// They do not themselves have any side-effects; they just describe something that is going to happen.
+// Use @typeName and isActionType for type detection that works even after serialization/deserialization.
+
+export interface IncrementCountAction { type: 'INCREMENT_COUNT' }
+export interface DecrementCountAction { type: 'DECREMENT_COUNT' }
+
+// Declare a 'discriminated union' type. This guarantees that all references to 'type' properties contain one of the
+// declared type strings (and not any other arbitrary string).
+export type KnownAction = IncrementCountAction | DecrementCountAction;
+
+// ----------------
+// ACTION CREATORS - These are functions exposed to UI components that will trigger a state transition.
+// They don't directly mutate state, but they can have external side-effects (such as loading data).
+
+export const actionCreators = {
+ increment: () =>
{ type: 'INCREMENT_COUNT' },
+ decrement: () => { type: 'DECREMENT_COUNT' }
+};
+
+// ----------------
+// REDUCER - For a given state and action, returns the new state. To support time travel, this must not mutate the old state.
+
+export const reducer: Reducer = (state: CounterState | undefined, incomingAction: Action): CounterState => {
+ if (state === undefined) {
+ return { count: 0 };
+ }
+
+ const action = incomingAction as KnownAction;
+ switch (action.type) {
+ case 'INCREMENT_COUNT':
+ return { count: state.count + 1 };
+ case 'DECREMENT_COUNT':
+ return { count: state.count - 1 };
+ default:
+ return state;
+ }
+};
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/WeatherForecasts.js b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/WeatherForecasts.js
deleted file mode 100644
index 6f0049030f..0000000000
--- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/WeatherForecasts.js
+++ /dev/null
@@ -1,43 +0,0 @@
-const requestWeatherForecastsType = 'REQUEST_WEATHER_FORECASTS';
-const receiveWeatherForecastsType = 'RECEIVE_WEATHER_FORECASTS';
-const initialState = { forecasts: [], isLoading: false };
-
-export const actionCreators = {
- requestWeatherForecasts: startDateIndex => async (dispatch, getState) => {
- if (startDateIndex === getState().weatherForecasts.startDateIndex) {
- // Don't issue a duplicate request (we already have or are loading the requested data)
- return;
- }
-
- dispatch({ type: requestWeatherForecastsType, startDateIndex });
-
- const url = `api/SampleData/WeatherForecasts?startDateIndex=${startDateIndex}`;
- const response = await fetch(url);
- const forecasts = await response.json();
-
- dispatch({ type: receiveWeatherForecastsType, startDateIndex, forecasts });
- }
-};
-
-export const reducer = (state, action) => {
- state = state || initialState;
-
- if (action.type === requestWeatherForecastsType) {
- return {
- ...state,
- startDateIndex: action.startDateIndex,
- isLoading: true
- };
- }
-
- if (action.type === receiveWeatherForecastsType) {
- return {
- ...state,
- startDateIndex: action.startDateIndex,
- forecasts: action.forecasts,
- isLoading: false
- };
- }
-
- return state;
-};
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/WeatherForecasts.ts b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/WeatherForecasts.ts
new file mode 100644
index 0000000000..e43f996075
--- /dev/null
+++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/WeatherForecasts.ts
@@ -0,0 +1,91 @@
+import { Action, Reducer } from 'redux';
+import { AppThunkAction } from './';
+
+// -----------------
+// STATE - This defines the type of data maintained in the Redux store.
+
+export interface WeatherForecastsState {
+ isLoading: boolean;
+ startDateIndex?: number;
+ forecasts: WeatherForecast[];
+}
+
+export interface WeatherForecast {
+ dateFormatted: string;
+ temperatureC: number;
+ temperatureF: number;
+ summary: string;
+}
+
+// -----------------
+// ACTIONS - These are serializable (hence replayable) descriptions of state transitions.
+// They do not themselves have any side-effects; they just describe something that is going to happen.
+
+interface RequestWeatherForecastsAction {
+ type: 'REQUEST_WEATHER_FORECASTS';
+ startDateIndex: number;
+}
+
+interface ReceiveWeatherForecastsAction {
+ type: 'RECEIVE_WEATHER_FORECASTS';
+ startDateIndex: number;
+ forecasts: WeatherForecast[];
+}
+
+// Declare a 'discriminated union' type. This guarantees that all references to 'type' properties contain one of the
+// declared type strings (and not any other arbitrary string).
+type KnownAction = RequestWeatherForecastsAction | ReceiveWeatherForecastsAction;
+
+// ----------------
+// ACTION CREATORS - These are functions exposed to UI components that will trigger a state transition.
+// They don't directly mutate state, but they can have external side-effects (such as loading data).
+
+export const actionCreators = {
+ requestWeatherForecasts: (startDateIndex: number): AppThunkAction => (dispatch, getState) => {
+ // Only load data if it's something we don't already have (and are not already loading)
+ const appState = getState();
+ if (appState && appState.weatherForecasts && startDateIndex !== appState.weatherForecasts.startDateIndex) {
+ fetch(`api/SampleData/WeatherForecasts?startDateIndex=${startDateIndex}`)
+ .then(response => response.json() as Promise)
+ .then(data => {
+ dispatch({ type: 'RECEIVE_WEATHER_FORECASTS', startDateIndex: startDateIndex, forecasts: data });
+ });
+
+ dispatch({ type: 'REQUEST_WEATHER_FORECASTS', startDateIndex: startDateIndex });
+ }
+ }
+};
+
+// ----------------
+// REDUCER - For a given state and action, returns the new state. To support time travel, this must not mutate the old state.
+
+const unloadedState: WeatherForecastsState = { forecasts: [], isLoading: false };
+
+export const reducer: Reducer = (state: WeatherForecastsState | undefined, incomingAction: Action): WeatherForecastsState => {
+ if (state === undefined) {
+ return unloadedState;
+ }
+
+ const action = incomingAction as KnownAction;
+ switch (action.type) {
+ case 'REQUEST_WEATHER_FORECASTS':
+ return {
+ startDateIndex: action.startDateIndex,
+ forecasts: state.forecasts,
+ isLoading: true
+ };
+ case 'RECEIVE_WEATHER_FORECASTS':
+ // Only accept the incoming data if it matches the most recent request. This ensures we correctly
+ // handle out-of-order responses.
+ if (action.startDateIndex === state.startDateIndex) {
+ return {
+ startDateIndex: action.startDateIndex,
+ forecasts: action.forecasts,
+ isLoading: false
+ };
+ }
+ break;
+ }
+
+ return state;
+};
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/configureStore.js b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/configureStore.js
deleted file mode 100644
index 7da6766bda..0000000000
--- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/configureStore.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { applyMiddleware, combineReducers, compose, createStore } from 'redux';
-import thunk from 'redux-thunk';
-import { routerReducer, routerMiddleware } from 'react-router-redux';
-import * as Counter from './Counter';
-import * as WeatherForecasts from './WeatherForecasts';
-
-export default function configureStore (history, initialState) {
- const reducers = {
- counter: Counter.reducer,
- weatherForecasts: WeatherForecasts.reducer
- };
-
- const middleware = [
- thunk,
- routerMiddleware(history)
- ];
-
- // In development, use the browser's Redux dev tools extension if installed
- const enhancers = [];
- const isDevelopment = process.env.NODE_ENV === 'development';
- if (isDevelopment && typeof window !== 'undefined' && window.devToolsExtension) {
- enhancers.push(window.devToolsExtension());
- }
-
- const rootReducer = combineReducers({
- ...reducers,
- routing: routerReducer
- });
-
- return createStore(
- rootReducer,
- initialState,
- compose(applyMiddleware(...middleware), ...enhancers)
- );
-}
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/configureStore.ts b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/configureStore.ts
new file mode 100644
index 0000000000..06928a6241
--- /dev/null
+++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/configureStore.ts
@@ -0,0 +1,29 @@
+import { applyMiddleware, combineReducers, compose, createStore, Reducer } from 'redux';
+import thunk from 'redux-thunk';
+import { connectRouter, routerMiddleware } from 'connected-react-router';
+import { History } from 'history';
+import { ApplicationState, reducers } from './';
+
+export default function configureStore(history: History, initialState?: ApplicationState) {
+ const middleware = [
+ thunk,
+ routerMiddleware(history)
+ ];
+
+ const rootReducer = combineReducers({
+ ...reducers,
+ router: connectRouter(history)
+ });
+
+ const enhancers = [];
+ const windowIfDefined = typeof window === 'undefined' ? null : window as any;
+ if (windowIfDefined && windowIfDefined.__REDUX_DEVTOOLS_EXTENSION__) {
+ enhancers.push(windowIfDefined.__REDUX_DEVTOOLS_EXTENSION__());
+ }
+
+ return createStore(
+ rootReducer,
+ initialState,
+ compose(applyMiddleware(...middleware), ...enhancers)
+ );
+}
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/index.ts b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/index.ts
new file mode 100644
index 0000000000..a5f7c05b39
--- /dev/null
+++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/store/index.ts
@@ -0,0 +1,22 @@
+import * as WeatherForecasts from './WeatherForecasts';
+import * as Counter from './Counter';
+
+// The top-level state object
+export interface ApplicationState {
+ counter: Counter.CounterState | undefined;
+ weatherForecasts: WeatherForecasts.WeatherForecastsState | undefined;
+}
+
+// Whenever an action is dispatched, Redux will update each top-level application state property using
+// the reducer with the matching name. It's important that the names match exactly, and that the reducer
+// acts on the corresponding ApplicationState property type.
+export const reducers = {
+ counter: Counter.reducer,
+ weatherForecasts: WeatherForecasts.reducer
+};
+
+// This type can be used as a hint on action creators so that its 'dispatch' and 'getState' params are
+// correctly typed to match your store.
+export interface AppThunkAction {
+ (dispatch: (action: TAction) => void, getState: () => ApplicationState): void;
+}
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/tsconfig.json b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/tsconfig.json
new file mode 100644
index 0000000000..8a6b4c16b4
--- /dev/null
+++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "allowJs": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "preserve",
+ "lib": [
+ "es2015",
+ "dom"
+ ],
+ "skipLibCheck": false
+ },
+ "include": [
+ "src"
+ ]
+}