Skip to content

Commit cf7a519

Browse files
Working React+Redux template
1 parent ec93377 commit cf7a519

26 files changed

Lines changed: 1475 additions & 131 deletions
Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
1-
import 'bootstrap';
2-
import 'bootstrap/dist/css/bootstrap.css';
31
import './css/site.css';
4-
2+
import 'bootstrap/dist/css/bootstrap.css';
53
import * as React from 'react';
64
import * as ReactDOM from 'react-dom';
75
import { browserHistory, Router } from 'react-router';
6+
import { Provider } from 'react-redux';
7+
import { syncHistoryWithStore } from 'react-router-redux';
88
import routes from './routes';
9+
import configureStore from './configureStore';
10+
import { ApplicationState } from './store';
11+
12+
// Get the application-wide store instance, prepopulating with state from the server where available.
13+
const initialState = (window as any).initialReduxState as ApplicationState;
14+
const store = configureStore(initialState);
15+
const history = syncHistoryWithStore(browserHistory, store);
916

1017
// This code starts up the React app when it runs in a browser. It sets up the routing configuration
1118
// and injects the app into a DOM element.
1219
ReactDOM.render(
13-
<Router history={ browserHistory } children={ routes } />,
20+
<Provider store={ store }>
21+
<Router history={ history } children={ routes } />
22+
</Provider>,
1423
document.getElementById('react-app')
1524
);
Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,27 @@
11
import * as React from 'react';
2+
import { Link } from 'react-router';
3+
import { provide } from 'redux-typed';
4+
import { ApplicationState } from '../store';
5+
import * as CounterStore from '../store/Counter';
26

