PageRenderTime 50ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/Blog/App_Data/Posts/96,2016,3,15,23,55,0,0,Bridge,React.txt

https://bitbucket.org/DanRoberts/blog
Plain Text | 1095 lines | 895 code | 200 blank | 0 comment | 0 complexity | 885c8b28bc069796ff39d43628e84049 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. ## Writing React apps using Bridge.NET - The Dan Way (from first principles)
  2. *(This is part one of a three part series, each post is longer than the last so strap yourself in if you're thinking of playing along - hopefully you'll think that it was worth it by the end! :)*
  3. I've had a request from someone to write about how I think that someone from a .net background should write a web-based application. The short answer is that I strongly believe that using Bridge.NET with React, using a Flux-like architecture is the way forward. I think that React changed the game by trying to introduce a way to write applications that addressed the big challenges, rather than what many of the other frameworks did, which was to try to make some aspects of development easier but without tackling the underlying problems. We're going to be using these technologies to create some big applications where I work and some resources as to how best to do this will be valuable. I'm going to try to roll this all together into a short series of posts about creating Bridge / React / Flux apps, where I'll try to start from the simplest approach at each point and only introduce something new when I can explain why it's valuable. So, initially, Flux will be nowhere to be seen - but, hopefully, when it *is* introduced, it will be clear why.
  4. (I'm not going to expel any effort on convincing you that writing C# in Visual Studio is incredibly powerful, and that it's fantastic to be able to do this while writing browser-based applications, nor am I going to try to sell you any more on React - if you're not on board with these ideas already then there's a *chance* that these posts will sell you on it, but that's not going to be my main focus).
  5. ### From the very start
  6. I'm going to begin from a completely fresh project, so if you've got any experience with Bridge then these steps will be familiar. But I'll go through them quickly and then start building the application itself. It's going to be extremely simple but will illustrate how to work with React and how to deal with user input and Virtual DOM re-rendering, how and where to implement validation and how to make *all the things* asynchronous so that async is not only used for scary edge cases and can be seen as a valuable tool to decouple code (and, in doing so, async will become a not-at-all-scary thing).
  7. All that the application will do will be to allow the user to write a message, entering Title and Content strings, and to save this message. There will be a "Message API", which will emulate reading and writing to a remote endpoint, for data persistence, but the implementation will all be kept in-memory / in the browser, just to make things simple. It will look something like this:
  8. <img alt="The proposed example app" src="/Content/Images/Posts/ReactTutorial1.png" class="NoBorder AlwaysFullWidth" />
  9. As more messages are written, more entries will appear in the "Message History". Seems simple.
  10. ### React components
  11. Before getting into any React-with-Bridge specifics, I want to talk a little about React components; how to arrange them into a hierarchy and how they should and shouldn't talk to each other.
  12. Almost all of the time, components that you use will be "[controlled components](https://facebook.github.io/react/docs/forms.html#controlled-components)" -
  13. > A Controlled component does not maintain its own internal state; the component renders purely based on props.
  14. This means that when you render a text input, you give it a "value" property and an "onChange" property - when the user tries to change what's in the text box (whether by pressing a number or letter, or by pressing backspace or by pasting something in) then the "onChange" callback is executed. The text box is *not* updated automatically, all that happens is that a callback is made that indicates that the user has done something that means that the text input probably *should* be updated.
  15. This seems odd at the very start since you may be used to events being driven by html elements and the updates being broadcast elsewhere; with React, events arise from components that describe the desire for a change to be made, but the change does not happen automatically. This is what is meant in the quote above when it says that a controlled component "does not maintain its own internal state".
  16. This hints at one of the key aims of React - to make code explicit and easy to reason about. If a component *only* varies based upon its props, then it's very easy to reason about; given this props data, draw in this manner. (If user-entered changes to a text input were automatically reflected in the text box then the component would *not* solely vary by its props, it would vary according to its props and whatever else the user has done to it).
  17. The only way for a component to update / re-render itself (and any child components that it may have) is for it to changes its "state". This is a special concept in React - if "SetState" is called then the component will re-render, but now it may have to consider both its props *and* its new state. If we really wanted to have a text input that would automatically update its own value as well as raise a change event, we could write a component to do so -
  18. *(Note: if you're coming into this fresh, don't worry about how to compile this C# code into a React application, I'll be getting to that after I've finished giving my view on React components).*
  19. public class TextInput : Component<TextInput.Props, TextInput.State>
  20. {
  21. public TextInput(Props props) : base(props) { }
  22. public override ReactElement Render()
  23. {
  24. return DOM.Input(new InputAttributes
  25. {
  26. Type = InputType.Text,
  27. Value = (state == null) ? props.InitialValue : state.Value,
  28. OnChange = ev =>
  29. {
  30. var newValue = ev.CurrentTarget.Value;
  31. SetState(new State { Value = newValue });
  32. props.OnChange(newValue);
  33. }
  34. });
  35. }
  36. public class Props
  37. {
  38. public string InitialValue;
  39. public Action<string> OnChange;
  40. }
  41. public class State
  42. {
  43. public string Value;
  44. }
  45. }
  46. The problem here is that now the component depends upon two things whenever it has to render - its props *and* its state. It can change its own state but it can't change its props (React demands that a components props be considered to be immutable).
  47. *This* means that the component becomes more difficult to reason about, it was much easier when it didn't have to worry about state. (Granted, there may have been some question as to who would receive that OnChange callback to get the component to re-render, but we're going to get to that shortly).
  48. Partly for this reason, it's strongly recommended that the vast majority of components be stateless - meaning that they render according to their props and nothing else.
  49. Another reason that it is strongly recommended that components not update themselves (meaning that they are stateless, since the only way for a component to update itself is to change its state) is that it makes the handling of events much clearer. In the example application that I'm going to refer to in this series, the "Title" value that is entered by the user is reflected in the fieldset legend -
  50. <img alt="Fieldset legend mirros the Title input value" src="/Content/Images/Posts/ReactTutorial2.png" class="NoBorder AlwaysFullWidth" />
  51. If the "Title" input box was to maintain its own state and update itself when its contents change, there still needs to be something listening for changes in order to update the fieldset legend text. If it was common for components to maintain their own state then things would quickly get out of hand as more and more components have to listen for (and react to) changes in other components. Just in the example here, there is a validation message that needs to be hidden if the "Title" input has a non-blank value, so that component would need to listen for the change event on the input. (Alternatively, the **TextInput** component could be provided with validation logic and *it* would be responsible for showing or hiding the validation message - which would complicate the **TextInput** class). On top of this, there is the "Save" button which should be disabled if either of the "Title" or "Content" input boxes have no value - so the Save button component would need to listen to change events from both text inputs and decide whether or not it should be enabled based upon their states. Maybe the input form itself wants to add an "invalid" class to itself for styling purposes if either of the inputs are invalid - now the form component has to listen to changes to the text inputs and add or remove this class. This way lies madness.
  52. In summary, most components should *not* try to update themselves and so do not need state. The React bindings make it easy to write components that don't use state (again, I'll talk about using these bindings more shortly, I just wanted to point out now that the distinction between stateful and stateless components is an important one and that the bindings reflect this) -
  53. public class TextInput : StatelessComponent<TextInput.Props>
  54. {
  55. public TextInput(Props props) : base(props) { }
  56. public override ReactElement Render()
  57. {
  58. return DOM.Input(new InputAttributes
  59. {
  60. Type = InputType.Text,
  61. Value = props.Value,
  62. OnChange = ev => props.OnChange(ev.CurrentTarget.Value)
  63. });
  64. }
  65. public class Props
  66. {
  67. public string Value;
  68. public Action<string> OnChange;
  69. }
  70. }
  71. The component code is much more succinct this way, as well as helping us avoid the nightmare scenario described above.
  72. It does leave one big question, though.. if these components don't update themselves, *then what does?*
  73. Answer: There should be a top-level "Container Component" that maintains state for the application. This should be the only stateful component, all components further down the hierarchy should be stateless.
  74. In the sample application here -
  75. <img alt="The proposed example app" src="/Content/Images/Posts/ReactTutorial1.png" class="NoBorder AlwaysFullWidth" />
  76. The component hierarchy will look something like this:
  77. AppContainer
  78. MessageEditor
  79. Span ("Title")
  80. ValidatedTextInput
  81. Input
  82. Span ("Content")
  83. ValidatedTextInput
  84. Input
  85. Button
  86. MessageHistory
  87. Div
  88. Span (Message Title)
  89. Span (Message Content)
  90. Div
  91. Span (Message Title)
  92. Span (Message Content)
  93. The **MessageHistory** will be a read-only component tree (it just shows saved messages) and so is very simple (there are no callbacks to handle). The **MessageEditor** will render Span labels ("Title" and "Content"), two **ValidatedTextInput** components and a "Save" button. The **ValidatedTextInput** has props for a current text input value, an on-change callback and an optional validation message.
  94. When an input component's on-change callback is executed, it is an action with a single argument; the html element. In the **TextInput** example class above, the new value is extracted from that element ("ev.CurrentTarget.Value") and then passed into the on-change callback of the **TextInput**, which is an action with a simple string argument. **ValidatedTextInput** will be very similar (it will wrap the **Action&lt;InputElement&gt;** callback that the input raises in a simpler **Action&lt;string&gt;**). The only difference between it and the **TextInput** example class earlier is that it will also be responsible for rendering a validation message element if its props value has a non-blank validation message to show (and it may apply an "invalid" class name to its wrapper if there is a validation message to show).
  95. When the Title or Content **ValidatedTextInput** raise an on-change event, the **MessageEditor** will execute some code that translates this callback further. The **MessageEditor** will have an on-change props value whose single argument is a **MessageDetails** - this will have have values for the current "Title" and "Content". Just as an on-change from an input element resulted in an on-change being raised by a **ValidatedTextInput**, an on-change by a **ValidatedTextInput** will result in an on-change from the **MessageEditor**. Each on-change event changes the type of value that the on-change describes (from an input element to a string to a **MessageDetails**). The **MessageEditor**'s on-change will be received by the **AppContainer** Component, which is where the change will result in the component tree being re-rendered.
  96. The **AppContainer** component will re-render by calling "SetState" and creating a new state reference for itself that include the new **MessageDetails** reference (that was passed up in the on-change callback from the **MessageEditor**). The call to "SetState" will result in the component being re-rendered, which will result in it rendering a new version of the **MessageEditor**. When the **MessageEditor** is rendered, the current "Title" value will be used to populate the text input *and* to set the text in the legend of the fieldset that wraps the editor's input boxes. This is how the "nightmare scenario" described earlier is avoided - instead of having lots of components listen out to events from lots of *other* components, all components just pass their events up to the top and then the entire UI is re-rendered in React's Virtual DOM.
  97. I'm going to repeat that part about event-handling because it's important; events are passed *up* from where they occur, up to the top-level component. This will trigger a re-render, which works all the way *down* through the component tree, so that the requested change is then reflected in the UI.
  98. The Virtual DOM determines what (if anything) needs to change in the browser's DOM and applies those changes - this works well because the Virtual DOM is very fast (and so we can do these "full Virtual DOM re-renders" frequently) and it minimises changes to the browser DOM (which is much slower).
  99. *(The Facebook tutorial [Thinking in React](https://facebook.github.io/react/docs/thinking-in-react.html) talks about how to mentally break down a UI into components and talks about passing state up the tree, but I wanted to try to really drive home how components should be put together and how they should communicate before fobbing you off with a Facebook link)*.
  100. I have some more recommendations on how to decide what to put into props and what into state when creating stateful container components, but I'll cover that ground after some more practical work.
  101. ### Let's start coding then!
  102. Open up Visual Studio (the version isn't too important, but if you're using 2015 then bear in mind that Bridge.NET doesn't yet support C# 6 syntax). Create a new "Class Library" project. Using NuGet, add the "Bridge" and the "Bridge.React" packages. This will bring in bindings for React as well as pulling in Bridge itself - the Bridge package removes the usual System, System.Collections, etc.. references and replaces them with a single "Bridge" reference, which re-implements those framework methods in code that has JavaScript translations.
  103. The Bridge package also adds some README files and a bridge.json file (under the Bridge folder in the project), which instructs Bridge how to compile your C# code into JavaScript. Change bridge.json's content to:
  104. {
  105. "output": "Bridge/output",
  106. "combineScripts": true
  107. }
  108. This will tell it create a single JavaScript file when translating, including the Bridge library content and the React bindings and JavaScript generated from code that you write. The name of the file that it generates is based upon the name of your project. I named mine "BridgeReactTutorial" and so the Bridge compiler will generate "BridgeReactTutorial.js" and "BridgeReactTutorial.min.js" files in the "Bridge/output" folder on each build of the project.
  109. Now change demo.html (which is another file that the Bridge NuGet package created, it will be in the Brige/www folder) to the following:
  110. <!DOCTYPE html>
  111. <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
  112. <head>
  113. <meta charset="utf-8" />
  114. <title>Bridge.React Tutorial</title>
  115. <link rel="Stylesheet" type="text/css" href="styles.css" media="screen" />
  116. </head>
  117. <body>
  118. <noscript>JavaScript is required</noscript>
  119. <div id="main" class="loading">Loading..</div>
  120. <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react.js"></script>
  121. <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react-dom.js"></script>
  122. <script src="../output/BridgeReactTutorial.js"></script>
  123. </body>
  124. </html>
  125. *(If you called your project something other than "BridgeReactTutorial" then you might have to change the filename in that last script tag).*
  126. This file will load in the latest (0.14.7, as of March 2016) version of the React library along with the Bridge / React-bindings / your-code bundle. All we need to do now is write some "your-code" content.
  127. When you created the class library project, a Class1.cs file will have been added to the project. Change its contents -
  128. using System.Linq;
  129. using Bridge.Html5;
  130. using Bridge.React;
  131. namespace BridgeReactTutorial
  132. {
  133. public class Class1
  134. {
  135. [Ready]
  136. public static void Main()
  137. {
  138. var container = Document.GetElementById("main");
  139. container.ClassName = string.Join(
  140. " ",
  141. container.ClassName.Split().Where(c => c != "loading")
  142. );
  143. React.Render(
  144. DOM.Div(new Attributes { ClassName = "welcome" }, "Hi!"),
  145. container
  146. );
  147. }
  148. }
  149. }
  150. Build the solution and then right-click on the "demo.html" file in the project and click on "View in Browser". You should see a happy little "Hi!" welcome message, rendered using React by JavaScript that was translated from C# - an excellent start!
  151. There are some subtle touches here, such as the "JavaScript is required" message that is displayed if the browser has JavaScript disabled (just in case you ever turn it off and forget!) and a "loading" message that is displayed while the JavaScript sorts itself out (usually this will be a barely-perceptibe amount of time but if the CDN host that the React library is coming from is being slow then it may not be instantaneous). The "main" div initially has a "loading" class on it, which is removed when the code above executes. Note that the [Ready] attribute on the "Main" function is a Bridge attribute, indicating code that should be called when the page has loaded (similar in principle to on-DOM-ready, frequently used by jQuery code).
  152. To take advantage of the "loading" class' presence / absence, it would be a nice touch to have the "loading" text quite pale initially (it's reassuring to know that the app is, in fact, loading, but it doesn't need to be right in your face). To do so, add a file "styles.css" alongside the "demo.html" file. It's already referenced by the markup we've pasted into "demo.html", so it will be picked up when you refresh the page. Since we're creating a stylesheet, it makes sense to include some style resets (my go-to for this is by Eric Meyer) -
  153. /* http://meyerweb.com/eric/tools/css/reset/ v2.0b1 | 201101 NOTE: WORK IN PROGRESS
  154. USE WITH CAUTION AND TEST WITH ABANDON */
  155. html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote,
  156. pre,a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s,
  157. samp, small, strike, strong, sub, sup, tt, var,b, u, i, center, dl, dt, dd, ol, ul,
  158. li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td,
  159. article, aside, canvas, details, figcaption, figure, footer, header, hgroup, menu,
  160. nav, section, summary, time, mark, audio, video
  161. {
  162. margin: 0;
  163. padding: 0;
  164. border: 0;
  165. outline: 0;
  166. font-size: 100%;
  167. font: inherit;
  168. vertical-align: baseline;
  169. }
  170. /* HTML5 display-role reset for older browsers */
  171. article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav,
  172. section { display: block; }
  173. body { line-height: 1; }
  174. ol, ul { list-style: none; }
  175. blockquote, q { quotes: none; }
  176. blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; }
  177. /* remember to highlight inserts somehow! */ ins { text-decoration: none; }
  178. del { text-decoration: line-through; }
  179. table { border-collapse: collapse; border-spacing: 0; }
  180. div#main.loading { color: #f1f1f1; }
  181. At this point, I also tend to remove the "App_Readme" folder that the Bridge package adds to my project - if I'm going to write some code and check it into source control somewhere then I don't think there's a lot of point in storing a copy of the Bridge README and LICENSE each time.
  182. ### Creating the Message Editor
  183. That's the theory and the project scaffolding out of the way. Now to create a form that actually does something.
  184. We've already seen how a **TextInput** component is helpful for wrapping a text input and simplifying the "OnChange" callback. So create a "Components" folder with a "TextInput.cs" file and paste in the following content -
  185. using System;
  186. using Bridge.Html5;
  187. using Bridge.React;
  188. namespace BridgeReactTutorial.Components
  189. {
  190. public class TextInput : StatelessComponent<TextInput.Props>
  191. {
  192. public TextInput(Props props) : base(props) { }
  193. public override ReactElement Render()
  194. {
  195. return DOM.Input(new InputAttributes
  196. {
  197. Type = InputType.Text,
  198. ClassName = props.ClassName,
  199. Value = props.Content,
  200. OnChange = e => props.OnChange(e.CurrentTarget.Value)
  201. });
  202. }
  203. public class Props
  204. {
  205. public string ClassName;
  206. public string Content;
  207. public Action<string> OnChange;
  208. }
  209. }
  210. }
  211. *(Note: When adding a new ".cs" file to a project, sometimes "System" will sneak back into the list of references in the project - this can confuse Bridge, so ensure that you remove the reference again if it gets added).*
  212. Now create another folder in the root of the project called "ViewModels". Add a new file to it; "MessageDetails.cs" and paste in the following content -
  213. namespace BridgeReactTutorial.ViewModels
  214. {
  215. public class MessageDetails
  216. {
  217. public string Title;
  218. public string Content;
  219. }
  220. }
  221. Now add another file to the "Components" folder; "MessageEditor.cs" and paste in this:
  222. using System;
  223. using Bridge.React;
  224. using BridgeReactTutorial.ViewModels;
  225. namespace BridgeReactTutorial.Components
  226. {
  227. public class MessageEditor : StatelessComponent<MessageEditor.Props>
  228. {
  229. public MessageEditor(Props props) : base(props) { }
  230. public override ReactElement Render()
  231. {
  232. return DOM.FieldSet(new FieldSetAttributes { ClassName = props.ClassName },
  233. DOM.Legend(null, string.IsNullOrWhiteSpace(props.Title) ? "Untitled" : props.Title),
  234. DOM.Span(new Attributes { ClassName = "label" }, "Title"),
  235. new TextInput(new TextInput.Props
  236. {
  237. ClassName = "title",
  238. Content = props.Title,
  239. OnChange = newTitle => props.OnChange(new MessageDetails
  240. {
  241. Title = newTitle,
  242. Content = props.Content
  243. })
  244. }),
  245. DOM.Span(new Attributes { ClassName = "label" }, "Content"),
  246. new TextInput(new TextInput.Props
  247. {
  248. ClassName = "content",
  249. Content = props.Content,
  250. OnChange = newContent => props.OnChange(new MessageDetails
  251. {
  252. Title = props.Title,
  253. Content = newContent
  254. })
  255. })
  256. );
  257. }
  258. public class Props
  259. {
  260. public string ClassName;
  261. public string Title;
  262. public string Content;
  263. public Action<MessageDetails> OnChange;
  264. }
  265. }
  266. }
  267. Now things are getting interesting!
  268. This is still a stateless component and so what is rendered depends solely and reliably upon its props data. When it renders, the "Title" value from its props is used to populate both the legend of the fieldset that it renders (unless "Title" is null, blank or white-space-only, in which case the legend text will be "Untitled) and it's used to populate the "Title" **TextInput**. When either of its **TextInput**s raises an on-change event, the **MessageEditor** raises its on-change events with a new **MessageDetails** instance.
  269. Note that there's no validation yet. We'll get this rough version working first and then add that later.
  270. There are still a few more steps until we have an application, though. We need a container component to render the form in the first place and to deal with on-change events that bubble up. Create another class file within the "Components" folder named "AppContainer.cs" -
  271. using Bridge.Html5;
  272. using Bridge.React;
  273. using BridgeReactTutorial.ViewModels;
  274. namespace BridgeReactTutorial.Components
  275. {
  276. public class AppContainer : Component<object, AppContainer.State>
  277. {
  278. public AppContainer() : base(null) { }
  279. protected override State GetInitialState()
  280. {
  281. return new State
  282. {
  283. Message = new MessageDetails { Title = "", Content = "" }
  284. };
  285. }
  286. public override ReactElement Render()
  287. {
  288. return new MessageEditor(new MessageEditor.Props
  289. {
  290. ClassName = "message",
  291. Title = state.Message.Title,
  292. Content = state.Message.Content,
  293. OnChange = newMessage => SetState(new State { Message = newMessage })
  294. });
  295. }
  296. public class State
  297. {
  298. public MessageDetails Message;
  299. }
  300. }
  301. }
  302. This is the *stateful* component that will trigger re-renders when required. It doesn't actually require any props data at this time, so the "TProps" type parameter specified on the **Component&lt;TProps, TState&gt;** base class is just "object".
  303. When the **MessageEditor** raises an on-change event, the **AppContainer** will call SetState to replace its current **MessageDetails** instance with the new one. This will trigger a re-render of the **MessageEditor**, which will be given the new **MessageDetails** instance as part of a new props value. It might seem a bit silly to have the **MessageEditor** pass up a new **MessageDetails** instance and then to just pass this back down into another **MessageEditor**, but the idea is to consider the first **MessageEditor** to be dead now and for the new **MessageEditor** (with the new **MessageDetails**) to exist in its place. And each time a stateless component is rendered, it renders simply from its props - there is no data shared between the new instance and the instance it replaces. This, again, makes the components very easy to reason about. And code that is easy to reason about is easy to write and easy to maintain.
  304. *Note: If you're au fait with React then you might know that components written as ES6 classes - which seems to be the way that is encouraged at the moment - don't support "GetInitialState" and, instead, specify initial state in the constructor. In the Bridge React bindings, "GetInitialState" should be used and the constructor should NOT be used - the way that the components are initialised by React means that constructors on component classes are not actually executed, so it is important that the constructor ONLY be used to pass the props and/or state to the base class.*
  305. The penultimate step is to change "Class1.cs" to render the **AppContainer** instead of just rendering a "Hi!" div. While we're editing it, let's give it a more official-sounding name. I like the starting point of my application to be called "App" -
  306. using System.Linq;
  307. using Bridge.Html5;
  308. using Bridge.React;
  309. using BridgeReactTutorial.Components;
  310. namespace BridgeReactTutorial
  311. {
  312. public class App
  313. {
  314. [Ready]
  315. public static void Go()
  316. {
  317. var container = Document.GetElementById("main");
  318. container.ClassName = string.Join(
  319. " ",
  320. container.ClassName.Split().Where(c => c != "loading")
  321. );
  322. React.Render(new AppContainer(), container);
  323. }
  324. }
  325. }
  326. All that's required now is to make it look a little nicer when you view "demo.html", so add the following to "styles.css" -
  327. body
  328. {
  329. font-family: 'Segoe UI';
  330. padding: 8px;
  331. }
  332. fieldset
  333. {
  334. padding: 8px;
  335. border: 1px solid #f1f1f1;
  336. border-radius: 4px;
  337. }
  338. fieldset legend
  339. {
  340. color: blue;
  341. padding: 0 8px;
  342. }
  343. fieldset.message span.label { padding: 0 8px; }
  344. That's the first major milestone reached! A very basic framework for constructing component hierarchies has been demonstrated, along with a way to handle events and re-render as required. There's nothing very radical, it's just what was described earlier; but it's good to see the theory executed in practice.
  345. I'm far from finished for today, though - I want to add a way to persist messages, a message history component and some validation. Best get cracking!
  346. ### Message persistence
  347. While I want to simulate a server-based API, where read / write requests aren't instantaneous and we need to think about how to deal with async calls, I don't want the overhead of needing an endpoint to be configured somewhere. So we'll go with a simple interface that will be implemented in an entirely client-side class, that introduces artifical delays to mimic server-calling time.
  348. Create a new folder in the project root called "API" and add a new .cs file "IReadAndWriteMessages.cs", the contents of which should be:
  349. using System.Threading.Tasks;
  350. using BridgeReactTutorial.ViewModels;
  351. namespace BridgeReactTutorial.API
  352. {
  353. public interface IReadAndWriteMessages
  354. {
  355. Task SaveMessage(MessageDetails message);
  356. }
  357. }
  358. We'll be using dependency injection to provide the **AppContainer** with an API implementation. In order to enable unit testing (which will come later) we need to be able to work against an interface. For now, the interface only has a "SaveMessage" method, we'll work on reading message history data later.
  359. Add another file into the "API" folder, "MessageApi.cs" -
  360. using System;
  361. using System.Threading.Tasks;
  362. using Bridge.Html5;
  363. using BridgeReactTutorial.ViewModels;
  364. namespace BridgeReactTutorial.API
  365. {
  366. public class MessageApi : IReadAndWriteMessages
  367. {
  368. public Task SaveMessage(MessageDetails message)
  369. {
  370. if (message == null)
  371. throw new ArgumentNullException("message");
  372. if (string.IsNullOrWhiteSpace(message.Title))
  373. throw new ArgumentException("A title value must be provided");
  374. if (string.IsNullOrWhiteSpace(message.Content))
  375. throw new ArgumentException("A content value must be provided");
  376. var task = new Task<object>(null);
  377. Window.SetTimeout(
  378. () => task.Complete(),
  379. 1000 // Simulate a roundtrip to the server
  380. );
  381. return task;
  382. }
  383. }
  384. }
  385. Bridge supports the C# "async" keyword and provides its own implementation of Tasks, which are used above to pretend that this class is communicating with a server when a save is requested.
  386. In order to enable saving, the **MessageEditor** needs a "Save" button and it needs an "on-save" callback to be specified on its props. While saving, the form should be disabled, so the **MessageEditor** props need a "Disabled" flag as well.
  387. *When designing an SPA like this, you need to think about whether you will support "optimistic updates", where clicking Save clears the form and acts as if the save action was instanteously accepted - but brings it to the user's attention somehow if the save failed or was rejected. I'm going to go for a simpler "pessimistic update" flow, where the form is disabled until the save is acknowledged, at which point the form will be cleared and re-enabled so that a further entry may be written and then saved.*
  388. The **MessageEditor** should now looks like this:
  389. using System;
  390. using Bridge.React;
  391. using BridgeReactTutorial.ViewModels;
  392. namespace BridgeReactTutorial.Components
  393. {
  394. public class MessageEditor : StatelessComponent<MessageEditor.Props>
  395. {
  396. public MessageEditor(Props props) : base(props) { }
  397. public override ReactElement Render()
  398. {
  399. return DOM.FieldSet(new FieldSetAttributes { ClassName = props.ClassName },
  400. DOM.Legend(null, string.IsNullOrWhiteSpace(props.Title) ? "Untitled" : props.Title),
  401. DOM.Span(new Attributes { ClassName = "label" }, "Title"),
  402. new TextInput(new TextInput.Props
  403. {
  404. ClassName = "title",
  405. Disabled = props.Disabled,
  406. Content = props.Title,
  407. OnChange = newTitle => props.OnChange(new MessageDetails
  408. {
  409. Title = newTitle,
  410. Content = props.Content
  411. })
  412. }),
  413. DOM.Span(new Attributes { ClassName = "label" }, "Content"),
  414. new TextInput(new TextInput.Props
  415. {
  416. ClassName = "content",
  417. Disabled = props.Disabled,
  418. Content = props.Content,
  419. OnChange = newContent => props.OnChange(new MessageDetails
  420. {
  421. Title = props.Title,
  422. Content = newContent
  423. })
  424. }),
  425. DOM.Button(
  426. new ButtonAttributes { Disabled = props.Disabled, OnClick = e => props.OnSave() },
  427. "Save"
  428. )
  429. );
  430. }
  431. public class Props
  432. {
  433. public string ClassName;
  434. public string Title;
  435. public string Content;
  436. public Action<MessageDetails> OnChange;
  437. public Action OnSave;
  438. public bool Disabled;
  439. }
  440. }
  441. }
  442. The "Disabled" flag needs to be able to be applied to the **TextInput** components, so **TextInput** needs to look like this:
  443. using System;
  444. using Bridge.Html5;
  445. using Bridge.React;
  446. namespace BridgeReactTutorial.Components
  447. {
  448. public class TextInput : StatelessComponent<TextInput.Props>
  449. {
  450. public TextInput(Props props) : base(props) { }
  451. public override ReactElement Render()
  452. {
  453. return DOM.Input(new InputAttributes
  454. {
  455. Type = InputType.Text,
  456. ClassName = props.ClassName,
  457. Disabled = props.Disabled,
  458. Value = props.Content,
  459. OnChange = e => props.OnChange(e.CurrentTarget.Value)
  460. });
  461. }
  462. public class Props
  463. {
  464. public string ClassName;
  465. public bool Disabled;
  466. public string Content;
  467. public Action<string> OnChange;
  468. }
  469. }
  470. }
  471. This enables the **MessageEditor** to initiate a save request and for a "Message API" to process the request. Now the **AppContainer** needs to tie these two aspects together.
  472. *Note that the OnSave action on the **MessageEditor** doesn't provide a new **MessageDetails** instance - that is because the Title and Content value that are rendered in the **MessageEditor** could not have been changed since the component was rendered, otherwise an OnChange callback would have been made before OnSave.*
  473. Now, the **AppContainer** gets a bit more interesting because it requires props *and* state. Its props will be external dependencies that it requires access to, while its state will be a copy of all data that is required to render the form. This is a good time to introduce my React (stateful) component guidelines -
  474. 1. A stateful component's "props" data should *only* consist of references to external dependencies
  475. 1. A stateful component's "state" data should include *everything* required to render the component tree, though the props may be required to deal with child components' events
  476. At this point, these rules are going to seem very straight-forward. Later, however, things will get a little more nuanced and I'll re-visit them at that point.
  477. The **AppContainer** will now become the following -
  478. using Bridge.React;
  479. using BridgeReactTutorial.API;
  480. using BridgeReactTutorial.ViewModels;
  481. namespace BridgeReactTutorial.Components
  482. {
  483. public class AppContainer : Component<AppContainer.Props, AppContainer.State>
  484. {
  485. public AppContainer(AppContainer.Props props) : base(props) { }
  486. protected override State GetInitialState()
  487. {
  488. return new State
  489. {
  490. Message = new MessageDetails { Title = "", Content = "" },
  491. IsSaveInProgress = false
  492. };
  493. }
  494. public override ReactElement Render()
  495. {
  496. return new MessageEditor(new MessageEditor.Props
  497. {
  498. ClassName = "message",
  499. Title = state.Message.Title,
  500. Content = state.Message.Content,
  501. OnChange = newMessage => SetState(new State
  502. {
  503. Message = newMessage,
  504. IsSaveInProgress = state.IsSaveInProgress
  505. }),
  506. OnSave = async () =>
  507. {
  508. SetState(new State { Message = state.Message, IsSaveInProgress = true });
  509. await props.MessageApi.SaveMessage(state.Message);
  510. SetState(new State
  511. {
  512. Message = new MessageDetails { Title = "", Content = "" },
  513. IsSaveInProgress = false
  514. });
  515. },
  516. Disabled = state.IsSaveInProgress
  517. });
  518. }
  519. public class Props
  520. {
  521. public IReadAndWriteMessages MessageApi;
  522. }
  523. public class State
  524. {
  525. public MessageDetails Message;
  526. public bool IsSaveInProgress;
  527. }
  528. }
  529. }
  530. You will need to update App.cs to pass a props reference with a **MessageApi** instance to the **AppContainer** constructor -
  531. using System.Linq;
  532. using Bridge.Html5;
  533. using Bridge.React;
  534. using BridgeReactTutorial.API;
  535. using BridgeReactTutorial.Components;
  536. namespace BridgeReactTutorial
  537. {
  538. public class App
  539. {
  540. [Ready]
  541. public static void Go()
  542. {
  543. var container = Document.GetElementById("main");
  544. container.ClassName = string.Join(
  545. " ",
  546. container.ClassName.Split().Where(c => c != "loading")
  547. );
  548. React.Render(
  549. new AppContainer(new AppContainer.Props { MessageApi = new MessageApi() }),
  550. container
  551. );
  552. }
  553. }
  554. }
  555. With this final piece, we have the outline of a fully functioning application! Granted, its functionality is not particular magnificent, but it *has* illustrated some important principles. We've seen how a component hierarchy should have a top-level *stateful* component, with a component tree beneath it of state*less* components (note that there are no guidelines required regarding what to put into props and what to put into state when writing a stateless component because props is your only option - another reason why stateless components are so much simpler!). We've also seen how we can deal with dependency injection for these top level components, which are the only point at which more complicated logic appears such as "a save request involves disabling the form, calling a method on the API, waiting for the result and then re-enabling the form". It's worth noting that in the next post, this logic will be moved out of the top-level component in a quest to make components as dumb as possible - but that's jumping ahead, and I want the format of these posts to be that we start simple and then get more complicated only as the benefits of doing so can be made clear.
  556. At this point, however, we have something of a problem. If the "Title" and "Content" text inputs do not both have values, then an exception will be raised by the **MessageApi** when a save is attempted. To avoid this, we need some..
  557. ### Validation
  558. I mentioned in the "React components" section that there would be a **ValidatedTextInput**, but no code had been presented yet. So here we go, nothing in it should be particularly surprising -
  559. using System;
  560. using Bridge.React;
  561. namespace BridgeReactTutorial.Components
  562. {
  563. public class ValidatedTextInput : StatelessComponent<ValidatedTextInput.Props>
  564. {
  565. public ValidatedTextInput(Props props) : base(props) { }
  566. public override ReactElement Render()
  567. {
  568. var className = props.ClassName;
  569. if (!string.IsNullOrWhiteSpace(props.ValidationMessage))
  570. className = (className + " invalid").Trim();
  571. return DOM.Span(new Attributes { ClassName = className },
  572. new TextInput(new TextInput.Props
  573. {
  574. ClassName = props.ClassName,
  575. Disabled = props.Disabled,
  576. Content = props.Content,
  577. OnChange = props.OnChange
  578. }),
  579. string.IsNullOrWhiteSpace(props.ValidationMessage)
  580. ? null
  581. : DOM.Span(
  582. new Attributes { ClassName = "validation-message" },
  583. props.ValidationMessage
  584. )
  585. );
  586. }
  587. public class Props
  588. {
  589. public string ClassName;
  590. public bool Disabled;
  591. public string Content;
  592. public Action<string> OnChange;
  593. public string ValidationMessage;
  594. }
  595. }
  596. }
  597. This allows the **MessageEditor** to be changed to use these **ValidatedTextInput**s instead of regular **TextInput**s, setting the "ValidationMessage" values according to whether the "Content" string has a value -
  598. using System;
  599. using Bridge.React;
  600. using BridgeReactTutorial.ViewModels;
  601. namespace BridgeReactTutorial.Components
  602. {
  603. public class MessageEditor : StatelessComponent<MessageEditor.Props>
  604. {
  605. public MessageEditor(Props props) : base(props) { }
  606. public override ReactElement Render()
  607. {
  608. var formIsInvalid =
  609. string.IsNullOrWhiteSpace(props.Title) ||
  610. string.IsNullOrWhiteSpace(props.Content);
  611. return DOM.FieldSet(new FieldSetAttributes { ClassName = props.ClassName },
  612. DOM.Legend(null, string.IsNullOrWhiteSpace(props.Title) ? "Untitled" : props.Title),
  613. DOM.Span(new Attributes { ClassName = "label" }, "Title"),
  614. new ValidatedTextInput(new ValidatedTextInput.Props
  615. {
  616. ClassName = "title",
  617. Disabled = props.Disabled,
  618. Content = props.Title,
  619. OnChange = newTitle => props.OnChange(new MessageDetails
  620. {
  621. Title = newTitle,
  622. Content = props.Content
  623. }),
  624. ValidationMessage = string.IsNullOrWhiteSpace(props.Title)
  625. ? "Must enter a title"
  626. : null
  627. }),
  628. DOM.Span(new Attributes { ClassName = "label" }, "Content"),
  629. new ValidatedTextInput(new ValidatedTextInput.Props
  630. {
  631. ClassName = "content",
  632. Disabled = props.Disabled,
  633. Content = props.Content,
  634. OnChange = newContent => props.OnChange(new MessageDetails
  635. {
  636. Title = props.Title,
  637. Content = newContent
  638. }),
  639. ValidationMessage = string.IsNullOrWhiteSpace(props.Content)
  640. ? "Must enter message content"
  641. : null
  642. }),
  643. DOM.Button(
  644. new ButtonAttributes
  645. {
  646. Disabled = props.Disabled || formIsInvalid,
  647. OnClick = e => props.OnSave()
  648. },
  649. "Save"
  650. )
  651. );
  652. }
  653. public class Props
  654. {
  655. public string ClassName;
  656. public string Title;
  657. public string Content;
  658. public Action<MessageDetails> OnChange;
  659. public Action OnSave;
  660. public bool Disabled;
  661. }
  662. }
  663. }
  664. Now, the "Save" button is disabled if the **MessageEditor** is disabled (according to its props flag) *or* if the form entry is invalid. Now, it's not possible for the user to attempt a save that we will know will fail!
  665. *(Moving validation logic out of the components is another thing that will come in the move towards dumb-as-possible components, but that's for part two).*
  666. To keep things looking pretty, adding the following to "styles.css" -
  667. fieldset.message span.title, fieldset.message span.content { position: relative; }
  668. fieldset.message span.validation-message
  669. {
  670. position: absolute;
  671. top: -6px;
  672. right: 2px;
  673. padding: 2px 4px;
  674. font-size: 70%;
  675. background: #FFF9D8;
  676. border: 1px solid #EFE9CB;
  677. border-radius: 2px;
  678. color: #A8A390;
  679. }
  680. fieldset.message button { margin-left: 8px; }
  681. ### Message History
  682. What's the point in saving messages if we can't read them back out again? To enable this, the **IReadAndWriteMessages** needs a "GetMessages" method to accompany "SaveMessage" -
  683. using System;
  684. using System.Collections.Generic;
  685. using System.Threading.Tasks;
  686. using BridgeReactTutorial.ViewModels;
  687. namespace BridgeReactTutorial.API
  688. {
  689. public interface IReadAndWriteMessages
  690. {
  691. Task SaveMessage(MessageDetails message);
  692. Task<IEnumerable<Tuple<int, MessageDetails>>> GetMessages();
  693. }
  694. }
  695. This needs implementing in **MessageApi** -
  696. using System;
  697. using System.Collections.Generic;
  698. using System.Threading.Tasks;
  699. using Bridge.Html5;
  700. using BridgeReactTutorial.ViewModels;
  701. namespace BridgeReactTutorial.API
  702. {
  703. public class MessageApi : IReadAndWriteMessages
  704. {
  705. private readonly List<Tuple<int, MessageDetails>> _messages;
  706. public MessageApi()
  707. {
  708. _messages = new List<Tuple<int, MessageDetails>>();
  709. }
  710. public Task SaveMessage(MessageDetails message)
  711. {
  712. if (message == null)
  713. throw new ArgumentNullException("message");
  714. if (string.IsNullOrWhiteSpace(message.Title))
  715. throw new ArgumentException("A title value must be provided");
  716. if (string.IsNullOrWhiteSpace(message.Content))
  717. throw new ArgumentException("A content value must be provided");
  718. var task = new Task<object>(null);
  719. Window.SetTimeout(
  720. () =>
  721. {
  722. _messages.Add(Tuple.Create(_messages.Count, message));
  723. task.Complete();
  724. },
  725. 1000 // Simulate a roundtrip to the server
  726. );
  727. return task;
  728. }
  729. public Task<IEnumerable<Tuple<int, MessageDetails>>> GetMessages()
  730. {
  731. // ToArray is used to return a clone of the message set - otherwise, the caller would
  732. // end up with a list that is updated when the internal reference within this class
  733. // is updated (which sounds convenient but it's not the behaviour that would be
  734. // exhibited if this was really persisting messages to a server somewhere)
  735. var task = new Task<IEnumerable<Tuple<int, MessageDetails>>>(null);
  736. Window.SetTimeout(
  737. () => task.Complete(_messages.ToArray()),
  738. 1000 // Simulate a roundtrip to the server
  739. );
  740. return task;
  741. }
  742. }
  743. }
  744. Now, we'll need a way to render this information -
  745. using System;
  746. using System.Collections.Generic;
  747. using System.Linq;
  748. using Bridge.React;
  749. using BridgeReactTutorial.ViewModels;
  750. namespace BridgeReactTutorial.Components
  751. {
  752. public class MessageHistory : StatelessComponent<MessageHistory.Props>
  753. {
  754. public MessageHistory(Props props) : base(props) { }
  755. public override ReactElement Render()
  756. {
  757. var className = props.ClassName;
  758. if (!props.Messages.Any())
  759. className = (className + " zero-messages").Trim();
  760. // Any time a set of child components is dynamically-created (meaning that the
  761. // numbers of items may vary from one render to another), each must have a unique
  762. // "Key" property set (this may be a int or a string). Here, this is simple as
  763. // each message tuple is a unique id and the contents of that message (and the
  764. // unique id is ideal for use as a unique "Key" property).
  765. var messageElements = props.Messages
  766. .Select(idAndMessage => DOM.Div(new Attributes { Key = idAndMessage.Item1 },
  767. DOM.Span(new Attributes { ClassName = "title" }, idAndMessage.Item2.Title),
  768. DOM.Span(new Attributes { ClassName = "content" }, idAndMessage.Item2.Content)
  769. ));
  770. // When child components are specified (as they are through the second argument of
  771. // DOM.Div), the argument is of type Any<ReactElement, string>[] (meaning that each
  772. // element may be another component or it may be a simple text value)
  773. // - The React bindings have an extension method that transforms an IEnumerable
  774. // set of components (such as "messageElements" above) into an
  775. // Any<ReactElement, string>[]
  776. return DOM.FieldSet(new FieldSetAttributes { ClassName = className },
  777. DOM.Legend(null, "Message History"),
  778. DOM.Div(null, messageElements.ToChildComponentArray())
  779. );
  780. }
  781. public class Props
  782. {
  783. public string ClassName;
  784. public IEnumerable<Tuple<int, MessageDetails>> Messages;
  785. }
  786. }
  787. }
  788. This highlights an important React principle - where there are sets of dynamic child components, each must be provided a unique key. In the component above, we take "props.Messages" and map the data onto a set of Div elements. It's very possible that different messages will be rendered each time and so this is precisely what is meant by "dynamic child components".
  789. There are two reasons why it's important to provide unique keys - the first is performance; the task of React's Virtual DOM is to take the last component tree and the new component tree and work out what changed, so that the minimum changes may be applied to the browser DOM. In order to do this, it is very helpful for React to be able to track components as they move around within a dynamic set - it can allow it to reuse data internally instead of having to throw away representations of components and recreate them:
  790. > When React reconciles the keyed children, it will ensure that any child with key will be reordered (instead of clobbered) or destroyed (instead of reused).
  791. The quote above is from [Facebook's docs about Dynamic Children](https://facebook.github.io/react/docs/multiple-components.html#dynamic-children) - and so "clobbered" must be an official term!
  792. The second reason why it's important is that component state can only be tracked with a component if the component itself can be tracked by React when dynamic elements move around. I'm not going to dwell too long on this because it's only applicable if you are relying on dynamic components having state, which you shouldn't be since only the top-level component should be stateful (and any component that may be created as a dynamic child component should be stateless).
  793. For our purposes here, providing a unique key for each **MessageHistory** row is easy because the "GetMessages" method in the API returns a set of tuples, where each pair is a combination of id for the message and the message itself. This was easy to implement with the in-memory message store that we're using for …

Large files files are truncated, but you can click here to view the full file