Following Part 1, we’re going to try to create an application with a pure React frontend and an F#-based WebApi backend.
I’m going to inject a bit of editorializing here: With client-side rendering, server-side rendering including ASP.NET MVC is dead. Why bother mixing cshtml and JavaScript when JavaScript alone will do it? This reduces the server-side code to business logic and data fetching returning JSON. Which further means that on the server we can use whatever technology is best suited to that job and switch it out as necessary. Once the back end is separate from the front end it’s easy enough to switch a C# WebAPI with F# or maybe with Erlang. We can use whatever is most efficient.
The F# Solution
We’re going to break with what we did in Part 1 and start a new F# solution. In Visual Studio, create a new solution called “ReactWebApiDemo” and under that a new F# WebAPI project called, again, “ReactWebApiDemo”. It will create a blank project with a controller called ValuesController
.
If you run the application it will open with a blank screen. You can navigate to the API to see it return some JSON, e.g. http://localhost:54111/api/values
Enabling static web content
We want the WebApi side producing JSON while our main site runs on static HTML and JavaScript. The project should have a folder called “wwwroot”. This is where our static pages go. Start by putting an “index.html” in that folder, e.g.
<html> <head> <meta charset="utf-8"> <title>Test</title> </head> <body> <h1>Test</h1> </body> </html>
Now if you run the solution and load the root URL, e.g. “http://localhost:54111/”, what do you see? The answer: nothing. We have to both enable static files (to get it to read from the folder) and default paths (to get it to recognize that / should load /index.html). To so this, modify Startup.fs
and add change the Configure method to do this:
member this.Configure(app: IApplicationBuilder, env: IHostingEnvironment) = app .UseDefaultFiles() .UseStaticFiles() .UseMvc() |> ignore
Adding our TypeScript, Less, React, etc. back in
If you have the files from part 1, copy
* .babelrc
* package.json
* package-lock.json
* tsconfig.json
* webpack.config.json
into the project folder and run
npm install
This will restore the modules.
Our files are going to wwwroot now, so first let’s establish this layout:
* wwwroot/
* wwwroot/components – Our React components
* wwwroot/css – Our CSS files
* wwwroot/js – Our scripts
* wwwroot/dist – Our bundled output
We need to change webpack.config.js with the entry and the output:
// Entry: a.k.a. "Entry Point", the JS file that will used to build the JavaScript dependency graph // If not specified, src/index.js is the default. entry: "./wwwroot/js/index.js", // Output: Where we'll output the build files. output: { // Path: The directory to which we'll write transformed files path: path.resolve(__dirname, 'wwwroot/dist'), // Filename: The name to which we'll write our bundled JavaScript. // If not specified, dist/main.js is the default. filename: 'main.js' },
Running webpack on build
One more thing we can do is make webpack run automatically every time we do a build. Edit your .fsproj file and add the following:
<Target Name="WebpackDebug" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug'"> <Message Importance="high" Text="Performing webpack build (Debug)..." /> <Exec Command="npm run debug" /> </Target> <Target Name="WebpackRelease" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Release'"> <Message Importance="high" Text="Performing webpack build (Release)..." /> <Exec Command="npm run release" /> </Target>
Stopping Visual Studio TypeScript compilation
By default, Visual Studio is going to try to compile our typescript and put it in its own “dist” folder. We’re using webpack so we don’t need it. To disable it, edit .fsproj and add:
<PropertyGroup> <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> </PropertyGroup>
Building and Running
Let’s put some code in input.js and run the project to make sure it works.
import style from '../css/site.less' function test() { console.log("test"); } window.onload = test;
Now we run it from Visual Studio, the browser opens, and we get… a page with script and style errors.
Content Security Policy
Here we’re going to run into a little problem with webpack and the application’s Content Security Polity. Using webpack’s “import style from ‘blah.css'” is an unsafe style import. Firefox or Chrome will show you a message like “The page’s settings blocked the loading of a resource at self (“style-src”).” and they’re right. A default Content Security Policy defines where you can load scripts and styles from and the default is “script-src ‘self’; style-src ‘self’;”. If we want to load CSS like this we need to override the security policy in .NET Core. I don’t actually recommend this, but until I find a better way, you can modify the Configure method like so:
member this.Configure(app: IApplicationBuilder, env: IHostingEnvironment) = let staticFileOptions = StaticFileOptions() staticFileOptions.OnPrepareResponse <- fun ( context ) -> context.Context.Response.Headers.Add( "Content-Security-Policy", StringValues( "script-src 'self'; " + "style-src 'self' 'unsafe-inline'; " + //"style-src 'self'; " + "img-src 'self'" ) ); app .UseDefaultFiles() .UseStaticFiles( staticFileOptions ) .UseMvc() |> ignore
This applies the content security policy to static files. To apply it to all files, use:
app .UseDefaultFiles() .UseStaticFiles() .UseMvc() .Use( fun ( context : HttpContext ) ( next : Func<Task> ) -> async { context.Response.Headers.Add( "Content-Security-Policy", StringValues( "script-src 'self'; " + "style-src 'self' 'unsafe-inline'; " + //"style-src 'self'; " + "img-src 'self'" ) ); return! next.Invoke() |> Async.AwaitTask } |> Async.StartAsTask :> Task ) |> ignore
At the time of writing this, webpack has the beginning of the concept of adding a nonce but they don’t seem to be doing it right in that it appears to be a static value rather than a per-request value making the application less secure.
Now with that temporarily solved…
React Router
We’re going to use react-router for page navigation. Add it with npm.
npm install react-router react-router-dom
- react-router – The core react-router library.
- react-router-dom – The DOM bindings (as opposed to React Native)
And for TypeScript compatibility:
npm install @types/react-router @types/react-router-dom
- @types/react-router – TypeScript types for react-router
- @types/react-router-dom – TypeScript types for react-router-dom
Let’s follow react-router’s basic example. Create a file called “App.jsx” and add the following code (taken verbatim from the given link):
import React from 'react' import { BrowserRouter as Router, Route, Link } from 'react-router-dom' const Home = () => ( <div> <h2>Home</h2> </div> ); const About = () => ( <div> <h2>About</h2> </div> ); const Topic = ({ match }) => ( <div> <h3>{match.params.topicId}</h3> </div> ); const Topics = ({ match }) => ( <div> <h2>Topics</h2> <ul> <li> Rendering with React </li> <li> Components </li> <li> Props v. State </li> </ul> ( <h3>Please select a topic.</h3> )} /> </div> ); const BasicExample = () => ( <div> <ul> <li>Home</li> <li>About</li> <li>Topics</li> </ul> <hr /> </div> ); export default BasicExample
Now change index.js to render our React routes:
import style from '../css/site.less' import React from 'react' import ReactDOM from 'react-dom' import BasicExample from './App' function test() { console.log("test"); ReactDOM.render(BasicExample(), document.getElementById('root')); } window.onload = test;
Since we’re rendering in the element “root” we need to define that in index.html:
<div id="root"> <h1>Test</h1> </div>
Run the page and you should see the example in action.
URL handling
Note that react-router will update the URL as you click on the various links. But you’ll also notice that if you bookmark one of the links, e.g. “/about” and try to go back to it, it’ll fail.
What we need to do is route all our URLs to the React page. We can do this with a URL rewrite rule in web.config:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.webServer> <rewrite> <rules> <rule name="React Routes" stopProcessing="true"> <match url="(.*)" /> <conditions logicalGrouping="MatchAll"> <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> <add input="{REQUEST_URI}" pattern="^/(api|scripts)" negate="true" /> </conditions> <action type="Rewrite" url="/" /> </rule> </rules> </rewrite> </system.webServer> </configuration>
Basically, everything that isn’t /api will get routed back to the React controller. This isn’t actually ideal – it also redirects scripts and images. We have a couple of options:
* We can ensure all our router paths have some distinct prefix and route only those. Imagine we put all our route paths under /r/ then we could do: (note "^" means "start of the path").
.
* We could exclude other paths, like /images, the same way we exclude /api. e.g.
Using TSX instead of JSX
If we want to use TypeScript instead of JavaScript we need to convert App.jsx to App.tsx. The converted example looks like this:
import * as React from 'react' import * as ReactRouter from 'react-router' import { BrowserRouter as Router, Route, Link } from 'react-router-dom' const Home = () => ( <div> <h2>Home TS</h2> </div> ); const About = () => ( <div> <h2>About TS</h2> </div> ); const Topic = ({ match } : { match: ReactRouter.match } ) => ( <div> <h3>{match.params.topicId}</h3> </div> ); const Topics = ({ match }: { match: ReactRouter.match }) => ( <div> <h2>Topics</h2> <ul> <li> Rendering with React </li> <li> Components </li> <li> Props v. State </li> </ul> ( <h3>Please select a topic.</h3> )} /> </div> ); const BasicExample = () => ( <div> <ul> <li>Home TS</li> <li>About TS</li> <li>Topics TS</li> </ul> <hr /> </div> ); export default BasicExample
The main change is to convert untyped parameters like Topic = ({ match })
to typed parameters like Topic = ({ match } : { match: ReactRouter.match } )
where match<P>
is a type found in @types/react-router.
Fetching data from the API
If you created the default F# project, you will already have a ValuesController
that supports a Get
returning an array:
[<Route("api/[controller]")>] type ValuesController () = inherit Controller() [<HttpGet>] member this.Get() = [|"value1"; "value2"; "value A"; "value B"|]
The usual way, I’m told, for a React component to load values via AJAX is in componentDidMount
. Let’s change our topics to fetch data from the API. It can’t be an arrow function anymore.
class Topics extends React.Component<any, { values: any[] }>{ constructor(props: any) { super(props); console.log("Topics.ctor"); this.state = { values: [] }; } public componentDidMount() { console.log("componentDidMount"); fetch("/api/values") .then((response) => { console.log(" got response " + response); return response.json(); }) .then((json) => { console.log("got json " + json); this.setState({ values: json }); }) ; } public render() { if (null === this.state) return null; return <div> <h2>Topics</h2> <ul> {this.state.values.map(function (value: any, index: number) { return <li>{value}</li>; })} </ul> </div> ; } }
And that’s it. When we load the /topic URL we’ll see the list from our API.