Add data-loading example to ReactSpa template, and remove server-side rendering (because you really need Redux/Flux or similar for that to make sense)

This commit is contained in:
SteveSandersonMS
2016-02-24 18:32:31 +00:00
parent a6ea8b5101
commit c668387dac
14 changed files with 214 additions and 58 deletions

View File

@@ -1,34 +0,0 @@
import * as React from 'react';
import { renderToString } from 'react-dom/server';
import { match, RouterContext } from 'react-router';
import createMemoryHistory from 'history/lib/createMemoryHistory';
import { routes } from './routes';
// The 'asp-prerender-module' tag helper invokes the following function when the React app is to
// be prerendered on the server. It runs asynchronously, and issues a callback with the React app's
// initial HTML and any other state variables.
export default function (params: any): Promise<{ html: string }> {
return new Promise<{ html: string, globals: { [key: string]: any } }>((resolve, reject) => {
// Match the incoming request against the list of client-side routes, and reject if there was no match
match({ routes, location: params.location }, (error, redirectLocation, renderProps: any) => {
if (error) {
throw error;
}
// Build an instance of the application and perform an initial render.
// This will cause any async tasks (e.g., data access) to begin.
const history = createMemoryHistory(params.url);
const app = <RouterContext {...renderProps} />;
renderToString(app);
// Once the tasks are done, we can perform the final render
params.domainTasks.then(() => {
resolve({
html: renderToString(app),
globals: { /* Supply any other JSON-serializable data you want to make available on the client */ }
});
}, reject); // Also propagate any errors back into the host application
});
});
}

View File

@@ -1,13 +0,0 @@
import * as React from 'react';
export class About extends React.Component<void, void> {
public render() {
return <div>
<h1>About</h1>
<p>This is another component.</p>
<p>It's here to demonstrate navigation.</p>
</div>;
}
}

View File

@@ -0,0 +1,62 @@
import * as React from 'react';
interface FetchDataExampleState {
forecasts: WeatherForecast[];
loading: boolean;
}
export class FetchData extends React.Component<void, FetchDataExampleState> {
constructor() {
super();
this.state = { forecasts: [], loading: true };
fetch('/api/SampleData/WeatherForecasts')
.then(response => response.json())
.then((data: WeatherForecast[]) => {
this.setState({ forecasts: data, loading: false });
});
}
public render() {
let contents = this.state.loading
? <p><em>Loading...</em></p>
: FetchData.renderForecastsTable(this.state.forecasts);
return <div>
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
{ contents }
<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>
</div>;
}
private static renderForecastsTable(forecasts: WeatherForecast[]) {
return <table className="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
{forecasts.map(forecast =>
<tr key={ forecast.dateFormatted }>
<td>{ forecast.dateFormatted }</td>
<td>{ forecast.temperatureC }</td>
<td>{ forecast.temperatureF }</td>
<td>{ forecast.summary }</td>
</tr>
)}
</tbody>
</table>;
}
}
interface WeatherForecast {
dateFormatted: string;
temperatureC: number;
temperatureF: number;
summary: string;
}

View File

@@ -13,12 +13,16 @@ export class Home extends React.Component<void, void> {
</ul> </ul>
<p>To help you get started, we've also set up:</p> <p>To help you get started, we've also set up:</p>
<ul> <ul>
<li><strong>Client-side navigation</strong>. For example, click <em>About</em> then <em>Back</em> to return here.</li> <li><strong>Client-side navigation</strong>. For example, click <em>Counter</em> then <em>Back</em> to return here.</li>
<li><strong>Server-side prerendering</strong>. For optimal performance, your React application is first executed on the server. The resulting HTML and state is then transferred to the client to continue execution. This is also known as being an <em>isomorphic</em> or <em>universal</em> application.</li>
<li><strong>Webpack dev middleware</strong>. In development mode, there's no need to run the <code>webpack</code> build tool. Your client-side resources are dynamically built on demand. Updates are available as soon as you modify any file.</li> <li><strong>Webpack dev middleware</strong>. In development mode, there's no need to run the <code>webpack</code> build tool. Your client-side resources are dynamically built on demand. Updates are available as soon as you modify any file.</li>
<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> <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>
<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> <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>
</ul> </ul>
<h4>Going further</h4>
<p>
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.
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>
</div>; </div>;
} }
} }

View File

@@ -23,13 +23,13 @@ export class NavMenu extends React.Component<void, void> {
</Link> </Link>
</li> </li>
<li> <li>
<Link to={ 'about' } activeClassName='active'> <Link to={ '/counter' } activeClassName='active'>
<span className="glyphicon glyphicon-info-sign"></span> About <span className="glyphicon glyphicon-education"></span> Counter
</Link> </Link>
</li> </li>
<li> <li>
<Link to={ '/counter' } activeClassName='active'> <Link to={ '/fetchdata' } activeClassName='active'>
<span className="glyphicon glyphicon-education"></span> Counter <span className="glyphicon glyphicon-th-list"></span> Fetch data
</Link> </Link>
</li> </li>
</ul> </ul>

View File

@@ -2,11 +2,11 @@ import * as React from 'react';
import { Router, Route, HistoryBase } from 'react-router'; import { Router, Route, HistoryBase } from 'react-router';
import { Layout } from './components/Layout'; import { Layout } from './components/Layout';
import { Home } from './components/Home'; import { Home } from './components/Home';
import { About } from './components/About'; import { FetchData } from './components/FetchData';
import { Counter } from './components/Counter'; import { Counter } from './components/Counter';
export const routes = <Route component={ Layout }> export const routes = <Route component={ Layout }>
<Route path="/" components={{ body: Home }} /> <Route path="/" components={{ body: Home }} />
<Route path="/about" components={{ body: About }} />
<Route path="/counter" components={{ body: Counter }} /> <Route path="/counter" components={{ body: Counter }} />
<Route path="/fetchdata" components={{ body: FetchData }} />
</Route>; </Route>;

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;
namespace WebApplicationBasic.Controllers
{
[Route("api/[controller]")]
public class SampleDataController : Controller
{
private static string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
[HttpGet, Route("[action]")]
public IEnumerable<WeatherForecast> WeatherForecasts()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
DateFormatted = DateTime.Now.AddDays(index).ToString("d"),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
});
}
public class WeatherForecast
{
public string DateFormatted { get; set; }
public int TemperatureC { get; set; }
public string Summary { get; set; }
public int TemperatureF
{
get
{
return 32 + (int)(this.TemperatureC / 0.5556);
}
}
}
}
}