3-
interface CounterState {
4-
currentCount: number;
5-
}
6-
7-
export class Counter extends React.Component<any, CounterState> {
8-
constructor() {
9-
super();
10-
this.state = { currentCount: 0 };
11-
}
12-
7+
class Counter extends React.Component<CounterProps, void> {
138
public render() {
149
return <div>
1510
<h1>Counter</h1>
1611

1712
<p>This is a simple example of a React component.</p>
1813

19-
<p>Current count: <strong>{ this.state.currentCount }</strong></p>
14+
<p>Current count: <strong>{ this.props.count }</strong></p>
2015

21-
<button onClick={ () => { this.incrementCounter() } }>Increment</button>
16+
<button onClick={ () => { this.props.increment() } }>Increment</button>
2217
</div>;
2318
}
24-
25-
incrementCounter() {
26-
this.setState({
27-
currentCount: this.state.currentCount + 1
28-
});
29-
}
3019
}
20+
21+
// Build the CounterProps type, which allows the component to be strongly typed
22+
const provider = provide(
23+
(state: ApplicationState) => state.counter, // Select which part of global state maps to this component
24+
CounterStore.actionCreators // Select which action creators should be exposed to this component
25+
);
26+
type CounterProps = typeof provider.allProps;
27+
export default provider.connect(Counter);
Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,36 @@
11
import * as React from 'react';
2+
import { Link } from 'react-router';
3+
import { provide } from 'redux-typed';
4+
import { ApplicationState } from '../store';
5+
import * as WeatherForecastsState from '../store/WeatherForecasts';
26

3-
interface FetchDataExampleState {
4-
forecasts: WeatherForecast[];
5-
loading: boolean;
7+
interface RouteParams {
8+
startDateIndex: string;
69
}
710

8-
export class FetchData extends React.Component<any, FetchDataExampleState> {
9-
constructor() {
10-
super();
11-
this.state = { forecasts: [], loading: true };
12-
13-
fetch('/api/SampleData/WeatherForecasts')
14-
.then(response => response.json())
15-
.then((data: WeatherForecast[]) => {
16-
this.setState({ forecasts: data, loading: false });
17-
});
11+
class FetchData extends React.Component<WeatherForecastProps, void> {
12+
componentWillMount() {
13+
// This method runs when the component is first added to the page
14+
let startDateIndex = parseInt(this.props.params.startDateIndex) || 0;
15+
this.props.requestWeatherForecasts(startDateIndex);
16+
}
17+
18+
componentWillReceiveProps(nextProps: WeatherForecastProps) {
19+
// This method runs when incoming props (e.g., route params) change
20+
let startDateIndex = parseInt(nextProps.params.startDateIndex) || 0;
21+
this.props.requestWeatherForecasts(startDateIndex);
1822
}
1923

2024
public render() {
21-
let contents = this.state.loading
22-
? <p><em>Loading...</em></p>
23-
: FetchData.renderForecastsTable(this.state.forecasts);
24-
2525
return <div>
2626
<h1>Weather forecast</h1>
27-
<p>This component demonstrates fetching data from the server.</p>
28-
{ contents }
29-
<p>For more sophisticated applications, consider an architecture such as Redux or Flux for managing state. You can generate an ASP.NET Core application with React and Redux using <code>dotnet new aspnet/spa/reactredux</code> instead of using this template.</p>
27+
<p>This component demonstrates fetching data from the server and working with URL parameters.</p>
28+
{ this.renderForecastsTable() }
29+
{ this.renderPagination() }
3030
</div>;
3131
}
3232

33-
private static renderForecastsTable(forecasts: WeatherForecast[]) {
33+
private renderForecastsTable() {
3434
return <table className='table'>
3535
<thead>
3636
<tr>
@@ -41,7 +41,7 @@ export class FetchData extends React.Component<any, FetchDataExampleState> {
4141
</tr>
4242
</thead>
4343
<tbody>
44-
{forecasts.map(forecast =>
44+
{this.props.forecasts.map(forecast =>
4545
<tr key={ forecast.dateFormatted }>
4646
<td>{ forecast.dateFormatted }</td>
4747
<td>{ forecast.temperatureC }</td>
@@ -52,11 +52,23 @@ export class FetchData extends React.Component<any, FetchDataExampleState> {
5252
</tbody>
5353
</table>;
5454
}
55-
}
55+
56+
private renderPagination() {
57+
let prevStartDateIndex = this.props.startDateIndex - 5;
58+
let nextStartDateIndex = this.props.startDateIndex + 5;
5659

57-
interface WeatherForecast {
58-
dateFormatted: string;
59-
temperatureC: number;
60-
temperatureF: number;
61-
summary: string;
60+
return <p className='clearfix text-center'>
61+
<Link className='btn btn-default pull-left' to={ `/fetchdata/${ prevStartDateIndex }` }>Previous</Link>
62+
<Link className='btn btn-default pull-right' to={ `/fetchdata/${ nextStartDateIndex }` }>Next</Link>
63+
{ this.props.isLoading ? <span>Loading...</span> : [] }
64+
</p>;
65+
}
6266
}
67+
68+
// Build the WeatherForecastProps type, which allows the component to be strongly typed
69+
const provider = provide(
70+
(state: ApplicationState) => state.weatherForecasts, // Select which part of global state maps to this component
71+
WeatherForecastsState.actionCreators // Select which action creators should be exposed to this component
72+
).withExternalProps<{ params: RouteParams }>(); // Also include a 'params' property on WeatherForecastProps
73+
type WeatherForecastProps = typeof provider.allProps;
74+
export default provider.connect(FetchData);
Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import * as React from 'react';
22

3-
export class Home extends React.Component<any, void> {
3+
export default class Home extends React.Component<any, void> {
44
public render() {
55
return <div>
66
<h1>Hello, world!</h1>
77
<p>Welcome to your new single-page application, built with:</p>
88
<ul>
99
<li><a href='https://get.asp.net/'>ASP.NET Core</a> and <a href='https://msdn.microsoft.com/en-us/library/67ef8sbd.aspx'>C#</a> for cross-platform server-side code</li>
10-
<li><a href='https://facebook.github.io/react/'>React</a> and <a href='http://www.typescriptlang.org/'>TypeScript</a> for client-side code</li>
10+
<li><a href='https://facebook.github.io/react/'>React</a>, <a href='http://redux.js.org'>Redux</a>, and <a href='http://www.typescriptlang.org/'>TypeScript</a> for client-side code</li>
1111
<li><a href='https://webpack.github.io/'>Webpack</a> for building and bundling client-side resources</li>
1212
<li><a href='http://getbootstrap.com/'>Bootstrap</a> for layout and styling</li>
1313
</ul>
@@ -18,11 +18,6 @@ export class Home extends React.Component<any, void> {
1818
<li><strong>Hot module replacement</strong>. In development mode, you don't even need to reload the page after making most changes. Within seconds of saving changes to files, rebuilt CSS and React components will be injected directly into your running application, preserving its live state.</li>
1919
<li><strong>Efficient production builds</strong>. In production mode, development-time features are disabled, and the <code>webpack</code> build tool produces minified static CSS and JavaScript files.</li>
2020
</ul>
21-
<h4>Going further</h4>
22-
<p>
23-
For larger applications, or for server-side prerendering (i.e., for <em>isomorphic</em> or <em>universal</em> applications), you should consider using a Flux/Redux-like architecture.
24-
You can generate an ASP.NET Core application with React and Redux using <code>dotnet new aspnet/spa/reactredux</code> instead of using this template.
25-
</p>
2621
</div>;
2722
}
2823
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
2+
import * as thunkModule from 'redux-thunk';
3+
import { routerReducer } from 'react-router-redux';
4+
import * as Store from './store';
5+
import { typedToPlain } from 'redux-typed';
6+
7+
export default function configureStore(initialState?: Store.ApplicationState) {
8+
// Build middleware. These are functions that can process the actions before they reach the store.
9+
const thunk = (thunkModule as any).default; // Workaround for TypeScript not importing thunk module as expected
10+
const devToolsExtension = (window as any).devToolsExtension; // If devTools is installed, connect to it
11+
const createStoreWithMiddleware = compose(
12+
applyMiddleware(thunk, typedToPlain),
13+
devToolsExtension ? devToolsExtension() : f => f
14+
)(createStore);
15+
16+
// Combine all reducers and instantiate the app-wide store instance
17+
const allReducers = buildRootReducer(Store.reducers);
18+
const store = createStoreWithMiddleware(allReducers, initialState) as Redux.Store;
19+
20+
// Enable Webpack hot module replacement for reducers
21+
if (module.hot) {
22+
module.hot.accept('./store', () => {
23+
const nextRootReducer = require<typeof Store>('./store');
24+
store.replaceReducer(buildRootReducer(nextRootReducer.reducers));
25+
});
26+
}
27+
28+
return store;
29+
}
30+
31+
function buildRootReducer(allReducers) {
32+
return combineReducers(Object.assign({}, allReducers, { routing: routerReducer })) as Redux.Reducer;
33+
}
Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import * as React from 'react';
22
import { Router, Route, HistoryBase } from 'react-router';
33
import { Layout } from './components/Layout';
4-
import { Home } from './components/Home';
5-
import { FetchData } from './components/FetchData';
6-
import { Counter } from './components/Counter';
4+
import Home from './components/Home';
5+
import FetchData from './components/FetchData';
6+
import Counter from './components/Counter';
77

88
export default <Route component={ Layout }>
99
<Route path='/' components={{ body: Home }} />
1010
<Route path='/counter' components={{ body: Counter }} />
11-
<Route path='/fetchdata' components={{ body: FetchData }} />
11+
<Route path='/fetchdata' components={{ body: FetchData }}>
12+
<Route path='(:startDateIndex)' /> { /* Optional route segment that does not affect NavMenu highlighting */ }
13+
</Route>
1214
</Route>;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { typeName, isActionType, Action, Reducer } from 'redux-typed';
2+
import { ActionCreator } from './';
3+
4+
// -----------------
5+
// STATE - This defines the type of data maintained in the Redux store.
6+
7+
export interface CounterState {
8+
count: number;
9+
}
10+
11+
// -----------------
12+
// ACTIONS - These are serializable (hence replayable) descriptions of state transitions.
13+
// They do not themselves have any side-effects; they just describe something that is going to happen.
14+
// Use @typeName and isActionType for type detection that works even after serialization/deserialization.
15+
16+
@typeName("INCREMENT_COUNT")
17+
class IncrementCount extends Action {
18+
}
19+
20+
// ----------------
21+
// ACTION CREATORS - These are functions exposed to UI components that will trigger a state transition.
22+
// They don't directly mutate state, but they can have external side-effects (such as loading data).
23+
24+
export const actionCreators = {
25+
increment: (): ActionCreator => (dispatch, getState) => {
26+
dispatch(new IncrementCount());
27+
}
28+
};
29+
30+
// ----------------
31+
// REDUCER - For a given state and action, returns the new state. To support time travel, this must not mutate the old state.
32+
export const reducer: Reducer<CounterState> = (state, action) => {
33+
if (isActionType(action, IncrementCount)) {
34+
return { count: state.count + 1 };
35+
}
36+
37+
// For unrecognized actions (or in cases where actions have no effect), must return the existing state
38+
// (or default initial state if none was supplied)
39+
return state || { count: 0 };
40+
};
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { fetch } from 'domain-task/fetch';
2+
import { typeName, isActionType, Action, Reducer } from 'redux-typed';
3+
import { ActionCreator } from './';
4+
5+
// -----------------
6+
// STATE - This defines the type of data maintained in the Redux store.
7+
8+
export interface WeatherForecastsState {
9+
isLoading: boolean;
10+
startDateIndex: number;
11+
forecasts: WeatherForecast[];
12+
}
13+
14+
export interface WeatherForecast {
15+
dateFormatted: string;
16+
temperatureC: number;
17+
temperatureF: number;
18+
summary: string;
19+
}
20+
21+
// -----------------
22+
// ACTIONS - These are serializable (hence replayable) descriptions of state transitions.
23+
// They do not themselves have any side-effects; they just describe something that is going to happen.
24+
// Use @typeName and isActionType for type detection that works even after serialization/deserialization.
25+
26+
@typeName("REQUEST_WEATHER_FORECASTS")
27+
class RequestWeatherForecasts extends Action {
28+
constructor(public startDateIndex: number) {
29+
super();
30+
}
31+
}
32+
33+
@typeName("RECEIVE_WEATHER_FORECASTS")
34+
class ReceiveWeatherForecasts extends Action {
35+
constructor(public startDateIndex: number, public forecasts: WeatherForecast[]) {
36+
super();
37+
}
38+
}
39+
40+
// ----------------
41+
// ACTION CREATORS - These are functions exposed to UI components that will trigger a state transition.
42+
// They don't directly mutate state, but they can have external side-effects (such as loading data).
43+
44+
export const actionCreators = {
45+
requestWeatherForecasts: (startDateIndex: number): ActionCreator => (dispatch, getState) => {
46+
// Only load data if it's something we don't already have (and are not already loading)
47+
if (startDateIndex !== getState().weatherForecasts.startDateIndex) {
48+
fetch(`/api/SampleData/WeatherForecasts?startDateIndex=${ startDateIndex }`)
49+
.then(response => response.json())
50+
.then((data: WeatherForecast[]) => {
51+
dispatch(new ReceiveWeatherForecasts(startDateIndex, data));
52+
});
53+
54+
dispatch(new RequestWeatherForecasts(startDateIndex));
55+
}
56+
}
57+
};
58+
59+
// ----------------
60+
// REDUCER - For a given state and action, returns the new state. To support time travel, this must not mutate the old state.
61+
const unloadedState: WeatherForecastsState = { startDateIndex: null, forecasts: [], isLoading: false };
62+
export const reducer: Reducer<WeatherForecastsState> = (state, action) => {
63+
if (isActionType(action, RequestWeatherForecasts)) {
64+
return { startDateIndex: action.startDateIndex, isLoading: true, forecasts: state.forecasts };
65+
} else if (isActionType(action, ReceiveWeatherForecasts)) {
66+
// Only accept the incoming data if it matches the most recent request. This ensures we correctly
67+
// handle out-of-order responses.
68+
if (action.startDateIndex === state.startDateIndex) {
69+
return { startDateIndex: action.startDateIndex, forecasts: action.forecasts, isLoading: false };
70+
}
71+
}
72+
73+
// For unrecognized actions (or in cases where actions have no effect), must return the existing state
74+
// (or default initial state if none was supplied)
75+
return state || unloadedState;
76+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ActionCreatorGeneric } from 'redux-typed';
2+
import * as WeatherForecasts from './WeatherForecasts';
3+
import * as Counter from './Counter';
4+
5+
// The top-level state object
6+
export interface ApplicationState {
7+
counter: Counter.CounterState,
8+
weatherForecasts: WeatherForecasts.WeatherForecastsState
9+
}
10+
11+
// Whenever an action is dispatched, Redux will update each top-level application state property using
12+
// the reducer with the matching name. It's important that the names match exactly, and that the reducer
13+
// acts on the corresponding ApplicationState property type.
14+
export const reducers = {
15+
counter: Counter.reducer,
16+
weatherForecasts: WeatherForecasts.reducer
17+
};
18+
19+
// This type can be used as a hint on action creators so that its 'dispatch' and 'getState' params are
20+
// correctly typed to match your store.
21+
export type ActionCreator = ActionCreatorGeneric<ApplicationState>;

0 commit comments

Comments
 (0)