View File

@@ -8,6 +8,7 @@ using Microsoft.AspNet.SpaServices.Webpack;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Serialization;
namespace WebApplicationBasic namespace WebApplicationBasic
{ {
@@ -28,7 +29,10 @@ namespace WebApplicationBasic
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
// Add framework services. // Add framework services.
services.AddMvc(); services.AddMvc().AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

View File

@@ -2,7 +2,7 @@
ViewData["Title"] = "Home Page"; ViewData["Title"] = "Home Page";
} }
<div id="react-app" asp-prerender-module="ClientApp/boot-server">Loading...</div> <div id="react-app">Loading...</div>
@section scripts { @section scripts {
<script src="~/dist/main.js" asp-append-version="true"></script> <script src="~/dist/main.js" asp-append-version="true"></script>

View File

@@ -16,6 +16,9 @@
}, },
"react-router/history.d.ts": { "react-router/history.d.ts": {
"commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7" "commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7"
},
"whatwg-fetch/whatwg-fetch.d.ts": {
"commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7"
} }
} }
} }

View File

@@ -3,3 +3,4 @@
/// <reference path="react-router/history.d.ts" /> /// <reference path="react-router/history.d.ts" />
/// <reference path="react-router/react-router.d.ts" /> /// <reference path="react-router/react-router.d.ts" />
/// <reference path="react/react-dom.d.ts" /> /// <reference path="react/react-dom.d.ts" />
/// <reference path="whatwg-fetch/whatwg-fetch.d.ts" />

View File

@@ -0,0 +1,85 @@
// Type definitions for fetch API
// Project: https://github.com/github/fetch
// Definitions by: Ryan Graham <https://github.com/ryan-codingintrigue>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
declare class Request extends Body {
constructor(input: string|Request, init?:RequestInit);
method: string;
url: string;
headers: Headers;
context: string|RequestContext;
referrer: string;
mode: string|RequestMode;
credentials: string|RequestCredentials;
cache: string|RequestCache;
}
interface RequestInit {
method?: string;
headers?: HeaderInit|{ [index: string]: string };
body?: BodyInit;
mode?: string|RequestMode;
credentials?: string|RequestCredentials;
cache?: string|RequestCache;
}
declare enum RequestContext {
"audio", "beacon", "cspreport", "download", "embed", "eventsource", "favicon", "fetch",
"font", "form", "frame", "hyperlink", "iframe", "image", "imageset", "import",
"internal", "location", "manifest", "object", "ping", "plugin", "prefetch", "script",
"serviceworker", "sharedworker", "subresource", "style", "track", "video", "worker",
"xmlhttprequest", "xslt"
}
declare enum RequestMode { "same-origin", "no-cors", "cors" }
declare enum RequestCredentials { "omit", "same-origin", "include" }
declare enum RequestCache { "default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached" }
declare class Headers {
append(name: string, value: string): void;
delete(name: string):void;
get(name: string): string;
getAll(name: string): Array<string>;
has(name: string): boolean;
set(name: string, value: string): void;
}
declare class Body {
bodyUsed: boolean;
arrayBuffer(): Promise<ArrayBuffer>;
blob(): Promise<Blob>;
formData(): Promise<FormData>;
json(): Promise<any>;
json<T>(): Promise<T>;
text(): Promise<string>;
}
declare class Response extends Body {
constructor(body?: BodyInit, init?: ResponseInit);
error(): Response;
redirect(url: string, status: number): Response;
type: string|ResponseType;
url: string;
status: number;
ok: boolean;
statusText: string;
headers: Headers;
clone(): Response;
}
declare enum ResponseType { "basic", "cors", "default", "error", "opaque" }
interface ResponseInit {
status: number;
statusText?: string;
headers?: HeaderInit;
}
declare type HeaderInit = Headers|Array<string>;
declare type BodyInit = Blob|FormData|string;
declare type RequestInfo = Request|string;
interface Window {
fetch(url: string|Request, init?: RequestInit): Promise<Response>;
}
declare var fetch: typeof window.fetch;

View File

@@ -17,7 +17,7 @@ module.exports = merge({
] ]
}, },
entry: { entry: {
main: ['./ClientApp/boot-client.tsx'], main: ['./ClientApp/boot.tsx'],
vendor: ['bootstrap', 'bootstrap/dist/css/bootstrap.css', 'style-loader', 'jquery'] vendor: ['bootstrap', 'bootstrap/dist/css/bootstrap.css', 'style-loader', 'jquery']
}, },
output: { output: {