30 March 2016
In parts One and Two, I described how to create a simple application using React and a Flux-like architecture, written in Bridge.NET - it covered where and how to deal with validation, how to integrate with a persistence layer, how to deal with asynchronous interactions (and how they don't need to be scary) and how the approach made the code easy to test and easy to reason about.
The components created using the React / Bridge bindings have their requirements / dependencies described in a strongly-typed manner since each component's "props" reference is a nested class with properties for each value or reference that will be needed in order for it to render.
This combination of technologies has the potential to be really powerful for writing client-side / browser-based applications, particularly with the ability to leverage C#'s proven strength in allowing the writing of large and reliable systems. However, I'm not happy with the example application yet. Although the way that it's written makes a lot of it easy to understand and, hopefully, makes the intent of the code clear, it still could be even easier to understand and the intent and the requirements even clearer.
People often like to talk as if a language is dynamically-typed (or "non-typed" or "uni-typed", depending upon the language, their vocabulary and their knowledge and opinion of the language) or statically-typed; as if it is a binary choice. Really, it is a sliding scale. C# definitely sits on the "statically-typed side", but the way that you write C# dictates how far along the scale that your C# is.
(C# can also be written to act as a dynamically-typed language, particularly if you use the "dynamic" keyword - but it's principally a statically-typed language).
I'm going to describe some ways to improve the example application and, in doing so, extrapolate some rules as to how to make the code clearer (and, again, easier to reason about, write and maintain - since these are extremely important qualities of code to me, that I strive for when developing and that appealed to me when I first encountered React). These will be my opinions (based upon my experiences) and you might disagree with them - but this is where we really get into "The Dan Way" of working with Bridge and React. If you do choose to disagree, then hopefully parts One and Two will continue to be useful (but I'm afraid we can never be friends*).
* (Only joking**)
** (I'm not)
Let's jump straight in with a simple example. We have a TextInput component that renders a text input element and passes up any requests by the user that the input's content be changed. The main primary purpose of this class is to provide a simple interface. Many of the html attributes that may be applied to a text input are not relevant (this exposes only the basics, such as "ClassName"). Similarly, the "OnChange" that a text input raises has a relatively complicated event reference (it allows you to identify the element that changed and then get the requested "new value" from it, but I want a TextInput's "OnChange" event to simply provide the new string and nothing else).
using System;
using Bridge.Html5;
using Bridge.React;
namespace BridgeReactTutorial.Components
{
public class TextInput : StatelessComponent<TextInput.Props>
{
public TextInput(Props props) : base(props) { }
public override ReactElement Render()
{
return DOM.Input(new InputAttributes
{
Type = InputType.Text,
ClassName = props.ClassName,
Disabled = props.Disabled,
Value = props.Content,
OnChange = e => props.OnChange(e.CurrentTarget.Value)
});
}
public class Props
{
public string ClassName;
public bool Disabled;
public string Content;
public Action<string> OnChange;
}
}
}
In the context of this small class, looking at the code, I would say that it's fairly clear what each line of code does and what each of the properties of the Props class is required for and how it will be used. However, even within such a small class, there are several implicit assumptions that are being made - eg.
First off, I don't like the should-or-shouldn't-be-allowed-to-be-null confusion around the "ClassName" and "Content" values. Secondly, I don't like the fact that, as it stands, you need to read (or already be familiar with) the code inside the TextInput component. One way to try to address these issues would be to consider using summary comments on the Props class - eg.
public class Props
{
/// <summary>
/// This is optional and so may be null (if non-null, then it should be a valid class
/// name - ie. non-blank)
/// </summary>
public string ClassName;
public bool Disabled;
/// <summary>
/// An input may not always have a value and so this may be blank (but it should never
/// be null)
/// </summary>
public string Content;
/// <summary>
/// This is mandatory and may never be null
/// </summary>
public Action<string> OnChange;
}
The problem with this approach is that, if the comments are ignored, runtime problems will occur at some point and it may not be very easy to trace them back to where they originated. If the "OnChange" value is null, for example, then the problem won't be noticed until the user interacts with the input box - and the code that raises the exception (the "props.OnChange" call with TextInput's "Render" method) will be completely removed from the code that incorrectly set the null value (the code that instantiated and populated Props instance).
So another alternative would be to combine these comments with some validation - eg.
public class Props
{
private string _className, _content;
private Action<string> _onChange;
/// <summary>
/// This is optional and so may be null (if non-null, then it should be a valid class
/// name - ie. non-blank)
/// </summary>
public string ClassName
{
get { return _className; }
set
{
if (value == "")
throw new ArgumentException("ClassName should not be set to a blank string");
_className = value;
}
}
public bool Disabled { get; set; }
/// <summary>
/// An input may not always have a value and so this may be blank (but it should never
/// be null)
/// </summary>
public string Content
{
get { return _content; }
set
{
if (value == null)
throw new ArgumentNullException("Content should not be set to null");
_content = value;
}
}
/// <summary>
/// This is mandatory and may never be null
/// </summary>
public Action<string> OnChange
{
get { return _onChange; }
set
{
if (value == null)
throw new ArgumentNullException("OnChange should not be set to null");
_onChange = value;
}
}
}
This way, it would not be possible to explicitly set "OnChange" to null - if this was attempted then an exception would be thrown immediately, right at the point in the code that was attempting to assign this invalid value. This is much better than it failing later on, at some point that depends upon how the user does or doesn't interact with the UI component. This is potentially the sort of mistake that goes unnoticed for some time. For cases that are clearly a "programmer error" bug like this, I much prefer to "fail fast".
There's still a problem, though, because the initial state of the Props class is invalid, since "OnChange" will start as null. If the initialisation code explicitly tries to set it to null then it will fail fast, but if it doesn't set it at all then it remain null and we'll be back to where we started in terms of where and when the exception is raised compared to where the programming mistake was made.
Attempt three could be to set appropriate defaults - eg.
public class Props
{
private string _className = null;
private string _content = "";
private Action<string> _onChange = newValue => { };
/// <summary>
/// This is optional and so may be null (if non-null, then it should be a valid class
/// name - ie. non-blank)
/// </summary>
public string ClassName
{
get { return _className; }
set
{
if (value == "")
throw new ArgumentException("ClassName should not be set to a blank string");
_className = value;
}
}
public bool Disabled { get; set; }
/// <summary>
/// An input may not always have a value and so this may be blank (but it should never
/// be null)
/// </summary>
public string Content
{
get { return _content; }
set
{
if (value == null)
throw new ArgumentNullException("Content should not be set to null");
_content = value;
}
}
/// <summary>
/// This is mandatory and may never be null
/// </summary>
public Action<string> OnChange
{
get { return _onChange; }
set
{
if (value == null)
throw new ArgumentNullException("OnChange should not be set to null");
_onChange = value;
}
}
}
Now it's not possible for "OnChange" to be null, so a null reference exception can not be thrown when the user tries to interact with the component.
This still isn't fantastic, though. Is it really likely that there's ever a time where no "OnChange" value should have been set? Changes are that, in that case, there is still a programmer error (a TextInput is useless without an "OnChange" callback and so one should be set).. but now the error is being silently swallowed.
So, maybe most properties should have to be specified in order to get a new Props instance. Since values have to be provided at the point of initialisation, they may as well be validated at that point. This is a very good argument for making the Props class immutable - eg.
public class Props
{
public Props(string className, string content, bool disabled, Action<string> onChange)
{
if (className == "")
throw new ArgumentException("className should not be set to a blank string");
if (content == null)
throw new ArgumentNullException("content");
if (onChange == null)
throw new ArgumentNullException("onChange");
ClassName = className;
Content = content;
Disabled = disabled;
OnChange = onChange;
}
/// <summary>
/// This is optional and so may be null (if non-null, then it will never be blank)
/// </summary>
public string ClassName { get; }
public bool Disabled { get; }
/// <summary>
/// An input may not always have a value and so this may be blank (but it should never
/// be null)
/// </summary>
public string Content { get; }
/// <summary>
/// This is mandatory and will never be null
/// </summary>
public Action<string> OnChange { get; }
}
Two nice benefits arise from this. Firstly, the comments may be tightened up - so "OnChange" is no longer described as
This is mandatory and should never be null
now it is
This is mandatory and will never be null
It's a seemingly small change, but I'm looking for confidence in the code and this is a positive step from "hopefully this won't happen" to "this can not happen (because an ArgumentNullException would have been instantly thrown if an attempt was made to create a Props instance in this manner)".
The second benefit is that the Props class now communicates one of the React guidelines - the React documentation states that props data should be considered immutable; once a component has a props reference, it should not try to change its data, nor should it expect any other code to be able to change it. Now, that commandment is baked into the code - this is a great example of what I mean when I talk about using a "richer type system", there's more information that may be encoded than just "this class has a property that is of type string".
One final tweak to this sort of approach is to enable optional values to truly be optional. In this example, I'm talking about "className". The constructor may be changed from:
public Props(string className, string content, bool disabled, Action<string> onChange)
{
if (className == "")
throw new ArgumentException("className should not be set to a blank string");
if (content == null)
throw new ArgumentNullException("content");
if (onChange == null)
throw new ArgumentNullException("onChange");
ClassName = className;
Content = content;
Disabled = disabled;
OnChange = onChange;
}
to:
public Props(string content, bool disabled, Action<string> onChange, string className = "")
{
if (content == null)
throw new ArgumentNullException("content");
if (onChange == null)
throw new ArgumentNullException("onChange");
if (className == "")
throw new ArgumentException("className should not be set to a blank string");
Content = content;
Disabled = disabled;
OnChange = onChange;
ClassName = className;
}
This means that an instance of TextInput.Props may be created, if no class name is required, like this:
new TextInput.Props(title, isSaveInProgress, onTitleChange)
Or, if a class name is required:
new TextInput.Props(title, isSaveInProgress, onTitleChange, "title")
Personally, I like to use named constructor arguments when creating Props instances, so I would probably write:
new TextInput.Props(
className: "title",
value: title,
disabled: isSaveInProgress,
onChange: onTitleChange
)
I think that this makes the code easier to read (I don't need to resort to looking at the Props constructor to see that "title" is a class name and not an ID or any other use for a string) and means that it doesn't matter that the "className" argument moved from the start of the constructor argument list to the end, since the position of arguments doesn't matter when their names are used. (As an added benefit, this makes the code slightly more similar to the React component initialisation code that you might see in non-Bridge/C# projects, where JSON objects are used to set properties - but, here, we have the added benefit that's it all typed).
This is a big step forward in terms of including additional information in the type system (and in terms of catching errors quickly - and as close to the root cause of the error as possible), meaning that I can more reliably use a component without having to know everything about how it works (which, in a lot of ways, is like the idea of coding against an interface - you want to know about how to communicate with an interface to get the desired result without having to know all of the details of its implementation).
I'm not completely happy with the code at this point, though. It feels like the Props class has ballooned considerably from:
public class Props
{
public string ClassName;
public bool Disabled;
public string Content;
public Action<string> OnChange;
}
to:
public class Props
{
public Props(
string content,
bool disabled,
Action<string> onChange,
string className = "")
{
if (content == null)
throw new ArgumentNullException("content");
if (onChange == null)
throw new ArgumentNullException("onChange");
if (className == "")
throw new ArgumentException("className should not be set to a blank string");
Content = content;
Disabled = disabled;
OnChange = onChange;
ClassName = className;
}
/// <summary>
/// This is optional and so may be null (if non-null, then it will never be blank)
/// </summary>
public string ClassName { get; }
public bool Disabled { get; }
/// <summary>
/// An input may not always have a value and so this may be blank (but it should never
/// be null)
/// </summary>
public string Content { get; }
/// <summary>
/// This is mandatory and will never be null
/// </summary>
public Action<string> OnChange { get; }
}
While I am willing to make a certain trade-off between the cost of writing the code to begin with against the long term benefits of it being easier to quickly understand and then reason about*, I don't want to have to write any more of this monotonous form of code than absolutely necessary - in particular, I think I would get bored of writing "This is mandatory and will never be null" over and over again on different properties on different classes**.
* (Since "Code is read much more often than it is written, so plan accordingly", I strongly believe that a little extra writing effort is worth it to reduce the more-often-incurred reading effort).
** (I have personally written a lot of code that uses immutable, always-valid types and that was littered with these sorts of comments - while I definitely think it was worth it, I definitely HAVE gotten bored with writing "This will never be null" time after time after time).
But what alternative is there?
Since this whole series is about "The Dan Way", I think that it is entirely reasonable to introduce a library that I've written at this point. It's a NuGet package for Bridge that makes it easier to write immutable types, such as the Props class above: "ProductiveRage.Immutable".
If a class implements an empty interface IAmImmutable then it may access extension methods that make the constructor easier to write. Something along the lines of:
public class Props : IAmImmutable
{
public Props(string content, bool disabled, Action<string> onChange, string className = "")
{
this.CtorSet(_ => _.Content, content);
this.CtorSet(_ => _.Disabled, disabled);
this.CtorSet(_ => _.OnChange, onChange);
this.CtorSet(_ => _.ClassName, className);
}
public string ClassName { get; }
public bool Disabled { get; }
public string Content { get; }
public Action<string> OnChange { get; }
}
The extension method is "CtorSet" and it takes a lambda that specifies a property on the class and it takes a new value for that property. The type of the property and the type of the new value must be consistent - so, although there's apparently a little magic involved, we're not sacrificing any type safety.
One interesting feature of "CtorSet" is that it will never allow a null value. This means the comments from the Props class along the lines of "This will never be null" are unnecessary because an IAmImmutable-implementing class that sets all of its properties in its constructor will never have any null property values.
This actually doesn't work for the Props class we're looking at here since we want "ClassName" to be allowed to be null. To enable that, the library comes with a new struct - Optional<T>. Any time that you want to have a constructor argument be null, you have to wrap its type in this struct - ie.
public class Props : IAmImmutable
{
public Props(
string content,
bool disabled,
Action<string> onChange,
Optional<string> className = new Optional<string>())
{
this.CtorSet(_ => _.Content, content);
this.CtorSet(_ => _.Disabled, disabled);
this.CtorSet(_ => _.OnChange, onChange);
this.CtorSet(_ => _.ClassName, className);
}
public Optional<string> ClassName { get; }
public bool Disabled { get; }
public string Content { get; }
public Action<string> OnChange { get; }
}
Once again, we've made a step forward to encoding additional information in the class itself. In terms of being able to more easily reason about the code, this is a great win - classes such as these will never have null values to worry about; any property that may or may not have a value will be of type Optional<T>, which has properties "IsDefined" (a boolean indicating whether or not it has a value) and "Value" (which is the value itself, so long as "IsDefined" is true).
If you were in an argumentative mood, then you might say that Optional<T> can't save you from nulls in all cases since any code that deals with them could choose not to check "IsDefined" and to just try to access "className.Value" in all cases. This is true, but this faulty style of calling code had to be explicitly written to "work around" the Optional<T> wrapper. If the person who wrote it had sufficiently little interest to try to understand why Optional<T> was used then they may need some help in getting back on to the right path in their programming. This wrapper type acts as a sign post at each point where a null may be encountered, if the sign posts are ignored then that's unfortunate (but the benefit remains that there is a sign post for every potentially-null value whereas, in normal C# code, you need to be wary that nulls may leap out at you at any point, without warning).
This change doesn't affect how you call the constructor if you used the named arguments approach from above, so the following works fine:
new TextInput.Props(
className: "title",
value: title,
disabled: isSaveInProgress,
onChange: onTitleChange
)
as does:
new TextInput.Props(
value: title,
disabled: isSaveInProgress,
onChange: onTitleChange
)
If you wanted to really deliberately indicate that a TextInput should have no title then you could use any of the following three -
// The explicit way
new TextInput.Props(
className: Optional<string>.Missing,
value: title,
disabled: isSaveInProgress,
onChange: onTitleChange
)
// The implicit way (there is an implicit cast from T to Optional<T>, which is why passing
// the string "title" in the earlier example works, because the "title" string is implicitly
// cast to an Optional<string> with value "title" - similarly null is implicitly cast to
// an Optional<string> with value null, which is the same as Optional<string>.Missing)
new TextInput.Props(
className: null,
value: title,
disabled: isSaveInProgress,
onChange: onTitleChange
)
// Using the struct's constructor - the most verbose and least-prefered way. Note that this
// is the only way that a "Missing" value may be specified as a constructor argument's
// default value, as may be seen in the Props constructor above (this is because default
// argument values must be compile time constants, which a new instance of a struct is but
// the static "Missing" property is not. Null can't be used as a constructor argument's
// default value for an Optional as the implicit cast is a runtime operation and so is
// not available at compile time).
new TextInput.Props(
className: new Optional<string>(),
value: title,
disabled: isSaveInProgress,
onChange: onTitleChange
)
I'm still not happy with this, though. I talked earlier about the ambiguity between null and blank string - for the "Value" property, this is solved since it can never be null now. It's valid for it to be a blank string (if there is no content in the input box) but it's not valid for it to be null (an ArgumentNullException will be raised in the constructor for a null "value"). Problem solved. However, the "ClassName" property can be "Optional<string>.Missing" (which is similar to null) or it can be a value that is a blank string. It would be much better to say that "ClassName" is "Missing" (meaning that it has no value and that the DOM element should have a "class" attribute at all) or that it has a value that is not blank.
One way to try to address this would be with another type -
using System;
namespace BridgeReactTutorial.API
{
public class NonBlankTrimmedString
{
public NonBlankTrimmedString(string value)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Null, blank or whitespace-only value specified");
Value = value.Trim();
}
/// <summary>
/// This will never be null, blank or have any leading or trailing whitespace
/// </summary>
public string Value { get; }
/// <summary>
/// It's convenient to be able to pass a NonBlankTrimmedString instance as any argument
/// that requires a string
/// </summary>
public static implicit operator string(NonBlankTrimmedString value)
{
if (value == null)
throw new ArgumentNullException("value");
return value.Value;
}
}
}
I said that I wasn't keen on writing out more of this type of "This will never be null.." summary comment if I could avoid it, but the idea with this class it that it will be applicable in multiple places. So I have had to type "This will never be null, blank or have any leading or trailing whitespace" once again but I will take advantage of this one comment over and over again throughout my code.
Now the TextInput.Props class becomes:
public class Props : IAmImmutable
{
public Props(
string content,
bool disabled,
Action<string> onChange,
Optional<NonBlankTrimmedString> className = new Optional<NonBlankTrimmedString>())
{
this.CtorSet(_ => _.Content, content);
this.CtorSet(_ => _.Disabled, disabled);
this.CtorSet(_ => _.OnChange, onChange);
this.CtorSet(_ => _.ClassName, className);
}
public Optional<NonBlankTrimmedString> ClassName { get; }
public bool Disabled { get; }
public string Content { get; }
public Action<string> OnChange { get; }
}
If you wanted to instantiate one then you would need to change the calling code slightly - eg.
new TextInput.Props(
className: new NonBlankTrimmedString("title"),
value: title,
disabled: isSaveInProgress,
onChange: onTitleChange
)
This does make this code a little more verbose. But we have the benefit that the Props class contains more information about what is and isn't acceptable for its property values. Also, making the calling code more explicit like this forces the writer to consider whether an appropriate value will always be passed to it - they should be careful to pass null or "Optional<NonBlankTrimmedString>.Missing" if they don't want to set a class name and to provide a populated, non-blank string if they do want a class name.
At this point, I'm finally satisfied with the TextInput.Props class!
Note: This is probably the most controversial part of my recommended approach - if you're happy to consider making your classes immutable like this, for the reasons I outlined above (which, by the way, can be applied to all classes in your code, not just props types for React components) and you're willing to consider the benefits and trade-offs of building up a more detailed type system (such as using Optional<NonBlankOrTrimmedString> instead of just "string" and only using "string" to mean "non-nullable string") then I think that you'll enjoy the rest of what I've got to say.
I want to extend this ostracising of nulls to the TextInput class itself, though. At the end of Part Two, the component looked like this:
using System;
using Bridge.Html5;
using Bridge.React;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.Components
{
public class TextInput : StatelessComponent<TextInput.Props>
{
public TextInput(Props props) : base(props) { }
public override ReactElement Render()
{
return DOM.Input(new InputAttributes
{
Type = InputType.Text,
ClassName = props.ClassName,
Disabled = props.Disabled,
Value = props.Content,
OnChange = e => props.OnChange(e.CurrentTarget.Value)
});
}
public class Props : IAmImmutable
{
public string ClassName;
public bool Disabled;
public string Content;
public Action<string> OnChange;
}
}
}
My problem with this is that it's possible to call the constructor with a null "props" value - but, if you do so, then the "Render" method will throw a null reference exception. Unfortunately, it's not possible to check for a null "props" value in the component's constructor due to the way that the Bridge / React bindings work with the React library; the constructor is never actually executed and so a null-check in there would never run. What I suggest is that the Props constructor arguments be repeated in the component's constructor -
using System;
using Bridge.Html5;
using Bridge.React;
using BridgeReactTutorial.API;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.Components
{
public class TextInput : StatelessComponent<TextInput.Props>
{
public TextInput(
string content,
bool disabled,
Action<string> onChange,
Optional<NonBlankTrimmedString> className = new Optional<NonBlankTrimmedString>())
: base(new Props(content, disabled, onChange, className)) { }
public override ReactElement Render()
{
return DOM.Input(new InputAttributes
{
Type = InputType.Text,
ClassName = props.ClassName.IsDefined ? props.ClassName.Value : null,
Disabled = props.Disabled,
Value = props.Content,
OnChange = e => props.OnChange(e.CurrentTarget.Value)
});
}
public class Props : IAmImmutable
{
public Props(
string content,
bool disabled,
Action<string> onChange,
Optional<NonBlankTrimmedString> className)
{
this.CtorSet(_ => _.Content, content);
this.CtorSet(_ => _.Disabled, disabled);
this.CtorSet(_ => _.OnChange, onChange);
this.CtorSet(_ => _.ClassName, className);
}
public Optional<NonBlankTrimmedString> ClassName { get; }
public bool Disabled { get; }
public string Content { get; }
public Action<string> OnChange { get; }
}
}
}
Again, this has expanded the amount of code that is required within the component class. But the real effect is magnified by the line-wrapping that I use on my blog post code samples - in Visual Studio, I would likely have the constructor arguments all on one line.
If we can get past the cost of additional code within the component then we get two benefits. The first is that it's no longer possible for the TextInput to ever have a null "props" reference as a Props reference is no longer passed in, but is created in the call to the base constructor by using the individual arguments passed to the TextInput constructor. The second benefit is more marginal, but still nice (especially since it partially offsets the additional code added above) - the way that a new TextInput was declared previously required duplication of the word "TextInput" (with "new TextInput" and "new TextInput.Props") -
new TextInput(new TextInput.Props
{
ClassName = "title"
Disabled = props.Disabled,
Content = props.Content,
OnChange = props.OnChange
})
With the updated TextInput implementation, this duplication is avoided -
new TextInput(
className: new NonBlankTrimmedString("title"),
disabled: props.Disabled,
content: props.Content,
onChange: props.OnChange
)
Even without this (admittedly minor) second benefit, I would still be much happier with the new version of TextInput. The additional code (the final version is definitely somewhat longer than the previous version) pays for itself in what it communicates to someone who wishes to consume that component. However, one of the themes that I've been pushing in this series is that components should be dumb and that the real logic of the application should be outside of any UI classes; in the application code that will deal with the complications of the business logic and how to deal with user interactions.. if there is a way to move some of the syntactic noise around component creation away from the complicated library code and into the dumb components, then that seems a sensible trade-off. And that's what has been done here!
There's actually a third benefit to using this "IAmImmutable style" for writing these data types, when it comes to passing events from the simple components all the way up to the top of the component tree, where each "OnChange" (or whatever) adds increasing detail on the way up - but I'll come to that later on, first I want to address a burning question:
Trick question! I am convinced that it does make sense to use IAmImmutable for data types throughout the application and to make null arguments and properties unacceptable in all places.
One obvious example is in the "SaveMessage" method in the MessageApi class. Currently, it starts like this:
private RequestId SaveMessage(MessageDetails message, Action optionalSaveCompletedCallback)
{
if (message == null)
throw new ArgumentNullException("message");
if (string.IsNullOrWhiteSpace(message.Title))
throw new ArgumentException("A title value must be provided");
if (string.IsNullOrWhiteSpace(message.Content))
throw new ArgumentException("A content value must be provided");
Not accepting a null MessageDetails instance is good - if someone tried to call this method with a null "message" argument then it should be rejected immediately. However, it feels wrong that it's necessary to then check the "Title" and "Content" properties - should there ever be a case where a MessageDetails instance exists without these values being populated? In this application, the answer is no - the MessageEditor component only allows a new message to be saved if both its Title and Content have values.
This is another opportunity to encode this additional information into the type system. Instead of MessageDetails being implemented like this:
namespace BridgeReactTutorial.ViewModels
{
public class MessageDetails
{
public string Title;
public string Content;
}
}
It should be rewritten thusly:
using BridgeReactTutorial.API;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.ViewModels
{
public class MessageDetails : IAmImmutable
{
public MessageDetails(NonBlankTrimmedString title, NonBlankTrimmedString content)
{
this.CtorSet(_ => _.Title, title);
this.CtorSet(_ => _.Content, content);
}
public NonBlankTrimmedString Title { get; }
public NonBlankTrimmedString Content { get; }
}
}
Now, the "SaveMessage" validation is much simpler - all that is required is:
private RequestId SaveMessage(MessageDetails message, Action optionalSaveCompletedCallback)
{
if (message == null)
throw new ArgumentNullException("message");
Since a MessageDetails instance may no longer exist with missing Title or Content values, the property validation in "SaveMessage" is unnecessary. This has the benefit that there is less code at the point at which data is retrieved from a MessageDetails instance, which goes some way to offsetting the additional code required in defining the type. The real benefit, though, to removing that code is not just reducing line count but in removing potential duplication (the property validation code may have appeared elsewhere in the application if there were other methods that processed MessageDetails instances) and baking assumptions into the type system, rather than leaving them be implicit. Before, it was probably safe to assume that Title and Content would always have values since the code that would create a MessageDetails instance would always give them values - however, that was only an assumption and you would have had to have read all of the code that created MessageDetails instances to be confident. With this arrangement, you know that a MessageDetails has both Title and Content values at all times, since it's not possible for an instance to be created that doesn't!
When I talk about code being easy to reason about, it's not usually in terms of the dozen or so lines of code that are directly in front of you, it's how the objects and methods that you're dealing with may interact with the rest of the system and what assumptions are being made. Knowing that a MessageDetails instance will always be valid is extremely helpful. Knowing that any code that attempts to create a MessageDetails with invalid data will fail immediately, rather than cause an error later on (when the instance is presumed to be valid but turns out not to be) is extremely helpful. Knowing that a type is immutable and that it won't be changed "behind the scenes" when you pass it off to another method is extremely helpful - when types are mutable and you pass an instance to another method to read, you can't be sure whether the method will only read it or whether it will manipulate it; there's no way to tell from the method signature. Making mutations explicit by making types immutable is another big win for being able to reason about code.
Speaking of dealing with mutation brings me smoothly onto the third benefit of IAmImmutable that I hinted at earlier. Currently, the MessageEditor component in our example app looks like this:
using System;
using Bridge.React;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Components
{
public class MessageEditor : StatelessComponent<MessageEditor.Props>
{
public MessageEditor(Props props) : base(props) { }
public override ReactElement Render()
{
var formIsInvalid =
!string.IsNullOrWhiteSpace(props.Message.Title.ValidationError) ||
!string.IsNullOrWhiteSpace(props.Message.Content.ValidationError);
return DOM.FieldSet(new FieldSetAttributes { ClassName = props.ClassName },
DOM.Legend(null, props.Message.Caption),
DOM.Span(new Attributes { ClassName = "label" }, "Title"),
new ValidatedTextInput(new ValidatedTextInput.Props
{
ClassName = "title",
Disabled = props.Message.IsSaveInProgress,
Content = props.Message.Title.Text,
OnChange = newTitle => props.OnChange(new MessageEditState
{
Title = new TextEditState { Text = newTitle },
Content = props.Message.Content
}),
ValidationMessage = props.Message.Title.ValidationError
}),
DOM.Span(new Attributes { ClassName = "label" }, "Content"),
new ValidatedTextInput(new ValidatedTextInput.Props
{
ClassName = "content",
Disabled = props.Message.IsSaveInProgress,
Content = props.Message.Content.Text,
OnChange = newContent => props.OnChange(new MessageEditState
{
Title = props.Message.Title,
Content = new TextEditState { Text = newContent },
}),
ValidationMessage = props.Message.Content.ValidationError
}),
DOM.Button(
new ButtonAttributes
{
Disabled = formIsInvalid || props.Message.IsSaveInProgress,
OnClick = e => props.OnSave()
},
"Save"
)
);
}
public class Props
{
public string ClassName;
public MessageEditState Message;
public Action<MessageEditState> OnChange;
public Action OnSave;
}
}
}
The first thing I'm going to do is change the Props type and the component's constructor in the same way as I did for the TextInput -
using System;
using Bridge.React;
using BridgeReactTutorial.API;
using BridgeReactTutorial.ViewModels;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.Components
{
public class MessageEditor : StatelessComponent<MessageEditor.Props>
{
public MessageEditor(
MessageEditState message,
Action<MessageEditState> onChange,
Action onSave,
Optional<NonBlankTrimmedString> className = new Optional<NonBlankTrimmedString>())
: base(new Props(className, message, onChange, onSave)) { }
public override ReactElement Render()
{
var formIsInvalid =
!string.IsNullOrWhiteSpace(props.Message.Title.ValidationError) ||
!string.IsNullOrWhiteSpace(props.Message.Content.ValidationError);
return DOM.FieldSet(
new FieldSetAttributes {
ClassName = props.ClassName.IsDefined ? props.ClassName.Value : null
},
DOM.Legend(null, props.Message.Caption),
DOM.Span(new Attributes { ClassName = "label" }, "Title"),
new ValidatedTextInput(new ValidatedTextInput.Props
{
ClassName = "title",
Disabled = props.Message.IsSaveInProgress,
Content = props.Message.Title.Text,
OnChange = newTitle => props.OnChange(new MessageEditState
{
Title = new TextEditState { Text = newTitle },
Content = props.Message.Content
}),
ValidationMessage = props.Message.Title.ValidationError
}),
DOM.Span(new Attributes { ClassName = "label" }, "Content"),
new ValidatedTextInput(new ValidatedTextInput.Props
{
ClassName = "content",
Disabled = props.Message.IsSaveInProgress,
Content = props.Message.Content.Text,
OnChange = newContent => props.OnChange(new MessageEditState
{
Title = props.Message.Title,
Content = new TextEditState { Text = newContent },
}),
ValidationMessage = props.Message.Content.ValidationError
}),
DOM.Button(
new ButtonAttributes
{
Disabled = formIsInvalid || props.Message.IsSaveInProgress,
OnClick = e => props.OnSave()
},
"Save"
)
);
}
public class Props : IAmImmutable
{
public Props(
Optional<NonBlankTrimmedString> className,
MessageEditState message,
Action<MessageEditState> onChange,
Action onSave)
{
this.CtorSet(_ => _.ClassName, className);
this.CtorSet(_ => _.Message, message);
this.CtorSet(_ => _.OnChange, onChange);
this.CtorSet(_ => _.OnSave, onSave);
}
public Optional<NonBlankTrimmedString> ClassName { get; }
public MessageEditState Message { get; }
public Action<MessageEditState> OnChange { get; }
public Action OnSave { get; }
}
}
}
This means that the MessageEditor instantiation code changes slightly from:
new MessageEditor(new MessageEditor.Props
{
ClassName = "message",
Message = state.Message,
OnChange = newState => props.Dispatcher.Dispatch(
UserEditRequested.For(newState)
),
OnSave = () => props.Dispatcher.Dispatch(
SaveRequested.For(
new MessageDetails(
new NonBlankTrimmedString(state.Message.Title.Text),
new NonBlankTrimmedString(state.Message.Content.Text)
)
)
)
}),
to:
new MessageEditor(
className: new NonBlankTrimmedString("message"),
message: state.Message,
onChange: newState => props.Dispatcher.Dispatch(
UserEditRequested.For(newState)
),
onSave: () => props.Dispatcher.Dispatch(
SaveRequested.For(
new MessageDetails(
new NonBlankTrimmedString(state.Message.Title.Text),
new NonBlankTrimmedString(state.Message.Content.Text)
)
)
)
),
There are several steps that need following now until I can reveal my point, so bear with me. I'm going to change the MessageEditState data type, in the same way as I did the MessageDetails - from:
namespace BridgeReactTutorial.ViewModels
{
public class MessageEditState
{
public string Caption;
public TextEditState Title;
public TextEditState Content;
public bool IsSaveInProgress;
}
}
to:
using BridgeReactTutorial.API;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.ViewModels
{
public class MessageEditState : IAmImmutable
{
public MessageEditState(
NonBlankTrimmedString caption,
TextEditState title,
TextEditState content,
bool isSaveInProgress)
{
this.CtorSet(_ => _.Caption, caption);
this.CtorSet(_ => _.Title, title);
this.CtorSet(_ => _.Content, content);
this.CtorSet(_ => _.IsSaveInProgress, isSaveInProgress);
}
public NonBlankTrimmedString Caption { get; }
public TextEditState Title { get; }
public TextEditState Content { get; }
public bool IsSaveInProgress { get; }
}
}
And do the same with TextEditState, from -
namespace BridgeReactTutorial.ViewModels
{
public class TextEditState
{
public string Text;
public string ValidationError;
}
}
to:
using BridgeReactTutorial.API;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.ViewModels
{
public class TextEditState : IAmImmutable
{
public TextEditState(
string text,
Optional<NonBlankTrimmedString> validationError = new Optional<NonBlankTrimmedString>())
{
this.CtorSet(_ => _.Text, text);
this.CtorSet(_ => _.ValidationError, validationError);
}
public string Text { get; }
public Optional<NonBlankTrimmedString> ValidationError { get; }
}
}
I'm going to change the ValidatedTextInput to
using System;
using Bridge.React;
using BridgeReactTutorial.API;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.Components
{
public class ValidatedTextInput : StatelessComponent<ValidatedTextInput.Props>
{
public ValidatedTextInput(
bool disabled,
string content,
Action<string> onChange,
Optional<NonBlankTrimmedString> validationMessage,
Optional<NonBlankTrimmedString> className = new Optional<NonBlankTrimmedString>())
: base(new Props(className, disabled, content, onChange, validationMessage)) { }
public override ReactElement Render()
{
var className = props.ClassName;
if (props.ValidationMessage.IsDefined)
className = className.Add(" ", new NonBlankTrimmedString("invalid"));
return DOM.Span(new Attributes { ClassName = className.ToStringIfDefined() },
new TextInput(
className: props.ClassName,
disabled: props.Disabled,
content: props.Content,
onChange: props.OnChange
),
props.ValidationMessage.IsDefined
? DOM.Span(
new Attributes { ClassName = "validation-message" },
props.ValidationMessage.ToStringIfDefined()
)
: null
);
}
public class Props : IAmImmutable
{
public Props(
Optional<NonBlankTrimmedString> className,
bool disabled,
string content,
Action<string> onChange,
Optional<NonBlankTrimmedString> validationMessage)
{
this.CtorSet(_ => _.ClassName, className);
this.CtorSet(_ => _.Disabled, disabled);
this.CtorSet(_ => _.Content, content);
this.CtorSet(_ => _.OnChange, onChange);
this.CtorSet(_ => _.ValidationMessage, validationMessage);
}
public Optional<NonBlankTrimmedString> ClassName { get; }
public bool Disabled { get; }
public string Content { get; }
public Action<string> OnChange { get; }
public Optional<NonBlankTrimmedString> ValidationMessage { get; }
}
}
}
.. which requires a new class be added to the "API" folder with some extension methods to make dealing with Optional<NonBlankTrimmedString> a little nicer -
using System;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.API
{
public static class OptionalNonBlankTrimmedStringExtensions
{
/// <summary>
/// If the Optional NonBlankTrimmedString has a value then it will be unwrapped directly
/// into a string - if not, the null will be returned (this is one of the few places
/// where null will be an acceptable value in the app and it should be only used when
/// integrating with code that expects nulls - such as when setting attributes via
/// React html element factories)
/// </summary>
public static string ToStringIfDefined(this Optional<NonBlankTrimmedString> source)
{
return source.IsDefined ? source.Value : null;
}
/// <summary>
/// This will join two Optional NonBlankTrimmedString with a specified delimiter if
/// they both have values. If only one of them has a value then this will be returned
/// unaltered. If neither of them have a value then a Missing value will be returned.
/// </summary>
public static Optional<NonBlankTrimmedString> Add(
this Optional<NonBlankTrimmedString> source,
string delimiter,
Optional<NonBlankTrimmedString> other)
{
if (delimiter == null)
throw new ArgumentNullException("delimiter");
if (!source.IsDefined && !other.IsDefined)
return Optional<NonBlankTrimmedString>.Missing;
else if (!source.IsDefined)
return other;
else if (!other.IsDefined)
return source;
return new NonBlankTrimmedString(source.Value.Value + delimiter + other.Value.Value);
}
}
}
and a further implicit operator adding to the NonBlankTrimmedString -
/// <summary>
/// It's convenient to be able to pass a NonBlankTrimmedString instance as any argument
/// that requires a ReactElement-or-string, such as for the children array of the React
/// DOM component factories
/// </summary>
public static implicit operator Union<ReactElement, string>(NonBlankTrimmedString value)
{
if (value == null)
throw new ArgumentNullException("value");
return value.Value;
}
Ok, now I'm finally able to demonstrate this mysterious third benefit. The "OnChange" lambdas which were provided as ValidatedTextInput.Props values by the MessageEditor's "Render" method were previously specified like this:
OnChange = newTitle => props.OnChange(new MessageEditState
{
Title = new TextEditState { Text = newTitle },
Content = props.Message.Content
})
OnChange = newContent => props.OnChange(new MessageEditState
{
Title = props.Message.Title,
Content = new TextEditState { Text = newContent },
})
Within each "OnChange", we want to create a new MessageEditState instance with one of the properties changed. However, it get arduous having to repeat all of the property names each time that you want to change a single property - here it's not that bad because there are only two properties ("Title" and "Content"), but on classes with more properties this is annoying and, worse, error-prone.
Now that MessageEditState implements IAmImmutable, we can take advantage of another extension method available; "With". This takes an argument that specifies the property to change and it takes an argument for the new property value. This means that
OnChange = newTitle => props.OnChange(new MessageEditState
{
Title = new TextEditState { Text = newTitle },
Content = props.Message.Content
})
is replaced with
OnChange = newTitle => props.OnChange(
props.Message.With(_ => _.Title, new TextEditState(newTitle))
)
and
OnChange = newContent => props.OnChange(new MessageEditState
{
Title = props.Message.Title,
Content = new TextEditState { Text = newContent }
})
is replaced with
OnChange = newContent => props.OnChange(
props.Message.With(_ => _.Content, new TextEditState(newContent))
)
(Again, I'm only wrapping these lines for the sake of the formatting on my blog - if I was writing this code in Visual Studio then I would make those a single line each).
The "With" function takes an instance of an IAmImmutable-implementing class, clones it but changes the specified property value - unless the new value is the same as the old value, in which case it returns the original instance unaltered.
All of these changes combined mean that the MessageEditor component now becomes:
using System;
using Bridge.React;
using BridgeReactTutorial.API;
using BridgeReactTutorial.ViewModels;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.Components
{
public class MessageEditor : StatelessComponent<MessageEditor.Props>
{
public MessageEditor(
MessageEditState message,
Action<MessageEditState> onChange,
Action onSave,
Optional<NonBlankTrimmedString> className = new Optional<NonBlankTrimmedString>())
: base(new Props(className, message, onChange, onSave)) { }
public override ReactElement Render()
{
var formIsInvalid =
props.Message.Title.ValidationError.IsDefined ||
props.Message.Content.ValidationError.IsDefined;
return DOM.FieldSet(
new FieldSetAttributes { ClassName = props.ClassName.ToStringIfDefined() },
DOM.Legend(null, props.Message.Caption),
DOM.Span(new Attributes { ClassName = "label" }, "Title"),
new ValidatedTextInput(
className: new NonBlankTrimmedString("title"),
disabled: props.Message.IsSaveInProgress,
content: props.Message.Title.Text,
onChange: newTitle => props.OnChange(
props.Message.With(_ => _.Title, new TextEditState(newTitle))
),
validationMessage: props.Message.Title.ValidationError
),
DOM.Span(new Attributes { ClassName = "label" }, "Content"),
new ValidatedTextInput(
className: new NonBlankTrimmedString("content"),
disabled: props.Message.IsSaveInProgress,
content: props.Message.Content.Text,
onChange: newContent => props.OnChange(
props.Message.With(_ => _.Content, new TextEditState(newContent))
),
validationMessage: props.Message.Content.ValidationError
),
DOM.Button(
new ButtonAttributes
{
Disabled = formIsInvalid || props.Message.IsSaveInProgress,
OnClick = e => props.OnSave()
},
"Save"
)
);
}
public class Props : IAmImmutable
{
public Props(
Optional<NonBlankTrimmedString> className,
MessageEditState message,
Action<MessageEditState> onChange,
Action onSave)
{
this.CtorSet(_ => _.ClassName, className);
this.CtorSet(_ => _.Message, message);
this.CtorSet(_ => _.OnChange, onChange);
this.CtorSet(_ => _.OnSave, onSave);
}
public Optional<NonBlankTrimmedString> ClassName { get; }
public MessageEditState Message { get; }
public Action<MessageEditState> OnChange { get; }
public Action OnSave { get; }
}
}
}
Before moving on, I want to apply these changes to one more component to really drive the point home.
This is the MessageHistory component as it currently stands:
using System;
using System.Collections.Generic;
using System.Linq;
using Bridge.React;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Components
{
public class MessageHistory : StatelessComponent<MessageHistory.Props>
{
public MessageHistory(Props props) : base(props) { }
public override ReactElement Render()
{
var className = props.ClassName;
if (!props.Messages.Any())
className = (className + " zero-messages").Trim();
// Any time a set of child components is dynamically-created (meaning that the
// numbers of items may vary from one render to another), each must have a unique
// "Key" property set (this may be a int or a string). Here, this is simple as
// each message tuple is a unique ID and the contents of that message.
var messageElements = props.Messages
.Select(idAndMessage => DOM.Div(new Attributes { Key = idAndMessage.Item1 },
DOM.Span(new Attributes { ClassName = "title" }, idAndMessage.Item2.Title),
DOM.Span(new Attributes { ClassName = "content" }, idAndMessage.Item2.Content)
));
// When child components are specified (as they are through the second argument of
// DOM.Div), the argument is of type Union<ReactElement, string>[] (meaning that each
// element may be another component or it may be a simple text value)
// - The React bindings have an extension method that transforms an IEnumerable set
// of components (such as "messageElements") into an Union<ReactElement, string>[]
return DOM.FieldSet(new FieldSetAttributes { ClassName = className },
DOM.Legend(null, "Message History"),
DOM.Div(null, messageElements)
);
}
public class Props
{
public string ClassName;
public IEnumerable<Tuple<int, MessageDetails>> Messages;
}
}
}
Despite this appearing very simple at first glance, there are various implicit assumptions that you should be aware of. Firstly, it is assumed that "props" will never be null ("Render" will throw an exception if this is not the case). It is also assumed that "props.ClassName" may be null (and, technically, it may also be a blank string, though this is not desirable) while "props.Messages" should not null. Nor should "props.Messages" contain any tuples with a null MessageDetails instance. But these assumptions are neither documented nor enforced.
By this point, we've seen several examples of how to prevent "props" being null (ie. require that the props constructor arguments be passed as the component's constructor arguments) and we've seen how to better represent Props to allow "ClassName" to be optional but for "Messages" to not be ("ClassName" should be an Optional<NonBlankTrimmedString>). But there are two further tricks we can use for the MessageHistory.Props.
Firstly, IEnumerable is too loose for my liking - technically, there are no guarantees that an IEnumerable will report the same information if enumerated multiple times and there are definitely no guarantees that it won't contain any null references. I want consistency and I want a life free from null. The ProductiveRage.Immutable library contains another handy class for this sort of thing; NonNullList<T>. This is essentially an ordered list of items of type "T" that is immutable and that will never contain any null values. If you want a set of items that may or may not have values of type "T" then you need the list type to be NonNullList<Optional<T>>.
The second tweak that I want to make is to replace the Tuple<int, MessageDetails> - in part, again, because there is no guarantee that the second item in the pair will not be null but also because I don't like the "Item1" and "Item2" property names. I think it's just one more thing to mentally translate ("Oh yes, Item1 means Key and Item2 means Message"). So I'm going to extend the type system again.
When considering a simple API, the common actions are "Create", "Read", "Update", "Delete". When creating a new item (like when we save a new message in our example application), we don't have a unique key for the new message - that will be generated by the persistence layer as part of the save process. When we read values (to display existing messages in the MessageHistory, for example), we will have access to unique keys - the persistence layer will be reading data from wherever the data is stored and it will be able to draw the keys out along with the data. When updating an existing record, we should know what its key is, since we will have performed a read action in order to get the currently-persisted state for the record. Similarly, when requesting a delete, we will have the key from a previous read action, in order to know what record to remove.
I've seen object models before which try to have a single data type to use in all of the Create, Read and Update cases. This would be like our MessageDetails having a "Key", "Title" and "Content". However, sometimes the "Key" would be null because it would be unknown (when generating a brand new MessageDetails instance to pass to "SaveMessage", for example). I don't like this. The sometimes-Key-is-null-and-sometimes-it-isn't is an unnecessary complication and it means that there are places where we require a Key but can't guarantee (through the type system) that the reference that we have will have a non-null Key value. I think it's much better to have two data types; one for a record that has been persisted at some point (and thus has a non-null Key) and another type for a record that may or may not have been persisted. Currently, our MessageDetails class (which has only "Title" and "Content" properties) represents a message that may or may not have been persisted - when a new one is passed to "SaveMessage" when the user attempts to save a new message then we know that it hasn't been persisted yet, but it's not difficult to imagine that there could be other code that we add to the application in the future that wants to deal with some message data, but that doesn't care whether it's been persisted or not yet; it only wants access to its "Title" and / or "Content" values, it doesn't need the "Key" for anything.
So, instead of the MessageHistory using the generic Tuple class to represent a MessageDetails-plus-persisted-Key, I'm going to introduce something new. Create a new file under the "API" folder, "Saved.cs" -
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.API
{
public class Saved<TKey, TValue> : IAmImmutable
{
public Saved(TKey key, TValue value)
{
this.CtorSet(_ => _.Key, key);
this.CtorSet(_ => _.Value, value);
}
public TKey Key { get; }
public TValue Value { get; }
}
public static class Saved
{
/// <summary>
/// This generic method makes code that creates generic Saved instances more succinct
/// by relying upon type inference (based upon the key and value argument types), so
/// that the calling code does not have to explicitly declare TKey and TValue
/// </summary>
public static Saved<TKey, TValue> For<TKey, TValue>(TKey key, TValue value)
{
return new Saved<TKey, TValue>(key, value);
}
}
}
The Saved class makes differentiating between record-that-has-a-persistence-id and record-that-may-or-may-not-have-a-persistence-id simple. If the message has been persisted, then it may be represented as a Saved<int, MessageDetails>. If it's just the message data, with no persisted-or-not-persisted state associated with it then it will be simply a MessageDetails.
I'm still not happy, though. I think that Saved<int, MessageDetails> could still be more descriptive. This value represents a message with a unique persistence key for that message. Even if the underlying data store is a database which uses an integer column (in our example app, it's a simple in-browser-memory store, but a database on a server is likely much more common) that doesn't mean that we have to use such a vague term as "an integer" in our application's object model. I recommend strongly-typed ID representations. We need to add a new file "MessageId.cs" to the "API" folder:
namespace BridgeReactTutorial.API
{
public struct MessageId
{
public int Value { get; private set; }
public static explicit operator MessageId(int value)
{
return new MessageId { Value = value };
}
public static implicit operator int(MessageId id)
{
return id.Value;
}
}
}
This is the final step in the move away from IEnumerable<Tuple<int, MessageDetails>>, we will now represent this data with the type NonNullList<Saved<MessageId, MessageDetails>>. This list will never contain any null "Saved" instances and a "Saved" instance will never contain a null message. This list of messages will never vary, which is another way that React's "consider props to be immutable" guidelines is described and enforced in the type system.
Not only do I believe that having strongly-typed IDs makes the code clearer in cases like this but it can also avoid silly mistakes that have a nasty tendency to crop up from time to time - if you're writing code and having a bad day, then it's easy to accidentally pass the wrong ID around. For example, if I have a function:
void RecordMessageAsHavingBeenReadBy(int messageId, int userId)
then it's possible in the calling code to mix up the IDs if you're having a bad day (this isn't too contrived an example, I have done something like this in the past!) - eg.
RecordMessageAsHavingBeenReadBy(user.Id, message.id); // Whoops!
If the IDs were strongly-typed, meaning that the method signature would be..
void RecordMessageAsHavingBeenReadBy(MessageId messageId, UserId userId)
.. then that mishap would result in a compile error, rather than runtime confusion that may not get noticed immediately.
(Note: These changes to how messages are represented will require changes to the MessageApi, which I'll cover shortly - nothing very complicated, though).
These changes lead the MessageHistory component's code to now look like this:
using System.Linq;
using Bridge.React;
using BridgeReactTutorial.API;
using BridgeReactTutorial.ViewModels;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.Components
{
public class MessageHistory : StatelessComponent<MessageHistory.Props>
{
public MessageHistory(
NonNullList<Saved<MessageId, MessageDetails>> messages,
Optional<NonBlankTrimmedString> className = new Optional<NonBlankTrimmedString>())
: base(new Props(className, messages)) { }
public override ReactElement Render()
{
var className = props.ClassName;
if (!props.Messages.Any())
className = className.Add(" ", new NonBlankTrimmedString("zero-messages"));
// Any time a set of child components is dynamically-created (meaning that the
// numbers of items may vary from one render to another), each must have a unique
// "Key" property set (this may be a int or a string)
var messageElements = props.Messages
.Select(savedMessage => DOM.Div(new Attributes { Key = (int)savedMessage.Key },
DOM.Span(new Attributes { ClassName = "title" }, savedMessage.Value.Title),
DOM.Span(new Attributes { ClassName = "content" }, savedMessage.Value.Content)
));
// When child components are specified (as they are through the second argument of
// DOM.Div), the argument is of type Union<ReactElement, string>[] (meaning that each
// element may be another component or it may be a simple text value)
// - The React bindings have an extension method that transforms an IEnumerable set
// of components (such as "messageElements") into an Union<ReactElement, string>[]
return DOM.FieldSet(
new FieldSetAttributes { ClassName = className.ToStringIfDefined() },
DOM.Legend(null, "Message History"),
DOM.Div(null, messageElements)
);
}
public class Props : IAmImmutable
{
public Props(
Optional<NonBlankTrimmedString> className,
NonNullList<Saved<MessageId, MessageDetails>> messages)
{
this.CtorSet(_ => _.ClassName, className);
this.CtorSet(_ => _.Messages, messages);
}
public Optional<NonBlankTrimmedString> ClassName { get; }
public NonNullList<Saved<MessageId, MessageDetails>> Messages { get; }
}
}
}
All of those implicit assumptions are now explicitly described in the type system. This makes me feel much better.
I introduced the use of IAmImmutable in terms of a component's Props class. But I subsequently used it to tighten up the MessageDetails class and then again when the Saved class was added.
One option in incorporating IAmImmutable and this no-value-or-property-may-be-null behaviour into applications would be to say that any class that implements IAmImmutable will not allow null anywhere. As I've already hinted, I strongly suggest going further than that, however, and writing all code like this. Frankly, I can see no good reason why any public data type should be mutable. I can imagine that, in some special cases, it may be desirable to have some private mutable data structures for convenience, or maybe performance (in some very specialised cases) but where the data is shared with other classes, data types being immutable makes the code much easier to reason about. Transformations are explicit and do not occur "in place" for any references. Meaning that a reference that describes some data when a function starts will always describe the same data when the function ends.
It's actually worth remembering that JavaScript in the browser is single-threaded. A lot of the time that people talk about the benefits of immutability, they talk about the safety of being able to share references between multiple threads and not having to worry about corruption because one thread can't manipulate data in a way that another thread doesn't expect, with unfortunate (and often non-deterministic) results. Here, we are not concerned about multi-threading, I recommend the use of immutable structures solely because they make the code that accesses them and passes them around easier to reason about.
The largest downside in my eyes, as may have struck you after reading all of the above, is that changing code that doesn't use IAmImmutable into code that does use it requires changes not only to that particular class but, in many cases, to code that accesses or initialises that class and then to code that accesses or initialises that code (the changes to the MessageEditor and MessageHistory components required changes to the MessageDetails and MessageEditState classes and still require more changes to the AppContainer, the MessageWriterStore and the MessageApi). It's much better to bake this in from the start. The big benefit is that, if you do so, you'll rarely have to worry about "could this value be null"* and "could this data be changed if I pass it into another function".
* (There will still be some places where you have to be alert about potential nulls, but these should largely arise from interacting with other libraries - the "ToStringIfDefined" extension method we saw earlier is an example of a place where nulls may be returned, but it is clearly documented as such and the return value is only intended to be passed to a React element factory method).
If you've been following along and creating your own project with the code in this series, you will be all too aware that it doesn't build at the moment. Let's go through and fix everything up. Much of the required alterations will be similar to what is presented above, but there are a few other tips and tricks to consider along the way.
Let's start with the AppContainer. Last we saw it, it looked like this:
using System;
using System.Collections.Generic;
using Bridge.React;
using BridgeReactTutorial.Actions;
using BridgeReactTutorial.ViewModels;
using BridgeReactTutorial.Stores;
using BridgeReactTutorial.API;
namespace BridgeReactTutorial.Components
{
public class AppContainer : Component<AppContainer.Props, AppContainer.State>
{
public AppContainer(AppContainer.Props props) : base(props) { }
protected override void ComponentDidMount()
{
props.Store.Change += StoreChanged;
}
protected override void ComponentWillUnmount()
{
props.Store.Change -= StoreChanged;
}
private void StoreChanged()
{
SetState(new State
{
Message = props.Store.Message,
MessageHistory = props.Store.MessageHistory
});
}
public override ReactElement Render()
{
if (state == null)
return null;
return DOM.Div(null,
new MessageEditor(
className: new NonBlankTrimmedString("message"),
message: state.Message,
onChange: newState => props.Dispatcher.Dispatch(
UserEditRequested.For(newState)
),
onSave: () => props.Dispatcher.Dispatch(
SaveRequested.For(
new MessageDetails(
new NonBlankTrimmedString(state.Message.Title.Text),
new NonBlankTrimmedString(state.Message.Content.Text)
)
)
)
),
new MessageHistory(new MessageHistory.Props
{
ClassName = "history",
Messages = state.MessageHistory
})
);
}
public class Props
{
public AppDispatcher Dispatcher;
public MessageWriterStore Store;
}
public class State
{
public MessageEditState Message;
public IEnumerable<Tuple<int, MessageDetails>> MessageHistory;
}
}
}
It's nice and simple since we moved nearly all of the logic that it contained in Part One into the MessageWriterStore in Part Two.
The obvious thing to do, based on what I've been talking about today, is to change its Props and State classes to implement IAmImmutable.
After that, there is one difference between this component and the other components we've already looked at - this is stateful and they were state-less. That means that they only had "props" to think about, while the AppContainer has both "props" and "state". As with the stateless components, it is presumed that the "props" reference may never be null. This can be enforced by using the same trick as we did for the others - mirror the Props constructor arguments in the AppContainer's constructor arguments and generate a Props instance from them. However, the "state" reference may be null some times, as can be seen at the very start of the "Render" method. This means that the "state" type should be Optional<State>, rather than just State.
These changes result in this:
using Bridge.React;
using BridgeReactTutorial.Actions;
using BridgeReactTutorial.API;
using BridgeReactTutorial.Stores;
using BridgeReactTutorial.ViewModels;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.Components
{
public class AppContainer : Component<AppContainer.Props, Optional<AppContainer.State>>
{
public AppContainer(AppDispatcher dispatcher, MessageWriterStore store)
: base(new Props(dispatcher, store)) { }
protected override void ComponentDidMount()
{
props.Store.Change += StoreChanged;
}
protected override void ComponentWillUnmount()
{
props.Store.Change -= StoreChanged;
}
private void StoreChanged()
{
SetState(new State(
message: props.Store.Message,
messageHistory: props.Store.MessageHistory
));
}
public override ReactElement Render()
{
if (!state.IsDefined)
return null;
return DOM.Div(null,
new MessageEditor(
className: new NonBlankTrimmedString("message"),
message: state.Value.Message,
onChange: newState => props.Dispatcher.Dispatch(
UserEditRequested.For(newState)
),
onSave: () => props.Dispatcher.Dispatch(
SaveRequested.For(
new MessageDetails(
new NonBlankTrimmedString(state.Value.Message.Title.Text),
new NonBlankTrimmedString(state.Value.Message.Content.Text)
)
)
)
),
new MessageHistory(
className: new NonBlankTrimmedString("history"),
messages: state.Value.MessageHistory
)
);
}
public class Props : IAmImmutable
{
public Props(AppDispatcher dispatcher, MessageWriterStore store)
{
this.CtorSet(_ => _.Dispatcher, dispatcher);
this.CtorSet(_ => _.Store, store);
}
public AppDispatcher Dispatcher { get; }
public MessageWriterStore Store { get; }
}
public class State : IAmImmutable
{
public State(
MessageEditState message,
NonNullList<Saved<MessageId, MessageDetails>> messageHistory)
{
this.CtorSet(_ => _.Message, message);
this.CtorSet(_ => _.MessageHistory, messageHistory);
}
public MessageEditState Message { get; }
public NonNullList<Saved<MessageId, MessageDetails>> MessageHistory { get; }
}
}
}
That transformation should have felt quite run-of-the-mill and predictable by this point - seen one component-tightening-up, seen them all. Let's move on to the MessageWriterStore. This is what deals with the events from the application, both from user-initiated events from DOM elements and from new-messages-data-available events from the MessageApi.
At the bare minimum, it will need some changes since it was written when the MessageEditState type was mutable and so the "ValidateMessage" method was able to mutate it (such as setting or clearing validation warning messages) in-place. I've moved away from that so that mutations are always explicit - there will no longer be a method that may or may not mutate a reference, if a method needs to set values on something then it will take the initial reference as an input and return a new one as its return value. But there are more of those sneaky "implicit assumptions" tucked away in the store that we should address. In the last post, we left it implemented like this:
using System;
using System.Collections.Generic;
using Bridge.React;
using BridgeReactTutorial.Actions;
using BridgeReactTutorial.API;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Stores
{
public class MessageWriterStore
{
private RequestId _saveActionRequestId, _lastDataUpdatedRequestId;
public MessageWriterStore(IReadAndWriteMessages messageApi, AppDispatcher dispatcher)
{
if (messageApi == null)
throw new ArgumentNullException("messageApi");
if (dispatcher == null)
throw new ArgumentNullException("dispatcher");
Message = GetInitialMessageEditState();
MessageHistory = new Tuple<int, MessageDetails>[0];
dispatcher.Receive(a => a
.If<StoreInitialised>(
condition: action => (action.Store == this),
work: action => { }
)
.Else<MessageEditStateChanged>(action =>
{
Message = action.NewState;
ValidateMessage(Message);
})
.Else<MessageSaveRequested>(action =>
{
_saveActionRequestId = messageApi.SaveMessage(action.Message);
Message.IsSaveInProgress = true;
})
.Else<MessageSaveSucceeded>(
condition: action => (action.RequestId == _saveActionRequestId),
work: action =>
{
_saveActionRequestId = null;
Message = GetInitialMessageEditState();
_lastDataUpdatedRequestId = messageApi.GetMessages();
}
)
.Else<MessageHistoryUpdated>(
condition: action =>
action.RequestId.IsEqualToOrComesAfter(_lastDataUpdatedRequestId),
work: action =>
{
_lastDataUpdatedRequestId = action.RequestId;
MessageHistory = action.Messages;
}
)
.IfAnyMatched(OnChange);
);
}
public event Action Change;
public MessageEditState Message;
public IEnumerable<Tuple<int, MessageDetails>> MessageHistory;
private MessageEditState GetInitialMessageEditState()
{
// Note: By using the ValidateMessage here, we don't need to duplicate the "Untitled"
// string that should be used for the Caption value when the UI is first rendered
// or when the user has entered some Title content but then deleted it again.
// Similarly, we avoid having to repeat the validation messages that should be
// displayed when the form is empty, since they will be set by ValidateMessage.
var blankMessage = new MessageEditState
{
Caption = "",
Title = new TextEditState { Text = "" },
Content = new TextEditState { Text = "" },
IsSaveInProgress = false
};
ValidateMessage(blankMessage);
return blankMessage;
}
private void ValidateMessage(MessageEditState message)
{
if (message == null)
throw new ArgumentNullException("message");
message.Caption = string.IsNullOrWhiteSpace(message.Title.Text)
? "Untitled"
: message.Title.Text.Trim();
message.Title.ValidationError = string.IsNullOrWhiteSpace(message.Title.Text)
? "Must enter a title"
: null;
message.Content.ValidationError = string.IsNullOrWhiteSpace(message.Content.Text)
? "Must enter message content"
: null;
}
private void OnChange()
{
if (Change != null)
Change();
}
}
}
Three things jump out at me here. Firstly, the "_saveActionRequestId" and "_lastDataUpdatedRequestId" references may not always be populated. If there is no save action in progress that we're waiting to complete, for example, then "_saveActionRequestId" won't have a value. Let's explicitly describe this in the type system by changing the type of these two values from RequestId to Optional<RequestId> (even though these values aren't part of a public API of the store class, there's still a benefit to indicating what may and may not have a value, for the sake of code clarity).
The second thing is that the "Message" and "MessageHistory" properties are only intended to be written to internally. They are available for reading by other classes (like the AppContainer component), but not for updating by other classes. It makes sense to change these from being public fields to being properties with public getters and private setters. This wasn't done originally because I wanted to start from the simplest possible implementations and only stray from that when there was a clear benefit. Today, we're dealing with the clear benefit of increased code clarity through the reduction of implicit assumptions. Moving to private-setter properties allows the compiler to enforce what was only presumed to be true before (instead of working on the assumption that no-one would try to update these references, now we can sleep safe that no-one other than the MessageWriteStore itself can change the references).
The third thing is that "Change" is an event and so may be null if no-one has subscribed to it. That's just the way that events work in C#. We could either come up with a new way to represent events or we could accept that a null check is required (and that we can't use an Optional type to represent it). I think that the pragmatic thing to do is to just accept it - this is basically how events have worked in C# from day one and I don't think that there would be any improvement to code clarity by trying to shy away from this accepted practice.
What is really going to be the most interesting part in updating the MessageWriterStore is, I think, how we change the validation / MessageEditState-mutating code -
private void ValidateMessage(MessageEditState message)
{
if (message == null)
throw new ArgumentNullException("message");
message.Caption = string.IsNullOrWhiteSpace(message.Title.Text)
? "Untitled"
: message.Title.Text.Trim();
message.Title.ValidationError = string.IsNullOrWhiteSpace(message.Title.Text)
? "Must enter a title"
: null;
message.Content.ValidationError = string.IsNullOrWhiteSpace(message.Content.Text)
? "Must enter message content"
: null;
}
Probably the absolute simplest thing that we could do would be to rewrite it like this:
private MessageEditState ValidateMessage(MessageEditState message)
{
if (message == null)
throw new ArgumentNullException("message");
if (string.IsNullOrWhiteSpace(message.Title.Text))
message = message.With(_ => _.Caption, new NonBlankTrimmedString("Untitled"));
else
message = message.With(_ => _.Caption, new NonBlankTrimmedString(message.Title.Text));
if (string.IsNullOrWhiteSpace(message.Title.Text))
message = message.With(_ => _.Title, SetValidationMessage(message.Title, new NonBlankTrimmedString("Must enter a title")));
else
message = message.With(_ => _.Title, SetValidationMessage(message.Title, null));
if (string.IsNullOrWhiteSpace(message.Content.Text))
message = message.With(_ => _.Content, SetValidationMessage(message.Content, new NonBlankTrimmedString("Must enter message content")));
else
message = message.With(_ => _.Content, SetValidationMessage(message.Content, null));
return message;
}
private TextEditState SetValidationMessage(
TextEditState textEditState,
Optional<NonBlankTrimmedString> message)
{
if (textEditState == null)
throw new ArgumentNullException("textEditState");
return textEditState.With(_ => _.ValidationError, message);
}
That is.. more verbose, I think would be a polite way to describe it. I would normally have wrapped some of the lines in order to fit the horizontal scrolling budget I allow on code samples on my blog but I wanted to give this arrangement the best change at looking succint that it could. And it's still not looking very good.
What's much worse, though, is that I don't think that this code is very easy to read. I think that there's quite a lot of noise that masks the actual intent. It's not complicated, by a long shot, but I think that the actual logic that it's trying to apply is drowning a little bit in all the code that's required. The verbosity itself, is not the biggest problem for me - I will take code that is slightly longer if it's clearer (I'm not just talking about descriptive variable and method names and I'm don't mean avoiding compact "clever" code, I mean like the changes from mutable classes to IAmImmutable implementations; they are more verbose but they are much more expressive).
One alternative would be:
private MessageEditState ValidateMessage(MessageEditState message)
{
if (message == null)
throw new ArgumentNullException("message");
var caption = string.IsNullOrWhiteSpace(message.Title.Text)
? new NonBlankTrimmedString("Untitled")
: new NonBlankTrimmedString(message.Title.Text);
var titleEditState = string.IsNullOrWhiteSpace(message.Title.Text)
? SetValidationMessage(message.Title, new NonBlankTrimmedString("Must enter a title"))
: null;
var contentEditState = string.IsNullOrWhiteSpace(message.Content.Text)
? SetValidationMessage(message.Content, new NonBlankTrimmedString("Must enter message content"))
: null;
return message
.With(_ => _.Caption, caption)
.With(_ => _.Title, titleEditState)
.With(_ => _.Content, contentEditState);
}
This is much improved. The code looks cleaner at a glance and, crucially, it's much clearer in its intent.
However.. the way that we've reduced the syntactic noise is by separating the "what should the new values be" from the "set these new values". This isn't too bad with only three properties, but if the object being validated was more complex then the new-value-determining code would drift further from the new-value-setting code, which would be a pity since they are intrinsically linked concepts (and it would be nice - meaning that the code should be easier to understand at a glance - if the two types of code were linked again for each property, with each new-value-determiner being present alongside the new-value-setter).
Instead of splitting the code up for clarity, we can try to make it clearer by using abstractions.
Let's start by introducing a method to abstract the setting-or-removing of validation messages from TextEditState instances -
private TextEditState Validate(
TextEditState textEditState,
Predicate<TextEditState> validIf,
NonBlankTrimmedString messageIfInvalid)
{
if (textEditState == null)
throw new ArgumentNullException("textEditState");
if (validIf == null)
throw new ArgumentNullException("validIf");
if (messageIfInvalid == null)
throw new ArgumentNullException("messageIfInvalid");
return textEditState.With(_ => _.ValidationError, validIf(textEditState)
? null
: messageIfInvalid);
}
This will take a TextEditState, a rule that determines whether or not its "Text" value should be considered valid and a message to set if the value is not valid (if it is valid then the message will be cleared).
This would allow us to set (or remove) the validation message on the "Title" property with code such as:
message = message.With(
_ => _.Title,
Validate(
message.Title,
textEditState => string.IsNullOrWhiteSpace(textEditState.Text),
new NonBlankTrimmedString("Must enter a title")
)
);
Since the validation logic for both "Title" and "Content" is the same and "textEditState => string.IsNullOrWhiteSpace(textEditState.Text)" is quite long and going to be responsible for a lot of the "syntactic noise" that I want to avoid, this could also be abstracted by defining another method -
private bool MustHaveValue(TextEditState textEditState)
{
if (textEditState == null)
throw new ArgumentNullException("textEditState");
return !string.IsNullOrWhiteSpace(textEditState.Text);
}
If we also move the constant messages (the two validation warnings and the "Untitled" caption string) into static class members -
private readonly static NonBlankTrimmedString _defaultCaption
= new NonBlankTrimmedString("Untitled");
private readonly static NonBlankTrimmedString _noTitleWarning
= new NonBlankTrimmedString("Must enter a title");
private readonly static NonBlankTrimmedString _noContentWarning
= new NonBlankTrimmedString("Must enter message content");
.. then we can make the "Title" validation-message-setting/unsetting much clearer:
message = message
.With(_ => _.Title, Validate(message.Title, MustHaveValue, _noTitleWarning));
If we add a final helper method to make the setting of the "Caption" property simpler -
private NonBlankTrimmedString ToNonBlankTrimmedString(
TextEditState textEditState,
NonBlankTrimmedString fallback)
{
if (textEditState == null)
throw new ArgumentNullException("textEditState");
if (fallback == null)
throw new ArgumentNullException("fallback");
return (textEditState.Text.Trim() == "")
? fallback
: new NonBlankTrimmedString(textEditState.Text);
}
.. then the "ValidateMessage" can be reduced to the following:
private MessageEditState ValidateMessage(MessageEditState message)
{
if (message == null)
throw new ArgumentNullException("message");
return message
.With(_ => _.Caption, ToNonBlankTrimmedString(message.Title, fallback: _defaultCaption))
.With(_ => _.Title, Validate(message.Title, MustHaveValue, _noTitleWarning))
.With(_ => _.Content, Validate(message.Content, MustHaveValue, _noContentWarning));
}
Now I really think that we have the best of every world. The actual code in "ValidateMessage" is short and to the point. While there are more lines of code in total (when you also consider the "ToNonBlankTrimmedString", "Validate" and "MustHaveValue" methods), each method is more focused and very easy to fully comprehend at a glance. This is the crux of the matter for me - these changes are all about (say it with, because I'm sure that you know what's coming by this point): making code easier to reason about and hence easier to read, maintain and extend.
In the past, I've had a tendency to interchange the words "method" and "function". For the last year or so (and definitely in this series of posts, I hope!) I've been more careful not to use "function" when I mean "method". Having read around, it seems like the accepted difference between the two is (to quote an excellent example from a StackOverflow answer) -
A function is a piece of code that is called by name. It can be passed data to operate on (ie. the parameters) and can optionally return data (the return value).
All data that is passed to a function is explicitly passed.
A method is a piece of code that is called by name that is associated with an object. In most respects it is identical to a function except for two key differences.
It is implicitly passed the object on which it was called.
It is able to operate on data that is contained within the class (remembering that an object is an instance of a class - the class is the definition, the object is an instance of that data).
That means that C# only has methods since every method is associated with an object. Even static methods are, technically, since they have access to anything else that is static within the type that declares the static method.
A function will only consider data passed explicitly in arguments, which is not a concept that is possible to represent with C#.
The key difference, then, being that a function is absolutely guaranteed to always retun the same value given the same argument(s). Parallels are often drawn to mathematical functions. If you think about the need to "calculate the square root of x", this is a good example of a function as it will always return the same result for any value of "x". "x" is the only thing that matters.
With C#, you can get no such guarantees. Just to really drive the point home, here are three example - first, an instance method:
public class Adder
{
private readonly int _amountToIncrement;
public Adder(int amountToIncrement)
{
_amountToIncrement = amountToIncrement;
}
public int AddTo(int value)
{
return value + _amountToIncrement;
}
}
Obviously, the return value from "AddTo" depends on more than the argument passed in - it also depends upon the "_amountToIncrement" that the Adder instance has.
And a couple of static examples:
public static class Adder
{
private static int _amountToIncrement = 0;
public static int AddTo(int value)
{
_amountToIncrement++;
return value + _amountToIncrement;
}
}
public static class DayNameRetriever
{
public static string GetDayNameForDateThisMonth(int date)
{
var today = DateTime.Now;
var firstDayOfMonth = today.AddDays(-today.Day).AddDays(date);
return firstDayOfMonth.ToString("dddd");
}
}
Granted, theses examples are clearly contrived to illustrate a point and are not genuinely useful code. But they are also not totally unlike code that exists in the real world. The point is that, because C# only has methods, the mental burden in fully comprehending any method is increased because you have to be aware of anything else that the method might have access to.
Which is a pity, because the "ToNonBlankTrimmedString", "Validate" and "MustHaveValue" methods are perfect examples of genuine functions - they only operate on their arguments. The "ValidateMessage" only strays outside of its arguments to access the "default caption", "missing-title validation message" and "missing-content validation message" values, but since there are effectively constants (since they are static readonly instances of immutable types) then "ValidateMessage" could also be considered to be a true function (in particular, we know that it will always return the same data given the same arguments).
Note: Interestingly, there is a [Pure] attribute in the .net framework, which is intended to indicate that a method is a "pure function" (where the phrase "pure function" is effectively consistent with the description of a "function" that I gave above). This seems like a nice idea, but it's not actually enforced by the compiler and so it's more of a suggestion, which greatly reduces my enthusiasm. The reason that I want to use immutable types to represent data that should not change (like React components' props types) is that it encodes the "this data is immutable" information into the type system and results in any code that tries to break this requirement (by trying to set a value on an immutable type, for example) as being identified as an error by the compiler. The [Pure] attribute will, alas, not result in any compiler warnings or errors.
The closest that we can get to indicating that a method should be considered a "function" is by making it static. As I showed above, this does not mean that the method is truly "pure" (at least, there is no provision for the compiler to confirm that this is so) but making a function static does, at least, mean that it can not access any instance fields, properties or methods and so there is still less to consider when reading one of these methods. If you know that a method is static, then there is less mental burden in reading it since you know that there is less code that you need to consider that may possibly affect the current method.
What I'm trying to get at is that the "ValidateMessage", "ToNonBlankTrimmedString", "Validate" and "MustHaveValue" methods should all be made static and, to go further, it's worth writing all methods as static unless you have a compelling reason not to. For a lot of methods, it's obvious that they have to be instance methods - the "Render" methods on the React component classes have to be instance methods, obviously, because they depend upon the "props" data for that component instance. But, in the final MessageWriterStore implementation (see below), if we pull the "OnChange" method into a lambda then there is no need for any of the methods to not be static.
It seems, in general, that people write methods as instance methods by default and then make them static if they encounter a good reason to do so. I suggest that methods be written as static by default and only made into instance methods if there is a good reason to do so.
(To be honest, this is still something that I'm trying to consistently apply to my own work - it's very easy to unconsciously write instance methods by default by omitting the "static" keyword; old habits die hard!)
using System;
using System.Collections.Generic;
using Bridge.React;
using BridgeReactTutorial.Actions;
using BridgeReactTutorial.API;
using BridgeReactTutorial.ViewModels;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.Stores
{
public class MessageWriterStore
{
private Optional<RequestId> _saveActionRequestId, _lastDataUpdatedRequestId;
public MessageWriterStore(IReadAndWriteMessages messageApi, AppDispatcher dispatcher)
{
if (messageApi == null)
throw new ArgumentNullException("messageApi");
if (dispatcher == null)
throw new ArgumentNullException("dispatcher");
Message = GetInitialMessageEditState();
MessageHistory = NonNullList<Saved<MessageId, MessageDetails>>.Empty;
dispatcher.Receive(a => a
.If<StoreInitialised>(
condition: action => (action.Store == this),
work: action => { }
)
.Else<UserEditRequested<MessageEditState>>(action =>
Message = ValidateMessage(action.NewState)
)
.Else<SaveRequested<MessageDetails>>(action =>
{
_saveActionRequestId = messageApi.SaveMessage(action.Data);
Message = Message.With(_ => _.IsSaveInProgress, true);
})
.Else<SaveSucceeded>(
condition: action => (action.RequestId == _saveActionRequestId),
work: action =>
{
_saveActionRequestId = null;
Message = GetInitialMessageEditState();
_lastDataUpdatedRequestId = messageApi.GetMessages();
}
)
.Else<DataUpdated<NonNullList<Saved<MessageId, MessageDetails>>>>(
condition:
action => action.RequestId.IsEqualToOrComesAfter(_lastDataUpdatedRequestId),
work: action =>
{
_lastDataUpdatedRequestId = action.RequestId;
MessageHistory = action.Data;
}
)
.IfAnyMatched(() => { if (Change != null) Change(); })
);
}
public event Action Change;
public MessageEditState Message { get; private set; }
public NonNullList<Saved<MessageId, MessageDetails>> MessageHistory { get; private set; }
private readonly static NonBlankTrimmedString _defaultCaption
= new NonBlankTrimmedString("Untitled");
private readonly static NonBlankTrimmedString _noTitleWarning
= new NonBlankTrimmedString("Must enter a title");
private readonly static NonBlankTrimmedString _noContentWarning
= new NonBlankTrimmedString("Must enter message content");
private static MessageEditState GetInitialMessageEditState()
{
return new MessageEditState(
caption: _defaultCaption,
title: new TextEditState("", _noTitleWarning),
content: new TextEditState("", _noContentWarning),
isSaveInProgress: false
);
}
private static MessageEditState ValidateMessage(MessageEditState message)
{
if (message == null)
throw new ArgumentNullException("message");
return message
.With(_ => _.Caption, ToNonBlankTrimmedString(message.Title, _defaultCaption))
.With(_ => _.Title, Validate(message.Title, MustHaveValue, _noTitleWarning))
.With(_ => _.Content, Validate(message.Content, MustHaveValue, _noContentWarning));
}
private static NonBlankTrimmedString ToNonBlankTrimmedString(
TextEditState textEditState,
NonBlankTrimmedString fallback)
{
if (textEditState == null)
throw new ArgumentNullException("textEditState");
if (fallback == null)
throw new ArgumentNullException("fallback");
return (textEditState.Text.Trim() == "")
? fallback
: new NonBlankTrimmedString(textEditState.Text);
}
private static TextEditState Validate(
TextEditState textEditState,
Predicate<TextEditState> validIf,
NonBlankTrimmedString messageIfInvalid)
{
if (textEditState == null)
throw new ArgumentNullException("textEditState");
if (validIf == null)
throw new ArgumentNullException("validIf");
if (messageIfInvalid == null)
throw new ArgumentNullException("messageIfInvalid");
return textEditState.With(_ => _.ValidationError, validIf(textEditState)
? null
: messageIfInvalid);
}
private static bool MustHaveValue(TextEditState textEditState)
{
if (textEditState == null)
throw new ArgumentNullException("textEditState");
return !string.IsNullOrWhiteSpace(textEditState.Text);
}
}
}
Note that, since the RequestId values are now Optional<RequestId> instances, we need to change the "IsEqualToOrComesAfter" extension method -
using System;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.API
{
public static class RequestIdExtensions
{
public static bool IsEqualToOrComesAfter(
this RequestId source,
Optional<RequestId> other)
{
if (source == null)
throw new ArgumentNullException("source");
// If the "other" reference is no-RequestId then the "source" may be considered to
// come after it
if (!other.IsDefined)
return true;
return (source == other.Value) || source.ComesAfter(other.Value);
}
}
}
There's been a lot of theory covered so far. To really put it into practice, though, we need to fix the rest of the compile errors in the example application.
Changing the MessageEditor, MessageHistory, AppContainer and MessageWriteStore to use the new immutable types (the now-immutable MessageDetails and the NonNullList type from ProductiveRage.Immutable) require further changes to the MessageApi and the App file that initialises the application.
And, while we're making everything immutable, let's change the action classes. Currently, we have actions such as:
using Bridge.React;
using BridgeReactTutorial.API;
namespace BridgeReactTutorial.Actions
{
public class DataUpdated<T> : IDispatcherAction
{
public RequestId RequestId;
public T Data;
}
public static class DataUpdated
{
public static DataUpdated<T> For<T>(RequestId requestId, T data)
{
return new DataUpdated<T> { RequestId = requestId, Data = data };
}
}
}
This should be:
using Bridge.React;
using BridgeReactTutorial.API;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.Actions
{
public class DataUpdated<T> : IDispatcherAction, IAmImmutable
{
public DataUpdated(RequestId requestId, T data)
{
this.CtorSet(_ => _.RequestId, requestId);
this.CtorSet(_ => _.Data, data);
}
public RequestId RequestId { get; }
public T Data { get; }
}
public static class DataUpdated
{
public static DataUpdated<T> For<T>(RequestId requestId, T data)
{
return new DataUpdated<T>(requestId, data);
}
}
}
The others require similar changes -
using Bridge.React;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.Actions
{
public class SaveRequested<T> : IDispatcherAction, IAmImmutable
{
public SaveRequested(T data)
{
this.CtorSet(_ => _.Data, data);
}
public T Data { get; }
}
public static class SaveRequested
{
public static SaveRequested<T> For<T>(T data)
{
return new SaveRequested<T>(data);
}
}
}
using Bridge.React;
using BridgeReactTutorial.API;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.Actions
{
public class SaveSucceeded : IDispatcherAction, IAmImmutable
{
public SaveSucceeded(RequestId requestId)
{
this.CtorSet(_ => _.RequestId, requestId);
}
public RequestId RequestId { get; }
}
}
using Bridge.React;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.Actions
{
public class StoreInitialised : IDispatcherAction, IAmImmutable
{
public StoreInitialised(object store)
{
this.CtorSet(_ => _.Store, store);
}
public object Store { get; }
}
}
using Bridge.React;
using BridgeReactTutorial.ViewModels;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.Actions
{
public class UserEditRequested<T> : IDispatcherAction, IAmImmutable
{
public UserEditRequested(T newState)
{
this.CtorSet(_ => _.NewState, newState);
}
public T NewState { get; }
}
public static class UserEditRequested
{
public static UserEditRequested<T> For<T>(T newState)
{
return new UserEditRequested<T>(newState);
}
}
}
The App class requires only minor tweaks, from:
using System.Linq;
using Bridge.Html5;
using Bridge.React;
using BridgeReactTutorial.Actions;
using BridgeReactTutorial.API;
using BridgeReactTutorial.Components;
using BridgeReactTutorial.Stores;
namespace BridgeReactTutorial
{
public class App
{
[Ready]
public static void Go()
{
var container = Document.GetElementById("main");
container.ClassName = string.Join(
" ",
container.ClassName.Split().Where(c => c != "loading")
);
var dispatcher = new AppDispatcher();
var messageApi = new MessageApi(dispatcher);
var store = new MessageWriterStore(messageApi, dispatcher);
React.Render(
new AppContainer(new AppContainer.Props
{
Dispatcher = dispatcher,
Store = store
}),
container
);
dispatcher.Dispatch(new StoreInitialised { Store = store });
}
}
}
to:
using System.Linq;
using Bridge.Html5;
using Bridge.React;
using BridgeReactTutorial.Actions;
using BridgeReactTutorial.API;
using BridgeReactTutorial.Components;
using BridgeReactTutorial.Stores;
namespace BridgeReactTutorial
{
public class App
{
[Ready]
public static void Go()
{
var container = Document.GetElementById("main");
container.ClassName = string.Join(
" ",
container.ClassName.Split().Where(c => c != "loading")
);
var dispatcher = new AppDispatcher();
var messageApi = new MessageApi(dispatcher);
var store = new MessageWriterStore(messageApi, dispatcher);
React.Render(
new AppContainer(dispatcher, store),
container
);
dispatcher.Dispatch(new StoreInitialised(store));
}
}
}
Finally, the MessageApi needs various alterations to deal with the fact that all data types (such as the MessageDetails, the message history and the action classes) are immutable -
using System;
using Bridge;
using Bridge.Html5;
using Bridge.React;
using BridgeReactTutorial.Actions;
using BridgeReactTutorial.ViewModels;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.API
{
public class MessageApi : IReadAndWriteMessages
{
private readonly AppDispatcher _dispatcher;
private NonNullList<Saved<MessageId, MessageDetails>> _messages;
public MessageApi(AppDispatcher dispatcher)
{
if (dispatcher == null)
throw new ArgumentException("dispatcher");
_dispatcher = dispatcher;
_messages = NonNullList<Saved<MessageId, MessageDetails>>.Empty;
// To further mimic a server-based API (where other people may be recording messages
// of their own), after a 10s delay a periodic task will be executed to retrieve a
// new message
Window.SetTimeout(
() => Window.SetInterval(GetChuckNorrisFact, 5000),
10000
);
}
public RequestId SaveMessage(MessageDetails message)
{
return SaveMessage(message, optionalSaveCompletedCallback: null);
}
private RequestId SaveMessage(
MessageDetails message,
Action optionalSaveCompletedCallback)
{
if (message == null)
throw new ArgumentNullException("message");
var requestId = new RequestId();
Window.SetTimeout(
() =>
{
_messages = _messages.Add(Saved.For(
(MessageId)(int)_messages.Count,
message
));
_dispatcher.Dispatch(new SaveSucceeded(requestId));
if (optionalSaveCompletedCallback != null)
optionalSaveCompletedCallback();
},
1000 // Simulate a roundtrip to the server
);
return requestId;
}
public RequestId GetMessages()
{
var requestId = new RequestId();
Window.SetTimeout(
() => _dispatcher.Dispatch(DataUpdated.For(requestId, _messages)),
1000 // Simulate a roundtrip to the server
);
return requestId;
}
private void GetChuckNorrisFact()
{
var request = new XMLHttpRequest();
request.ResponseType = XMLHttpRequestResponseType.Json;
request.OnReadyStateChange = () =>
{
if (request.ReadyState != AjaxReadyState.Done)
return;
if ((request.Status == 200) || (request.Status == 304))
{
try
{
var apiResponse = (ChuckNorrisFactApiResponse)request.Response;
if ((apiResponse.Type == "success")
&& (apiResponse.Value != null)
&& !string.IsNullOrWhiteSpace(apiResponse.Value.Joke))
{
// The Chuck Norris Facts API (http://www.icndb.com/api/) returns strings
// html-encoded, so they need decoding before be wrapped up in a
// MessageDetails instance
// - Note: After the save has been processed, GetMessages is called so
// that a MessageHistoryUpdate action is dispatched
SaveMessage(
new MessageDetails(
title: new NonBlankTrimmedString("Fact"),
content: new NonBlankTrimmedString(HtmlDecode(apiResponse.Value.Joke))
),
() => GetMessages()
);
return;
}
}
catch
{
// Ignore any error and drop through to the fallback message-generator below
}
}
SaveMessage(new MessageDetails(
title: new NonBlankTrimmedString("Fact"),
content: new NonBlankTrimmedString("API call failed when polling for content :(")
));
};
request.Open("GET", "http://api.icndb.com/jokes/random");
request.Send();
}
private string HtmlDecode(string value)
{
if (value == null)
throw new ArgumentNullException("value");
var wrapper = Document.CreateElement("div");
wrapper.InnerHTML = value;
return wrapper.TextContent;
}
[IgnoreCast]
private class ChuckNorrisFactApiResponse
{
public extern string Type { [Template("type")] get; }
public extern FactDetails Value { [Template("value")] get; }
[IgnoreCast]
public class FactDetails
{
public extern int Id { [Template("id")] get; }
public extern string Joke { [Template("joke")]get; }
}
}
}
}
One pleasant change was the removal of code that was previously in the MessageApi with the following comment:
// ToArray is used to return a clone of the message set - otherwise, the caller would
// end up with a list that is updated when the internal reference within this class
// is updated (which sounds convenient but it's not the behaviour that would be
// exhibited if this was really persisting messages to a server somewhere)
Since the message list is described by an immutable structure, there is no way that a particular reference's data could change. There is no way that a component could have a reference to this data type and then find that the data in that reference had changed by the time that an event bubbled up to that component from one of its child components. Similarly, when the MessageApi passes its message history data out, there is no action that may be performed by any code that receives the message history reference that could "pollute" the data that the MessageApi stores internally.
Previously, when the MessageApi wanted to share its message history data, we had two options - we could hope that the mutable list of mutable MessageDetails would never be manipulated when it was passed out from the MessageApi or we could try to make it impossible for other code to pollute the MessageApi's copy of the data, which is what the "ToArray" call went some way towards (note that this wouldn't have saved us from code that received the mutable message history and then changed one of the fields on any of the individual mutable MessageDetails instances - this would have polluted to MessageApi's internal data).
This is a nice example of how immutable structures can actually aid performance as well as aiding code clarity. Before, we were using a defensive "ToArray" call to try to avoid any silly mistakes polluting the MessageApi's internal data. This was only a partial solution anyway, since, to really protect ourselves, we would have needed to clone the entire list - cloning each individual MessageDetails instance, as well as the list itself. Now that the data is immutable, such cloning (which can be very expensive in some case) is not necessary. I maintain, though, that the biggest benefit is to code clarity rather than performance - it is now impossible to make the "silly mistake" of mutating shared data, because the previously-implicit behaviour guideline of "do not try to mutate this data, please" is now encapsulated in the type system.
It's not uncommon to hear people claim that using immutable types incur a performance cost. I believe that this is only really true at a highly localised level. For example, if you have an array and you want to change the element at index 5, that is an incredibly cheap operation and it is not possible to have an "immutable array" that has a method that will give you a new immutable array instance with a different value at index 5 as cheaply. At this level, mutable structures can perform operations more quickly. However, immutable structures can allow techniques that provide performance benefits at a higher level, such as described above, where expensive cloning operations may be avoided entirely (general-case cloning operations can be expensive in CPU and in memory, since a clone will duplicate the entire data structure, whereas mature immutable-type libraries leverage clever "persistent structures" to reuse data between instances).
(It has become less common to hear this argument against immutable data structures since they are being much more widely used these days - React is an excellent example in that it leverages immutability to allow for fantastic performance, rather than immutability being a cost that the React library has to pay).
While performance is not my number one goal (which is not to say that I don't think it's an important target, I'm just saying that I value code clarity more highly), this topic of conversation leads me nicely on to the next topic.
If a "pure function" is one that returns a value based solely upon its arguments, then a "pure component" is a parallel concept - it generates content based solely upon its "props" data.
To recall what our example application looks like -
There are basically two parts to it; the Message Editor and the Message History. The Message Editor part changes after one of any of the following occurs -
The Message History part changes only when the MessageApi sends a message to say that there is new message data to display.
What happens in our application is that every change results in a full React re-render. React is very efficient and its Virtual DOM minimises (and can batch) changes to the slow browser DOM, so this is rarely something to worry about. However, it might lead you to think -
If I know that a particular event will only result in changes to the Message Editor, isn't it wasteful to React to re-render the entire Message History content in its Virtual DOM - particularly if it does this only to discover that no changes to the browser DOM need to be applied?
This is an entirely reasonable question. If there are a hundred messages in the Message History, does that component really have to re-render them all in the Virtual DOM every time that the user presses a button to change the value in the "Title" text input in the Message Editor? What if there 1000 messages in the history? Or what if this was a much more complicated application with many, many editable inputs and lists of results all over the page - do I really want the entirety of this complicated UI to be re-rendered by the Virtual DOM every time that the user edits a single field?
If we were using mutable structures to represent the data that the React component hierarchy has to render, we would have a few options. One would be to change the component structure such that there were more stateful components in order to try to only update branches of the hierarchy that need to change according to particular changes. In our example, the MessageEditor could be made stateful in a bid to limit changes to the text inputs from resulting in re-renders of the MessageHistory (which would also have to become a stateful component, so that it could update itself when the message history data changes). This would require the MessageWriteStore to be changed as well. On the surface of it, this doesn't necessarily sound like a terrible idea, but stateful components will always require more thought and planning than stateless components and so this would be a move back towards more complicated component models. This would be an unfortunate step back from where we are now, where the complications are minimised and most components are stateless. We would no longer have a render-down and pass-events-up model, we would have a sort of branched version of it.
Another option would be to try to have components make use of React's "shouldComponentUpdate" method. This is a method that may optionally be implemented on components, that is called before a component's "Render" method is called, so long as that component has been rendered at least once before. It will be given two "props" values - one is the props data from the last render and the second is the new props data that has been specified for the re-render. If this method returns true then the component is re-rendered (to the Virtual DOM) as normal. If it returns false then the component's "Render" method is not called. This would mean that none of its child components would be re-rendered either, since those re-renders are only triggered by code in their parent's component's "Render" method. If it was possible for the MessageHistory to look at its last props and its next props and see that they describe the same data, then the entire re-render work could be avoided. The problem comes in working that out, though - for the cases where the old and new messages data was the same and when we had an IEnumerable set of mutable MessageDetails instances, we would have had to have enumerated through every value in the set and compared the "Title" and "Content" values on each message. If they all matched then the old and new data would have been proven to have been the same and the re-render would not be required. But was all that comparison work really much cheaper than just letting the Virtual DOM do its magic?
One of the good thing about immutable structures is that data can safely be shared and reused. Now that the MessageHistory component takes an immutable NonNullList of immutable MessageDetails instances, if the data hasn't changed then the same NonNullList<Saved<MessageId, MessageDetails>> reference will be passed to the MessageHistory.Props instance - but if the data has changed then, by necessity, a new NonNullList<Saved<MessageId, MessageDetails>> reference will be provided. This would make a "shouldComponentUpdate" implementation very simple - just look at each property on the old and new props references and use reference equality comparisons to see if anything's changed.
The bad news is that the StatelessComponent base class in the Bridge / React bindings doesn't support "shouldComponentUpdate". So you can't try to take advantage of it to reduce the work that the Virtual DOM does - only the Component (which is the full stateful component) base class supports it. The good news is that you don't need to implement it manually. If you're creating stateless components whose props classes have properties that are all primitive values (such as bool, int, string, etc..) and / or immutable types and / or functions* and if the components genuinely render entirely according to the props data (no accessing DateTime.Now, for example) then you can derive from the PureComponent base class instead. This automatically implements "shouldComponentUpdate" behind the scenes.
* (There are some cases where "props" properties that are callbacks - like "OnChange" on the TextInput - can't be compared by the PureComponent's magic, but most of the time they can be and the details of when they can and can't are outside the scope of this article - so let's just assume that function / Action<T> / callback property types CAN always be handled).
To illustrate, add the following line to the start of the MessageHistory "Render" method -
Console.WriteLine("MessageHistory.Render");
Now, run the application in the browser and bring up the browser console in the dev tools. Every time that the app re-renders in the Virtual DOM, "MessageHistory.Render" will be written to the console. Every time that a key is pressed to change one of the text input elements, the entire UI will be re-rendered and "MessageHistory.Render" will be displayed in the console.
Now, change the base class of the MessageHistory from StatelessComponent<MessageHistory.Props> to PureComponent<MessageHistory.Props>. It will look like this:
using System;
using System.Linq;
using Bridge.React;
using BridgeReactTutorial.API;
using BridgeReactTutorial.ViewModels;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.Components
{
public class MessageHistory : StatelessComponent<MessageHistory.Props>
{
public MessageHistory(
NonNullList<Saved<MessageId, MessageDetails>> messages,
Optional<NonBlankTrimmedString> className = new Optional<NonBlankTrimmedString>())
: base(new Props(className, messages)) { }
public override ReactElement Render()
{
Console.WriteLine("MessageHistory.Render");
var className = props.ClassName;
if (!props.Messages.Any())
className = className.Add(" ", new NonBlankTrimmedString("zero-messages"));
// Any time a set of child components is dynamically-created (meaning that the
// numbers of items may vary from one render to another), each must have a unique
// "Key" property set (this may be a int or a string). Here, this is simple as
// each message tuple is a unique ID and the contents of that message.
var messageElements = props.Messages
.Select(savedMessage => DOM.Div(new Attributes { Key = (int)savedMessage.Key },
DOM.Span(new Attributes { ClassName = "title" }, savedMessage.Value.Title),
DOM.Span(new Attributes { ClassName = "content" }, savedMessage.Value.Content)
));
// When child components are specified (as they are through the second argument of
// DOM.Div), the argument is of type Union<ReactElement, string>[] (meaning that each
// element may be another component or it may be a simple text value)
// - The React bindings have an extension method that transforms an IEnumerable set
// of components (such as "messageElements") into an Union<ReactElement, string>[]
return DOM.FieldSet(
new FieldSetAttributes { ClassName = className.ToStringIfDefined() },
DOM.Legend(null, "Message History"),
DOM.Div(null, messageElements)
);
}
public class Props : IAmImmutable
{
public Props(
Optional<NonBlankTrimmedString> className,
NonNullList<Saved<MessageId, MessageDetails>> messages)
{
this.CtorSet(_ => _.ClassName, className);
this.CtorSet(_ => _.Messages, messages);
}
public Optional<NonBlankTrimmedString> ClassName { get; }
public NonNullList<Saved<MessageId, MessageDetails>> Messages { get; }
}
}
}
Re-build the application and refresh it in the browser. Now change the text input values while keeping an eye on the console. The "MessageHistory.Render" message will initially only appear in the console when the MessageHistory component is first rendered, the changes to the text inputs no longer require the MessageHistory component be redrawn by the Virtual DOM. When the message data actually changes (whether due to you saving a new message or due to a new Chuck Norris fact arriving), the MessageHistory component will update - indicated by another "MessageHistory.Render" message being displayed in the console.
This is an excellent example of how using immutable structures can result in cleaner and more efficient code. In a complicated UI (or if your application is very performance-sensitive - if you're trying to achieve 60fps on mobile, for example) then being able to save the Virtual DOM the work of determining whether entire branches of the component hierarchy should be re-rendered is invaluable. And getting this optimisation "for free" makes it even better - if you're following my recommendations and the vast majority of your React components are stateless and al of your data types are immutable then you might as well use the PureComponent base class and reap the performance benefits.
I want to go into some of the finer point of the PureComponent's optimisation rules. There's nothing particularly complicated or surprising, but there are some nuances that it's worth being aware of.
When the PureComponent implements "shouldComponentUpdate" behind the scenes, the React library provides it with two separate instances to compare of the Props class for the current component. The first thing that the PureComponent does is ensure that the two props instances are of the same type (while it would be strange, it would not be illegal to create types derived from the component's Props class - but if different derived types were provided for the old and new props then it doesn't seem safe to try to compare them, who knows why they are different or what significance there could be in the differences). If the old and new props references are of the precise same type, then it enumerates the properties on the type and compares the values on the old and new props instances. Each pair of property values must match - this means that they are either both null or they are both the same value of a primitive type or they are the same reference or they are both non-null and the first value has an "Equals" method that returns true when the second value is passed into it.
On the whole, this means that things largely work completely intuitively. Particularly due to the way that the "With" extension methods works for IAmImmutable-implementing class. If "With" is called with a property value update where the new value is the same as the old value, then "With" returns the original instance unaltered. This is most easily explained with an example:
var title = new TextEditState(text: "hello", validationError: null);
var title2 = title.With(_ => _.Text, "hello");
var title3 = title.With(_ => _.Text, "hell");
The "title2" instance will be the same as the "title" reference - the text value was asked to changed from "hello" to "hello", which is no change at all. Since the TextEditState type is immutable, there is no pointing copying "title" to create a new "title2" reference with all of the same data. "title3" will be a new instance, since the text value needs to change from "hello" to "hell".
Having "With" return the same reference when no change is required is beneficial for garbage collection - the fewer references that are created means the less work that it has to do. But it's also very important for the PureComponent since that prefers to use referential equality to determine whether a property value has changed. If a Props class has a TextEditState property on it, when the values on the old and new props are compared then we want the TextEditState references to be the same if the data that they represent hasn't changed.
I think that another example is called for. In the example application from this series, the Message Editor form is given information in its props that describes the current state of the form - the "Caption", the "Title" text-input-value-and-any-validation-message, the "Content" text-input-value-and-any-validation-message (and information about whether the form should be disabled because a save is in progress). This data is all contained within a MessageEditState instance. The MessageEditor component renders each text input by generating child ValidatedTextInput components. These child ValidatedTextInput components raise an "OnChange" event when the user wants to alter the content in the text input element, the event includes a string argument for the new text value. When this happens, the MessageEditor takes this new text value and uses it to create a new MessageEditState instance, using the "With" extension method - this new instance is then passed up on the MessageEditor's "OnChange" event. This event will result in the UI being re-rendered to display the new data.
However, React raises change events from text inputs for user interactions even if they don't actually result in a change. If, for instance, there is a text input with the value "Hello" in it and you highlight that text and copy it to the clipboard and then paste it into that same text input then the value obviously hasn't changed, but React still raises a change event from the text input due to the paste action. What we want to happen in this case is for the "new" MessageEditState instance that the MessageEditor creates when it raises its "OnChange" event to be the exact same reference as the MessageEditState given to the MessageEditor when it last rendered. This way, when the MessageEditor is asked to re-render then it will find that the "new" props data is exactly the same as the old props data and the PureComponent logic will tell React that it needn't bother re-rendering the component at all.
Maybe it's worth examining the MessageEditor code again to try to make this seem less abstract:
using System;
using Bridge.React;
using BridgeReactTutorial.API;
using BridgeReactTutorial.ViewModels;
using ProductiveRage.Immutable;
namespace BridgeReactTutorial.Components
{
public class MessageEditor : StatelessComponent<MessageEditor.Props>
{
public MessageEditor(
MessageEditState message,
Action<MessageEditState> onChange,
Action onSave,
Optional<NonBlankTrimmedString> className = new Optional<NonBlankTrimmedString>())
: base(new Props(className, message, onChange, onSave)) { }
public override ReactElement Render()
{
var formIsInvalid =
props.Message.Title.ValidationError.IsDefined ||
props.Message.Content.ValidationError.IsDefined;
return DOM.FieldSet(
new FieldSetAttributes { ClassName = props.ClassName.ToStringIfDefined() },
DOM.Legend(null, props.Message.Caption),
DOM.Span(new Attributes { ClassName = "label" }, "Title"),
new ValidatedTextInput(
className: new NonBlankTrimmedString("title"),
disabled: props.Message.IsSaveInProgress,
content: props.Message.Title.Text,
onChange: newTitle => props.OnChange(
props.Message.With(_ => _.Title, new TextEditState(newTitle))
),
validationMessage: props.Message.Title.ValidationError
),
DOM.Span(new Attributes { ClassName = "label" }, "Content"),
new ValidatedTextInput(
className: new NonBlankTrimmedString("content"),
disabled: props.Message.IsSaveInProgress,
content: props.Message.Content.Text,
onChange: newContent => props.OnChange(
props.Message.With(_ => _.Content, new TextEditState(newContent))
),
validationMessage: props.Message.Content.ValidationError
),
DOM.Button(
new ButtonAttributes
{
Disabled = formIsInvalid || props.Message.IsSaveInProgress,
OnClick = e => props.OnSave()
},
"Save"
)
);
}
public class Props : IAmImmutable
{
public Props(
Optional<NonBlankTrimmedString> className,
MessageEditState message,
Action<MessageEditState> onChange,
Action onSave)
{
this.CtorSet(_ => _.ClassName, className);
this.CtorSet(_ => _.Message, message);
this.CtorSet(_ => _.OnChange, onChange);
this.CtorSet(_ => _.OnSave, onSave);
}
public Optional<NonBlankTrimmedString> ClassName { get; }
public MessageEditState Message { get; }
public Action<MessageEditState> OnChange { get; }
public Action OnSave { get; }
}
}
}
If the user attempted this copy-paste-same-value thing in the "Title" input, then the "onChange" event from the ValidatedTextInput that renders the Title value would be raised -
onChange: newTitle => props.OnChange(
props.Message.With(_ => _.Title, new TextEditState(newTitle))
)
This will result in the MessageEditor's "OnChange" event being raised, with a new MessageEditState instance. The key thing is that we want the new MessageEditState instance to be the same as the current MessageEditState instance if the new "Title" string is exactly the same as the current "Title" string.
One way to do this would be to not worry about how "With" does or doesn't work and to add a condition into the lambda - eg.
onChange: newTitle =>
{
if (newTitle != props.Message.Title.text)
props.OnChange(props.Message.With(_ => _.Title, new TextEditState(newTitle)));
}
However, this means that more logic has to go in the components (which I want to avoid). Worse, it's boring and repetitive logic, which is the kind that I find is most likely to be done incorrectly by accident because you almost feel like you can write it on auto-pilot. It would be best if this could be handled automatically.
Well, it can be if we always use "With" to update values. In the code above, I've actually been a bit naughty. A TextEditState includes two values - "Text" and "ValidationMessage". The "ValidationMessage" will get set according to the "Text" value when validation is applied (which happens in the MessageWriterStore in this application). The code above creates a new TextEditState with the "newTitle" string, erasing any "ValidationMessage" value. This is not really correct, since only the validation logic should change the "ValidationMessage" value. In this example, a validation message should only be displayed if the text value is empty - but the components should have no knowledge of this since we want them to be as dumb as possible. So, any time that a component creates a new TextEditState to include in an "OnChange" event, the "ValidationMessage" property should be untouched - again, it is the responsibility of the store (and not the component) to worry about ensuring that the "ValidationMessage" value is correct for the "Text" value before a re-render is triggered.
So, this code:
onChange: newTitle => props.OnChange(
props.Message.With(_ => _.Title, new TextEditState(newTitle))
)
should really be this:
onChange: newTitle => props.OnChange(
props.Message.With(_ => _.Title, props.Message.Title.With(_ => _.Text, newContent))
)
This only changes the "Text" property on the "Title" TextEditState - which means that, in our copy-paste-same-value case, no new TextEditState instance will be created. This still means that the MessageEditor's "OnChange" event will be raised (which will result in an action being sent through the Dispatcher and received by the MessageWriterStore, which will raise a "Change" event and cause the AppContainer to re-render the UI), but when the MessageEditor is asked to re-render then it will realise that the new data is the same as its current data and it will tell React not to bother re-rendering it. The code still had to do one full pass of the raise-event-up-to-top-level-component-and-send-change-message-through-Dispatcher-to-the-Store, even though the data hadn't changed, but that sort of work is very cheap (certainly much cheaper than any DOM or even Virtual DOM interactions).
All of the above came "for free" by using IAmImmutable-implementing classes and PureComponent and by applying updates using the "With" extension method. There are some cases where you need to do a little work to help the system out, though. If you recall the "ValidateMessage" function that we wrote earlier -
private static MessageEditState ValidateMessage(MessageEditState message)
{
if (message == null)
throw new ArgumentNullException("message");
return message
.With(_ => _.Caption, ToNonBlankTrimmedString(message.Title, fallback: _defaultCaption))
.With(_ => _.Title, Validate(message.Title, MustHaveValue, _noTitleWarning))
.With(_ => _.Content, Validate(message.Content, MustHaveValue, _noContentWarning));
}
This always set the "Caption" based upon the "Title" value. If there is a "Title" value in the text input of the message editor form of "My New Message" then the "Caption" value will also be "My New Message". The "ToNonBlankTrimmedString" method is implemented as:
private static NonBlankTrimmedString ToNonBlankTrimmedString(
TextEditState textEditState,
NonBlankTrimmedString fallback)
{
if (textEditState == null)
throw new ArgumentNullException("textEditState");
if (fallback == null)
throw new ArgumentNullException("fallback");
return (textEditState.Text.Trim() == "")
? fallback
: new NonBlankTrimmedString(textEditState.Text);
}
Every time that "ValidateMessage" is called and there is a non-blank "Title" value then a new NonBlankTrimmedString instance will be created for the "Caption". The problem is that if the "Title" input isn't changing (if the user is currently changing the "Content" text input box, for example) then we will be creating new NonBlankTrimmedString instances for the same "Title" value - and the PureComponent will see each new NonBlankTrimmedString reference as a new and distinct value.
We could try to prevent this by changing "ValidateMessage" such that it tries to avoid creating new NonBlankTrimmedString instances for the Caption -
private static MessageEditState ValidateMessage(MessageEditState message)
{
if (message == null)
throw new ArgumentNullException("message");
if (message.Title.Text.Trim() == "")
message = message.With(_ => _.Caption, _defaultCaption);
else if (message.Title.Text.Trim() != message.Caption)
message = message.With(_ => _.Caption, new NonBlankTrimmedString(message.Title.Text));
return message
.With(_ => _.Title, Validate(message.Title, MustHaveValue, _noTitleWarning))
.With(_ => _.Content, Validate(message.Content, MustHaveValue, _noContentWarning));
}
.. but this brings us back round to "ValidateMessage" being very noisy. This will have a tendency to make it prone to error and it definitely moves away from the goal of code clarity that I'm aiming for.
An alternative is to implement an "Equals" override for the NonBlankTrimmedString class. The "With" extension method, like the PureComponent's "shouldComponentUpdate" logic", will consider an "Equals" method if referential equality fails. This means that if we call
.With(_ => _.Caption, ToNonBlankTrimmedString(message.Title, fallback: _defaultCaption))
.. and if the return value from "ToNonBlankTrimmedString" is a NonBlankTrimmedString instance that is equivalent to the current "Caption" value (according to the NonBlankTrimmedString "Equals" implementation below), then "With" will return the same reference.
using System;
using Bridge;
using Bridge.React;
namespace BridgeReactTutorial.API
{
public sealed class NonBlankTrimmedString
{
public NonBlankTrimmedString(string value)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Null, blank or whitespace-only value specified");
Value = value.Trim();
}
/// <summary>
/// This will never be null, blank or have any leading or trailing whitespace
/// </summary>
public string Value { get; }
/// <summary>
/// It's convenient to be able to pass a NonBlankTrimmedString instance as any argument
/// that requires a string
/// </summary>
public static implicit operator string(NonBlankTrimmedString value)
{
if (value == null)
throw new ArgumentNullException("value");
return value.Value;
}
/// <summary>
/// It's convenient to be able to pass a NonBlankTrimmedString instance as any argument
/// that requires a ReactElement-or-string, such as for the children array of the React
/// DOM component factories
/// </summary>
public static implicit operator Union<ReactElement, string>(NonBlankTrimmedString value)
{
if (value == null)
throw new ArgumentNullException("value");
return value.Value;
}
public override bool Equals(object o)
{
var otherNonBlankTrimmedString = o as NonBlankTrimmedString;
return
(otherNonBlankTrimmedString != null) &&
(otherNonBlankTrimmedString.Value == Value);
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
}
}
Having an "Equals" implementation on types such as NonBlankTrimmedString makes a lot of sense because we essentially want them to be treated as "value types" - if two references describe the same data then they should be treated as the same value. Note that a custom "Equals" implementation was not necessary for MessageId, since that is a struct - in C#, structs automatically get an "Equals" implementation created for them that considers two struct instances to be equivalent if all of their properties have the same values and the JavaScript generated by Bridge does the same in order to maintain compatibility.
One thing that I snuck into the new NonBlankTrimmedString above is that the class is now sealed. If it is possible to inherit from a given class, it becomes much more difficult to implement a reliable "Equals" method. For the sake of argument, imagine a class "X" that is not sealed and that has a single string "Value" property and that implements its own "Equals" method, exactly the same as the NonBlankTrimmedString "Equals" method above. Then imagine that a class "Y" is derived from "X" and adds a second property, a "Count" int. If "Y" does not override the "Equals" implementation on "X" then it seems likely that the "Equals" implementation that "Y" inherits from "X" will be inaccurate - if an instance of "Y" with a "Value" of "Hello" and "Count" of 1 was compared to another instance of "Y" with a "Value" of "Hello" but a "Count" of 2 then they would be considered to be equivalent, which would almost certainly not be the expected behaviour. There is an implicit assumption that if "X" is derived from and the derived type adds properties then that derived type should have its own "Equals" method. A failure to respect this implicit assumption will not result in any compiler warnings, it will likely result only in unexpected runtime behaviour at some point. I don't like implicit assumptions, as I'm sure that you've been able to tell from the themes in this post. An easy way to avoid this problem is to prevent "X" from being inherited from, by making it sealed. This is precisely what I've done with the NonBlankTrimmedString.
This leads me on to another guideline - I believe that 99% of all classes should be abstract or sealed. Any class that may be inherited from requires planning, to try to make it as easy as possible for any derived types to work in non-suprising manners. This is complicated, if not impossible (as the "Equals" conundrum above hopefully illustrates for an extremely simple case). However, if you really think that it should be possible for a class to be inherited from, then I think that you should document any implicit assumptions on that base class and you should carefully think about how it should and shouldn't be used. I have found that the most common cases for which a base class are suitable are those where some shared functionality is required that can't easily be provided via composition (which I will talk about in a moment), but where that base class is not fully-functional itself and so must be inherited from in order to be useful. The Component, StatelessComponent and PureComponent base classes in the Bridge / React bindings are excellent examples; they are required in order to define components that the React library can work with, but the classes are not functional on their own - they must be inherited from in order to be useful, therefore they are defined as abstract classes.
Historically, I think that there have been beliefs that many object models lend themselves well to inheritance hierarchies. In Web Forms (if I remember correctly - it's been a long time), a Button inherited from a WebControl and that inherited from a Control or a Component.. or something. The idea was that every component as you went up the hierarchy was a specialisation of the one below it. Which arguably made sense. But I find that the waters often get very murky when these "is-a" hierarchies are constructed and it's very easy to confuse a "has-a" characteristic for being an "is-a". For example, the MessageWriterStore needs to register for events from the Dispatcher, so perhaps it is a specialisation of a "DispatcherMessageReceiver". But it's also responsible for saving messages, so perhaps it's also specialisation of a "MessageRecorder". Not only is it more complicated to design classes that are intended to be inherited from, but C# only allows one class to inherit from a single base class - so if MessageWriterStore needs to be a specialisation of both a "DispatcherMessageReceiver" and a "MessageRecorder" then we have a problem; possibly only solved by creating a specialisation of the "DispatcherMessageReceiver" that also deals with recording messages, so that the MessageWriterStore can inherit from that.
Perhaps that example is a little fatuous, but similar sorts of issues do genuinely occur when inheritance trees are viewed as "the best way" to build classes. The alternative is to use composition, which is where classes are designed in a "has-a" manner. The current MessageWriterStore is designed in this way as its constructor declares that it requires an AppDispatcher and a IReadAndWriteMessages implementation. Not only does composition avoid the multiple inheritance problem (also known as the "deadly diamond of death") but it makes code easier to fully comprehend. With inheritance, each class is a sum of its behaviour and all of the behaviour of its base class (which is likewise a sum of its behaviour and all of the behaviour of its base class, repeated for as many levels as the inheritance tree is deep). With composition, each piece is fully self-contained and communication between one part and another is throughly tightly constrained interfaces.
(A couple of months ago, I read "5 reasons your team will love you for composition", which I think offers a good take on some of the practical benefits positives of preferring composition to inheritance - it's well worth a quick read).
In summary, I strongly believe that almost all relations between classes may be described better by using composition than inheritance (where "better" means that the code is easier to reason about and, also, easier to test) and so almost no classes should need to be inherited from. In the few cases where inheritance is necessary, the base classes must be carefully designed for inheritance and the use cases that necessitate inheritance will almost always be ones where the base classes are non-functional in isolation, where they require a derived type in order to work.
Therefore, 99% of classes should be written to be abstract or to be sealed. This goes for all classes that I've shown code for in this series - the MessageEditor component class should be sealed, its Props class should be sealed, the MessageWriterStore should be sealed, the MessageDetails should be sealed, the Saved class should be sealed and the NonBlankTrimmedString should be sealed. They should all be sealed.
Now, if we wanted to play devil's advocate then we could say that any "ClassName" property on a component class shouldn't be Optional<NonBlankTrimmedString> since I've said that everything should be really specific, since richer types have a lot of value in expressing intent. We could say that there should be a more specific ClassName class, which only allows characters that are valid in an html class name (React deals with encoding string "ClassName" properties, so this isn't really a big concern - but I'm trying to prove another point, so bear with me). We could also say that this ClassName class really is a specialisation of NonBlankTrimmedString and so surely we should allow NonBlankTrimmedString to be inherited from so that ClassName could be derived from it. On the surface, this doesn't seem too unreasonable. However, the only actual benefit of having ClassName inherit from NonBlankTrimmedString (other than it just feeling like something that may possibly be a good idea) would be if there was a point at which you had a method that took an argument of type NonBlankTrimmedString that you wanted to give a ClassName instance - because it seems like any valid ClassName will also be a valid NonBlankTrimmedString. I think that the benefit is just too small to outweight the cost. Inheritance brings with it too much potential complication (and associated mental burden) that any small benefit like this is not enough to bring inheritance into play. On top of which, if you really did want to be able to use a ClassName instance anywhere that a NonBlankTrimmedString is required, you could do this by implementing an implicit operator method on ClassName that allows it be implicitly cast to a NonBlankTrimmedString - then you would have the marginal benefit of being able to use a ClassName anywhere that you need a NonBlankTrimmedString but still be able to keep NonBlankTrimmedString sealed and less prone to error.
Writing classes that are sealed by default is another practice that I am still trying to instill into myself. Much like writing methods to be static by default, I think that it makes a huge amount of sense but it's also a concept that I've only cemented in the last year or so and I'm still trying to consistently apply it!
This has turned out to be a pretty huge post. But I didn't want to try to split it up because all of the principles are related, really.
If you'd stopped reading after Part Two then I think that you would have the tools to construct client-side applications that will scale from something simple up to something massively complex. The combination of C# (which is time-proven as a language that not only enables large applications to be built and maintained over long periods of time, it is a language where it is not exceptional for this to be the case*) and React is very powerful.
* (Languages such as PHP and JavaScript CAN be used to build and maintain large code bases, but I think that it's telling that the companies that take this work seriously rely on, and develop, additional tooling to make it manageable, like Hack and Flow).
However, the principles behind React that I found most appealing when I first encountered it - that it primarily exists to make complicated interactions in an application simpler and scalable - are what I want to apply to my C#. In the concepts that I'm advocating here for writing C# React applications and in React itself, performance is not the number one priority; it is not something for which code clarity must be sacrificed in order to attain. Performance is, however, still important - but it is achieved through high-level design decisions and not through micro-optimisations (I still think that the best example of this is how performing particular operations on an immutable structure will almost always be slower than on the corresponding mutable structure, but the algorithms that may be written based upon the safe sharing of immutable types mean that actual application performance will be greater - whether this is in the avoidance of deep-cloning references for safety or in the way that the PureComponent can avoid re-rendering of entire branches of the UI by the Virtual DOM).
To summarise, I believe that if the guidelines presented here are followed then you will be able to go beyond the C# React applications that you would have written with Part-Two Knowledge and be able to write code that is easier to understand (whether by other Developers or by yourself, when you come back to a piece of code after six months), easier to maintain and extend and that is more efficient.
To recap, the guidelines are as follows:
I'm not sure that there's anything earth-shatteringly original about any one of these suggestions. Combining them can, in my ever-so-humble opinion, result in significantly higher quality code than not following them. While these posts have been about writing React applications using Bridge.NET, these rules may be applied just as well to any C# code. The ProductiveRage.Immutable NuGet package only works with Bridge projects, though - but if anyone asked them I would happily consider recreating it for vanilla C#! :)
There are some other things to consider when writing complete Bridge / React applications. I keep saying that it's important for code to be testable, but I haven't offered any way to write the actual unit tests. I've also not had to offer any URL-routing capabilities, since the example application is so simple. But routing is something that is almost certainly going to be required in any real browser-based application.
These are both technologies that I would like to cover in future posts, but probably as shorter follow-up pieces. In terms of how to write React applications in C#, I am happy with what I've covered in these three posts - I wanted to get the example code to where it is now but without jumping straight to the final architecture; I wanted to illustrate why one-way data binding is so beneficial and then why a Flux-like structure is so helpful and then why immutable types are such a good idea. Hopefully, by starting from the basics and introducing new concepts and approaches only when there was a way to illustrate the improvements that they yield, you all agree with me that this is the way to do things!
Of course, if you disagree in general, or on any particular points, or you have any tweaks to what I've suggested then I would be very interested to hear. That's what the comments section is for!
Posted at 21:32
16 March 2016
To summarise where we got to in Part One: we've got an entry form where, after you enter Title and Content values, you may save a message. During the message-saving process, the form and save button are disabled. Once the saving has completed, the form will be cleared and it will be re-enabled. Validation logic in the MessageEditor prevents the form from being saved while one or both of the required inputs are without value. After the save has succeeded, a read action will begin in the background - once the read operation has completed, a MessageHistory component will display all of the saved messages. All interactions with the MessageApi are handled by the top-level AppContainer component. Similarly, all user interaction events are passed up to this top-level component, from where re-renders of the UI state are triggered.
I see this arrangement as a top-to-bottom tree in terms of rendering - the top-level component is in control of what to display in the component hierarchy beneath it, and all of the information required to display those child components is contained within the top-level component's state data.
In terms of event-handling, events may occur deep down within the tree and are then passed back up to the top-level component. As an event passes up from the html element that it originates in, up to the top level, it will gather more and more information - for example, when a user requests a change to the "Title" text input (such as by pressing a key while the input has focus), then an event is raised by the TextInput component saying that a string value should be changed. This TextInput is a child component of the MessageEditor, which acknowledges this string-changed event and raises it own event; a MessageDetails-changed event. The "Content" value of this new message will be unchanged, but the "Title" value will have a new value - the new value that the TextInput should take. This event is received by the AppContainer and it uses it to update its internal representation of the application, changing its "state" reference by calling "SetState" and so triggering a re-render.
The path may be traced downward when considering rendering and may be traced upward when considering events.
This one-way passing of data is very powerful in terms of modelling interactions that are easy to follow. As a reminder, a common way to deal with interactions like this in days gone by (ie. before React popularised "one-way bindings") was for changes to elements to be reflected immediately in-place and for any interested parties to subscribe to events that those elements raise after they change. So, for the message entry form -
With the old method, the "Title" and the "Content" inputs would accept changes that the user makes immediately - and the fieldset legend and the save button components would need to listen out to some of these changes. The legend changes to match what is entered in the "Title" input (unless it's blank, in which case the legend shows "Untitled"). The save button needs to be enabled if both "Title" and "Content" have values and disabled if one or both of them are without.
I mentally envisage this as star-shaped event handling - a change to one element may fan out to many others. In some cases, these changes would then cascade on to other elements, and then on again and again (hopefully not "again and again and..", but it was difficult to keep a handle on these things with this sort of approach - because it was difficult to get a simple view as to what could affect what).
With one-way data binding, events go up to the top, are processed and then the re-render distributes this new information all the way down. When the MessageEditor is rendered, it knows what the current "Title" value is and so it knows what to put in that "Title" TextInput and it knows what the fieldset legend should be and it knows whether the save button should be enabled or not.
This arrangement can take you a long way, I think. But there are a couple of things that I take issue with -
We can refine this arrangement a little by introducing an intermediary for events to pass through and by pulling out the logic that exists within components (such as the validation within the MessageEditor and the when-a-save-happens-then-.. logic in the AppContainer).
Rather than talk about how some applications could theoretically benefit from a change to the architecture, I want to do it with a demonstration.
I'm going to change the MessageApi so that, after a short delay, new messages start appearing in its internal list. The "SaveMessage" and "GetMessages" methods are unchanged, it's just that there's a background process going on as well. To keep things interesting, I'm going to source these message from the The Internet Chuck Norris Database API -
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bridge;
using Bridge.Html5;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.API
{
public class MessageApi : IReadAndWriteMessages
{
private readonly List<Tuple<int, MessageDetails>> _messages;
public MessageApi()
{
_messages = new List<Tuple<int, MessageDetails>>();
// To further mimic a server-based API (where other people may be recording messages
// of their own), after a 10s delay a periodic task will be executed to retrieve a
// new message
Window.SetTimeout(
() => Window.SetInterval(GetChuckNorrisFact, 5000),
10000
);
}
public Task SaveMessage(MessageDetails message)
{
if (message == null)
throw new ArgumentNullException("message");
if (string.IsNullOrWhiteSpace(message.Title))
throw new ArgumentException("A title value must be provided");
if (string.IsNullOrWhiteSpace(message.Content))
throw new ArgumentException("A content value must be provided");
var task = new Task<object>(null);
Window.SetTimeout(
() =>
{
_messages.Add(Tuple.Create(_messages.Count, message));
task.Complete();
},
1000 // Simulate a roundtrip to the server
);
return task;
}
public Task<IEnumerable<Tuple<int, MessageDetails>>> GetMessages()
{
// ToArray is used to return a clone of the message set - otherwise, the caller would
// end up with a list that is updated when the internal reference within this class
// is updated (which sounds convenient but it's not the behaviour that would be
// exhibited if this was really persisting messages to a server somewhere)
var task = new Task<IEnumerable<Tuple<int, MessageDetails>>>(null);
Window.SetTimeout(
() => task.Complete(_messages.ToArray()),
1000 // Simulate a roundtrip to the server
);
return task;
}
private void GetChuckNorrisFact()
{
var request = new XMLHttpRequest();
request.ResponseType = XMLHttpRequestResponseType.Json;
request.OnReadyStateChange = () =>
{
if (request.ReadyState != AjaxReadyState.Done)
return;
if ((request.Status == 200) || (request.Status == 304))
{
try
{
var apiResponse = (ChuckNorrisFactApiResponse)request.Response;
if ((apiResponse.Type == "success")
&& (apiResponse.Value != null)
&& !string.IsNullOrWhiteSpace(apiResponse.Value.Joke))
{
// The Chuck Norris Facts API (http://www.icndb.com/api/) returns strings
// html-encoded, so they need decoding before be wrapped up in a
// MessageDetails instance
SaveMessage(new MessageDetails
{
Title = "Fact",
Content = HtmlDecode(apiResponse.Value.Joke)
});
return;
}
}
catch
{
// Ignore any error and drop through to the fallback message-generator below
}
}
SaveMessage(new MessageDetails
{
Title = "Fact",
Content = "API call failed when polling for server content :("
});
};
request.Open("GET", "http://api.icndb.com/jokes/random");
request.Send();
}
private string HtmlDecode(string value)
{
if (value == null)
throw new ArgumentNullException("value");
var wrapper = Document.CreateElement("div");
wrapper.InnerHTML = value;
return wrapper.TextContent;
}
[IgnoreCast]
private class ChuckNorrisFactApiResponse
{
public extern string Type { [Template("type")] get; }
public extern FactDetails Value { [Template("value")] get; }
[IgnoreCast]
public class FactDetails
{
public extern int Id { [Template("id")] get; }
public extern string Joke { [Template("joke")]get; }
}
}
}
}
The Chuck-Norris-fact-retrieval code could be made shortener by casting the "apiResponse" reference to dynamic, but I thought that it would a nice opportunity to show how you can call a JSON-returning API with Bridge and then access the data through a known object model (which is why I created a ChuckNorrisFactApiResponse class to use instead).
Without any other changes to the code we have so far, this works.. in a manner of speaking. Any time you save a message of your own, the read action that follows the save will pull back all of the messages from the MessageApi's in-memory set. So, when a read is explicitly initiated, all of the most recent data will come back.
But it would be nice if the new messages could appear in the MessageHistory even without you, as the user, saving your own messages. They could appear in the history even as you are in the process of writing your own content.
One way to do this would be to have the MessageApi raise an event whenever its message history data changes. Then, the AppContainer would listen for events from its MessageApi reference (which it has in its props, since props on a stateful component are used to provide references to the "external environment") as well as listening to events from the components that it renders.
On the one hand, this would actually make the save logic cleaner - the OnSave handler that the AppContainer passes to the MessageEditor in its props would only have to disable the form during the save and clear / re-enable it after the save, it wouldn't have to then request updated message data from the MessageApi, since it would know that the MessageApi would raise its own event when it had accepted the newly-saved message.
But, on the other hand, dealing with more event sources means that more logic is required in the AppContainer (which I want to move away from) and we no longer have the simple rendering-goes-down-the-tree and events-come-up-the-tree, now we have rendering-goes-down-the-tree and events-come-up-the-tree and events-come-from-some-other-places-too.
So I'm going to look at an alternative..
Instead of the AppContainer potentially having to deal with multiple event sources and working out how events may or may not change its state, I'm going to pull that handling of UI state into another class entirely; somewhere to store this state, that will act as a single (and very simple) event source for AppContainer. This store will have a single event that the AppContainer will subscribe to; an argument-less "OnChange" event. When the AppContainer receives an "OnChange" event from the store, it will access data that the store makes available in order to update its own state reference and thus trigger a re-render.
Since events will be handled by this store, whenever an event from the component tree is passed up to the AppContainer, the AppContainer needs to pass it along to the store (instead of trying to process the event itself). So the AppContainer could act like an event source for this new store class. And, since the store class will be dealing with processing events (such as "user has requested a new message be saved"), it will also have to deal with the MessageApi being an event source.
This is a good illustration of separation of concerns - the AppContainer used to be responsible for rendering the component tree and dealing with handling events (which is where the real complexity of an application lies). With this new plan, the AppContainer will only deal with re-rendering and the event processing is dealt with by "pure" (ie. non-UI-related) C# code. However, we could still make the "multiple event source" issue a little cleaner. The store in this example will only have two event sources (the AppContainer - from user interactions - and the MessageApi - from new Chuck Norris facts arriving), but a more complex application could result in many event sources being required.
And, sometimes, a particular store might not even want access to the full event source reference; if our store was only going to be used for dealing with data for a form that edits an existing message (and doesn't want to show a message history anywhere), then it wouldn't need a full MessageApi reference - it just needs to be able to read one message in particular to edit, be able to request a change to that message be saved and then know when that save request had been processed. For this store to say that it requires a full MessageApi would be an exaggeration - it would be claiming that it had a greater dependency than it really does.
It makes sense to have one "UI store" per page of your application, so if we were to extend this example such that the main page allowed you to record new messages and see the message history and we added a way to edit a particular message on a different screen, then we might have two stores and two top-level components and a router to handle navigation from one to the other. I don't want to jump too far ahead here, I just mean to point out that different stores may rely upon different event sources and rely on different abilities of those event sources.
So the next change that I'm going to propose is to decouple event sources from the store by having all events broadcast as messages. These are sent to some sort of message bus, which then passes them on to any interested parties. This would mean that the AppContainer would send a message to the bus whenever the user has interacted with the UI (whether this be an edit to a field or a request to save). When "SaveMessage" is called on the MessageApi then it will no longer return a Task, instead a message will be passed to the bus when the save has completed. When a new Chuck Norris fact is received by the MessageApi, it will send a message to the bus to say that new message data is available.
This would mean that the store class will only have a single "event source", which is this message bus. It will subscribe to this bus, and whenever a message is dispatched that the store is interested in then it will deal with it accordingly. If a store were to receive a message that it wasn't interested in, then it would just ignore it.
Introducing a message bus like this is a common technique to decouple areas of a system and it is another approach that makes unit testing easier later on - since the store class(es) are where the complicated event-handling logic lives, this is the code that needs the most testing. With this proposed arrangement, to test a store's logic you need only to new one up, pass it a message bus reference used solely within the current unit test, push particular messages through the bus and then verify that the store raises its OnChange event after messages when you expect it to and that the data that the store makes public is what you expect at these times. This allows all of the complicated logic to be tested with zero UI components involved and reduces the times when mocks are required since so much of the communication with the store is done via messages. (In order to test the "Save" functionality in our example app, a mock IReadAndWriteMessages would be required by our MessageWriterStore, however, so that it could call "SaveMessage" on something that doesn't actually persist data - and so that the unit test could confirm that "SaveMessage" was called with the expected data).
To summarise all of the above, we will move away slightly from the paths of communication being rendering-goes-down-the-tree and events-come-up-the-tree. Before, these paths were a continuous chain because events came up the tree and were processed and the re-render immediately went back down the tree again. Now we have rendering-goes-down-the-tree and events-come-up-the-tree but then nothing seems to happen immediately, instead the top-level component sends off a message and does nothing more.. until the store receives that message, processes it as an event (and applies any complicated logic) and then raises its OnChange event, which the top-level component receives and triggers a re-render.
I must admit, that sounds more complicated! But don't lose sight of the fact that we will side-step the complexities that multiple event sources can introduce and we will separate logic from UI, making the code more testable and making each part of it easier to reason about and thus easier to maintain and extend in the future.
(Note: This approach brings us much closer to treating everything as "asynchronous by default" - even UI changes now are implemented in a fire-and-forget manner; the top-level component sends out a message when the UI should be updated and then does nothing until it's informed by the store that it should update. While it initially feels strange to not expect to "immediately" update UI components in a synchronous manner, the advantage to async being the norm is that it's common for async code to be a bit scary in otherwise synchronous code - but here it's all the same, and not scary at all. It's also worth noting that we weren't truly updating in a synchronous manner before, since React's SetState method actually operates asynchronously - this allows React to batch updates if many occur in succession, potentially further reducing the number of times that the browser DOM actually needs to be interacted with; clever stuff!)
I must admit, at this point, that I can't take any credit for the above ideas. What I've outlined is referred to as the Flux Architecture (since different people have extracted their own variations on the concept, it might be safer to describe it as a Flux-like architecture, but let's not split hairs).
Below is the classic Flux diagram -
The message bus is referred to as the "Dispatcher", messages are known as "Actions". The "View" is the React component tree (the AppContainer listens for the "OnChange" event from the Store and re-renders when it receives it). Note that actions come not just from the View but also from outside the cycle; in our case we have messages coming from the MessageApi when new Chuck Norris facts arrive. In an application with routing, there would be actions from the router when the URL changes.
So... what's required to get from the current architecture that the example application has to a Flux-like one?
Let's start with the Dispatcher and the messages that flow through it. The first good news is that the Bridge React bindings include a Dispatcher implementation; the AppDispatcher. This has two methods:
void Receive(Action<IDispatcherAction> callback);
void Dispatch(IDispatcherAction action);
The IDispatcherAction interface is empty and is only used as a marker to identify classes as being intended for use as a Dispatcher action.
When writing in JavaScript, actions tend to be simple objects with a "type" or "actionType" property that identifies what sort of action it is. It will then have further properties, depending upon what action it needs to describe. I've taken the below example from an article (that wasn't written by me); A (little more) complex react and flux example -
AppDispatcher.dispatch({
actionType: BookConstants.MESSAGE_ADD,
message: {
color: 'green',
text: msg
}
});
However, we're writing in C# and we're going to take advantage of that! Our actions will be distinct types. When the store listens out messages, it won't compare an "actionType" string to work out what a particular action represents, instead we'll perform type comparisons and, when we find an action that we're interested in, we'll cast the current action to the matched type and then access its data in a type-safe manner.
Create a new folder in the project from Part One called "Actions". The simplest action to begin with is the action that would be raised by the AppContainer when the user has clicked the Save button -
using Bridge.React;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Actions
{
public class MessageSaveRequested : IDispatcherAction
{
public MessageDetails Message;
}
}
Previously, the AppContainer's Render method contained logic about dealing with save requests -
OnSave = async () =>
{
// Set SaveInProgress to true while the save operation is requested
SetState(new State {
Message = state.Message,
IsSaveInProgress = true,
MessageHistory = state.MessageHistory
});
await props.MessageApi.SaveMessage(state.Message);
// After the save has completed, clear the message entry form and reset
// SaveInProgress to false
SetState(new State {
Message = new MessageDetails { Title = "", Content = "" },
IsSaveInProgress = false,
MessageHistory = state.MessageHistory
});
// Then re-load the message history state and re-render when that data arrives
var allMessages = await props.MessageApi.GetMessages();
SetState(new State {
Message = state.Message,
IsSaveInProgress = state.IsSaveInProgress,
MessageHistory = allMessages
});
}
But, if all that it needs to do when a save-request bubbles up to it is send an appropriate action through the Dispatcher, then it becomes much simpler -
OnSave = () => props.Dispatcher.Dispatch(
new MessageSaveRequested { Message = state.Message }
)
I've only shown the change to the "OnSave" property, rather than show the change within the context of the complete AppContainer class because there are other things I want to rearrange at this point. Firstly, the MessageEditor "OnChange" handler also needs to be altered - as I described above, when a user interaction is expected to require a re-render, this will not result in SetState being called immediately. Instead, an action will be sent to the Dispatcher. We need to define this action, so create another class in the "Actions" folder -
using Bridge.React;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Actions
{
public class MessageEditStateChanged : IDispatcherAction
{
public MessageEditState NewState;
}
}
The other change is in the format of the data that is passed to the MessageEditor. Before, separate "Title", "Content" and "Disabled" values were passed to it and the component would do three things with that information -
These second and third things are precisely the sort of logic that should be extracted out into the store. Consequently, the MessageEditor will be changed so that it no longer takes these individual values and, instead, takes a MessageEditState that has a "Caption" string (for the legend text), "Title" and "Content" strings and validation messages for these user-entered strings. The "Disabled" property will replaced with "IsSaveInProgress" - if this is true then none of the form elements (the text inputs or the save button) should be enabled. If a save is not in progress, then the text inputs should be enabled but the save button should only be enabled if neither validation message has any content. That is arguably more logic that could be extracted, but I think that this approach will strike a good balance - keeping the component "dumb" without having to spell out every little thing to the nth degree. Add two new files to the "ViewModels" folder to define the following classes -
namespace BridgeReactTutorial.ViewModels
{
public class MessageEditState
{
public string Caption;
public TextEditState Title;
public TextEditState Content;
public bool IsSaveInProgress;
}
}
namespace BridgeReactTutorial.ViewModels
{
public class TextEditState
{
public string Text;
public string ValidationError;
}
}
Now the MessageEditor may be rewritten to the following (note that after this change, the project isn't going to compile any more - there are a bunch of other alterations that will be required until everything builds again, all of which will be covered below):
using System;
using Bridge.React;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Components
{
public class MessageEditor : StatelessComponent<MessageEditor.Props>
{
public MessageEditor(Props props) : base(props) { }
public override ReactElement Render()
{
var formIsInvalid =
!string.IsNullOrWhiteSpace(props.Message.Title.ValidationError) ||
!string.IsNullOrWhiteSpace(props.Message.Content.ValidationError);
return DOM.FieldSet(new FieldSetAttributes { ClassName = props.ClassName },
DOM.Legend(null, props.Message.Caption),
DOM.Span(new Attributes { ClassName = "label" }, "Title"),
new ValidatedTextInput(new ValidatedTextInput.Props
{
ClassName = "title",
Disabled = props.Message.IsSaveInProgress,
Content = props.Message.Title.Text,
OnChange = newTitle => props.OnChange(new MessageEditState
{
Title = new TextEditState { Text = newTitle },
Content = props.Message.Content
}),
ValidationMessage = props.Message.Title.ValidationError
}),
DOM.Span(new Attributes { ClassName = "label" }, "Content"),
new ValidatedTextInput(new ValidatedTextInput.Props
{
ClassName = "content",
Disabled = props.Message.IsSaveInProgress,
Content = props.Message.Content.Text,
OnChange = newContent => props.OnChange(new MessageEditState
{
Title = props.Message.Title,
Content = new TextEditState { Text = newContent },
}),
ValidationMessage = props.Message.Content.ValidationError
}),
DOM.Button(
new ButtonAttributes
{
Disabled = formIsInvalid || props.Message.IsSaveInProgress,
OnClick = e => props.OnSave()
},
"Save"
)
);
}
public class Props
{
public string ClassName;
public MessageEditState Message;
public Action<MessageEditState> OnChange;
public Action OnSave;
}
}
}
Now the AppContainer becomes much simpler -
using System;
using System.Collections.Generic;
using Bridge.React;
using BridgeReactTutorial.Actions;
using BridgeReactTutorial.ViewModels;
using BridgeReactTutorial.Stores;
namespace BridgeReactTutorial.Components
{
public class AppContainer : Component<AppContainer.Props, AppContainer.State>
{
public AppContainer(AppContainer.Props props) : base(props) { }
protected override void ComponentDidMount()
{
props.Store.Change += StoreChanged;
}
protected override void ComponentWillUnmount()
{
props.Store.Change -= StoreChanged;
}
private void StoreChanged()
{
SetState(new State
{
Message = props.Store.Message,
MessageHistory = props.Store.MessageHistory
});
}
public override ReactElement Render()
{
if (state == null)
return null;
return DOM.Div(null,
new MessageEditor(new MessageEditor.Props
{
ClassName = "message",
Message = state.Message,
OnChange = newState => props.Dispatcher.Dispatch(
new MessageEditStateChanged { NewState = newState }
),
OnSave = () => props.Dispatcher.Dispatch(
new MessageSaveRequested
{
Message = new MessageDetails
{
Title = state.Message.Title.Text,
Content = state.Message.Content.Text
}
}
)
}),
new MessageHistory(new MessageHistory.Props
{
ClassName = "history",
Messages = state.MessageHistory
})
);
}
public class Props
{
public AppDispatcher Dispatcher;
public MessageWriterStore Store;
}
public class State
{
public MessageEditState Message;
public IEnumerable<Tuple<int, MessageDetails>> MessageHistory;
}
}
}
It is now almost devoid of logic, it only exists to listen to changes from the store (which we'll define in a moment), to render components and to direct events that are passed up from these components to the Dispatcher. Note that it no longer has any dependency upon the MessageApi.
A couple of things to note - the Dispatcher and Store are both passed to the component through its props, but when the Store raises a Change event the AppContainer copies data references from the Store into its own state, meaning that all of the information that the AppContainer requires to render itself is contained within its state. This follows the "Guidelines for Stateful Components" that I wrote last time -
The AppContainer now uses some React component life cycle methods that it wasn't before - "ComponentDidMount" and "ComponentWillUnmount" - to ensure that the handler attached to the Store's Change event is correctly removed when no longer required. In our example application, the AppContainer is never "unmounted" but in a more complicated application then the top-level components may be changed based upon how the user navigates through the application. (A component is "mounted" when it's being added to the component tree and "unmounted" when it's being removed - in an application with a router, the current top-level component may be changed based upon the current route, in which case there would be top-level components being mounted and unmounted as the route changes and it is important that any event handlers that they attached be detached when they're not needed).
One final thing to note before moving on to the Store implementation is that there is no longer a "GetInitialState" implementation in the AppContainer and the "Render" method will return null if state doesn't yet have a value. "GetInitialState" was another example of logic that is better placed outside of the component classes - now the AppContainer is not responsible for having to know what its initial state should be, it just renders nothing until the Store has raised a Change request that tells the AppContainer what to display.
The child components are "dumb" as all they have to do is render according to the props data that they are provided with and now the top-level stateful component is similarly "dumb" as all it does is listen to the Store and pass the information down to dumb stateless components - and then listen for events from the child components, in order to pass the information on to the Dispatcher.
We're almost ready to talk about how to create the Store now, but first we need to adapt the MessageApi to work with the Dispatcher, rather than with Tasks.
using System;
using System.Collections.Generic;
using Bridge;
using Bridge.Html5;
using Bridge.React;
using BridgeReactTutorial.Actions;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.API
{
public class MessageApi : IReadAndWriteMessages
{
private readonly AppDispatcher _dispatcher;
private readonly List<Tuple<int, MessageDetails>> _messages;
public MessageApi(AppDispatcher dispatcher)
{
if (dispatcher == null)
throw new ArgumentException("dispatcher");
_dispatcher = dispatcher;
_messages = new List<Tuple<int, MessageDetails>>();
// To further mimic a server-based API (where other people may be recording messages
// of their own), after a 10s delay a periodic task will be executed to retrieve a
// new message
Window.SetTimeout(
() => Window.SetInterval(GetChuckNorrisFact, 5000),
10000
);
}
public RequestId SaveMessage(MessageDetails message)
{
return SaveMessage(message, optionalSaveCompletedCallback: null);
}
private RequestId SaveMessage(
MessageDetails message,
Action optionalSaveCompletedCallback)
{
if (message == null)
throw new ArgumentNullException("message");
if (string.IsNullOrWhiteSpace(message.Title))
throw new ArgumentException("A title value must be provided");
if (string.IsNullOrWhiteSpace(message.Content))
throw new ArgumentException("A content value must be provided");
var requestId = new RequestId();
Window.SetTimeout(
() =>
{
_messages.Add(Tuple.Create(_messages.Count, message));
_dispatcher.Dispatch(
new MessageSaveSucceeded { RequestId = requestId }
);
if (optionalSaveCompletedCallback != null)
optionalSaveCompletedCallback();
},
1000 // Simulate a roundtrip to the server
);
return requestId;
}
public RequestId GetMessages()
{
// ToArray is used to return a clone of the message set - otherwise, the caller would
// end up with a list that is updated when the internal reference within this class
// is updated (which sounds convenient but it's not the behaviour that would be
// exhibited if this was really persisting messages to a server somewhere)
var requestId = new RequestId();
Window.SetTimeout(
() => _dispatcher.Dispatch(new MessageHistoryUpdated
{
RequestId = requestId,
Messages = _messages.ToArray()
}),
1000 // Simulate a roundtrip to the server
);
return requestId;
}
private void GetChuckNorrisFact()
{
var request = new XMLHttpRequest();
request.ResponseType = XMLHttpRequestResponseType.Json;
request.OnReadyStateChange = () =>
{
if (request.ReadyState != AjaxReadyState.Done)
return;
if ((request.Status == 200) || (request.Status == 304))
{
try
{
var apiResponse = (ChuckNorrisFactApiResponse)request.Response;
if ((apiResponse.Type == "success")
&& (apiResponse.Value != null)
&& !string.IsNullOrWhiteSpace(apiResponse.Value.Joke))
{
// The Chuck Norris Facts API (http://www.icndb.com/api/) returns strings
// html-encoded, so they need decoding before be wrapped up in a
// MessageDetails instance
// - Note: After the save has been processed, GetMessages is called so
// that a MessageHistoryUpdate action is dispatched
SaveMessage(
new MessageDetails
{
Title = "Fact",
Content = HtmlDecode(apiResponse.Value.Joke)
},
() => GetMessages()
);
return;
}
}
catch
{
// Ignore any error and drop through to the fallback message-generator below
}
}
SaveMessage(new MessageDetails
{
Title = "Fact",
Content = "API call failed when polling for server content :("
});
};
request.Open("GET", "http://api.icndb.com/jokes/random");
request.Send();
}
private string HtmlDecode(string value)
{
if (value == null)
throw new ArgumentNullException("value");
var wrapper = Document.CreateElement("div");
wrapper.InnerHTML = value;
return wrapper.TextContent;
}
[IgnoreCast]
private class ChuckNorrisFactApiResponse
{
public extern string Type { [Template("type")] get; }
public extern FactDetails Value { [Template("value")] get; }
[IgnoreCast]
public class FactDetails
{
public extern int Id { [Template("id")] get; }
public extern string Joke { [Template("joke")]get; }
}
}
}
}
This means that the IReadAndWriteMessages interface no longer returns Tasks -
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.API
{
public interface IReadAndWriteMessages
{
RequestId SaveMessage(MessageDetails message);
RequestId GetMessages();
}
}
Each of the two methods now return a "RequestId". This is a unique identifier that will be used to tie future actions back to specific calls to the "GetMessages" or "SaveMessage" methods. When a user requests that a message be saved in our sample app, the AppContainer sends a MessageSaveRequested action through the Dispatcher. The store will receive this action from the Dispatcher and use the message data in it to call "SaveMessage", which will give the Store a unique RequestId. After the MessageApi has completed the save, it will raise a MessageSaveSucceeded action that has a "RequestId" value, the same RequestId as "SaveMessage" returned. This is how the Store knows that the save which succeeded was, in fact, the save that it initiated. In the app here, there wouldn't be any doubt since there is only one place where a new message may be saved, but in a more complicated application it's feasible that there may be multiple components that could initiate a save and it would be important to be able to be able to trace any "save succeeded" notification back to where it came from.
The RequestId has a nice feature in that two instances may be compared to determine which is most recent - this could be applicable to an application like our example because, shortly, the message history will be updated after a user has created a new message and it will be automatically updated when a new Chuck Norris fact appears. It's not too difficult to imagine that there could be a race condition that occurs when two "new message history" actions are received by the Store (one from the user-saves-message-and-then-fresh-history-is-automatically-retrieved-after-the-save-is-completed process and one from a new Chuck Norris fact arriving). In the real world, with unpredictable server and network times, it's possible for "Server Call A" to start before "Server Call B" but for "Server Call A" to finish after "Server Call B" - in this case we want the "new message history" from "Server Call B", since it should be more recent, but the data from "Server Call A" arrives after it and we need to know which result is freshest. If each action that relates to "new data arrived from API" has a RequestId then we can compare the two values using the "ComesAfter" function, allow us to ignore the stale data.
The RequestId implementation is fairly simple (add a new "RequestId.cs" file to the "API" folder and paste in the following) -
using System;
namespace BridgeReactTutorial.API
{
public class RequestId
{
private static DateTime _timeOfLastId = DateTime.MinValue;
private static int _offsetOfLastId = 0;
private readonly DateTime _requestTime;
private readonly int _requestOffset;
public RequestId()
{
_requestTime = DateTime.Now;
if (_timeOfLastId < _requestTime)
{
_offsetOfLastId = 0;
_timeOfLastId = _requestTime;
}
else
_offsetOfLastId++;
_requestOffset = _offsetOfLastId;
}
public bool ComesAfter(RequestId other)
{
if (other == null)
throw new ArgumentNullException("other");
if (_requestTime == other._requestTime)
return _requestOffset > other._requestOffset;
return (_requestTime > other._requestTime);
}
}
}
In the new MessageApi code above, two actions were referenced that haven't been defined yet, so add two more classes to the "Actions" folder for the following:
using System;
using System.Collections.Generic;
using Bridge.React;
using BridgeReactTutorial.API;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Actions
{
public class MessageHistoryUpdated : IDispatcherAction
{
public RequestId RequestId;
public IEnumerable<Tuple<int, MessageDetails>> Messages;
}
}
using Bridge.React;
using BridgeReactTutorial.API;
namespace BridgeReactTutorial.Actions
{
public class MessageSaveSucceeded : IDispatcherAction
{
public RequestId RequestId;
}
}
(Note: A MessageHistoryUpdated will be emitted after a "GetMessages" call is made but one will also be emitted every time that a new Chuck Norris fact arrives).
While we're adding actions, we're going to need a StoreInitialised action class (I'll talk about this more later on, for now just add the following class to the "Actions" folder):
using Bridge.React;
using BridgeReactTutorial.API;
namespace BridgeReactTutorial.Actions
{
public class StoreInitialised : IDispatcherAction
{
public object Store;
}
}
Now, finally, we so create a Store.
Create a new folder in the project root called "Stores" and add a new class file to it; "MessageWriterStore.cs" -
using System;
using System.Collections.Generic;
using Bridge.React;
using BridgeReactTutorial.Actions;
using BridgeReactTutorial.API;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Stores
{
public class MessageWriterStore
{
private RequestId _saveActionRequestId, _lastDataUpdatedRequestId;
public MessageWriterStore(IReadAndWriteMessages messageApi, AppDispatcher dispatcher)
{
if (messageApi == null)
throw new ArgumentNullException("messageApi");
if (dispatcher == null)
throw new ArgumentNullException("dispatcher");
Message = GetInitialMessageEditState();
MessageHistory = new Tuple<int, MessageDetails>[0];
dispatcher.Receive(action =>
{
if (action is StoreInitialised)
{
var storeInitialised = (StoreInitialised)action;
if (storeInitialised.Store == this)
OnChange();
}
else if (action is MessageEditStateChanged)
{
var messageEditStateChanged = (MessageEditStateChanged)action;
Message = messageEditStateChanged.NewState;
ValidateMessage(Message);
OnChange();
}
else if (action is MessageSaveRequested)
{
var messageSaveRequested = (MessageSaveRequested)action;
_saveActionRequestId = messageApi.SaveMessage(messageSaveRequested.Message);
Message.IsSaveInProgress = true;
OnChange();
}
else if (action is MessageSaveSucceeded)
{
var messageSaveSucceeded = (MessageSaveSucceeded)action;
if (messageSaveSucceeded.RequestId == _saveActionRequestId)
{
_saveActionRequestId = null;
Message = GetInitialMessageEditState();
OnChange();
_lastDataUpdatedRequestId = messageApi.GetMessages();
}
}
else if (action is MessageHistoryUpdated)
{
var messageHistoryUpdated = (MessageHistoryUpdated)action;
if ((_lastDataUpdatedRequestId == null)
|| (_lastDataUpdatedRequestId == messageHistoryUpdated.RequestId)
|| messageHistoryUpdated.RequestId.ComesAfter(_lastDataUpdatedRequestId))
{
_lastDataUpdatedRequestId = messageHistoryUpdated.RequestId;
MessageHistory = messageHistoryUpdated.Messages;
OnChange();
}
}
});
}
public event Action Change;
public MessageEditState Message;
public IEnumerable<Tuple<int, MessageDetails>> MessageHistory;
private MessageEditState GetInitialMessageEditState()
{
// Note: By using the ValidateMessage here, we don't need to duplicate the "Untitled"
// string that should be used for the Caption value when the UI is first rendered
// or when the user has entered some Title content but then deleted it again.
// Similarly, we avoid having to repeat the validation messages that should be
// displayed when the form is empty, since they will be set by ValidateMessage.
var blankMessage = new MessageEditState
{
Caption = "",
Title = new TextEditState { Text = "" },
Content = new TextEditState { Text = "" },
IsSaveInProgress = false
};
ValidateMessage(blankMessage);
return blankMessage;
}
private void ValidateMessage(MessageEditState message)
{
if (message == null)
throw new ArgumentNullException("message");
message.Caption = string.IsNullOrWhiteSpace(message.Title.Text)
? "Untitled"
: message.Title.Text.Trim();
message.Title.ValidationError = string.IsNullOrWhiteSpace(message.Title.Text)
? "Must enter a title"
: null;
message.Content.ValidationError = string.IsNullOrWhiteSpace(message.Content.Text)
? "Must enter message content"
: null;
}
private void OnChange()
{
if (Change != null)
Change();
}
}
}
There's really nothing very complicated here at all. What I like is that all of the logic that was previously ensconced within component classes is now in a C# class which has zero UI-based dependencies. What I also like is how clear the logic is, it's very easy to read through how the various actions are matched and to see precisely what state changes will occur. In fact, the MessageWriterStore is just a simple state machine where each transition is based upon the action that it receives from the Dispatcher. It's reassuring that the Flux architecture is based upon these time-tested computer science devices; the message bus and state machine - while it might take a little while to internalise how to write applications based around this "one-way binding" / "one-way message passing" arrangement, once it clicks it feels very natural.
Having the core application logic in classes like this really helps ensure that code will have those two great properties that I will keep repeating through this series - that it's easy to reason about and that it's easy to test. It's easy to reason about as it's easy to see how user (and server) actions flow through the code, there are few surprises. It's easy to test because there are few dependencies - to test anything in the MessageWriterStore, each unit test would need to provide an AppDispatcher instance and a mock IReadAndWriteMessages implementation, it would then push one or more messages through the Dispatcher and confirm that the public MessageWriterStore state matches expectations at the end. For example, to test the "Content" text input validation, you would play back a MessageEditStateChanged action with a blank "Content" string in the MessageEditState and ensure that the expected validation message text was present in the "Message" reference of the MessageWriterStore afterwards.
There are a couple of minor things that I'm not so keen about in the code above. Firstly, there's the laborious type-checking and casting that is required when matching the actions. Secondly, there's the duplication on calling "OnChange" whenever an action is matched. Thirdly, the logic around RequestId comparison when a MessageHistoryUpdated is matched is a bit clumsy.
For that third point, add a new file "RequestIdExtensions.cs" to the "API" folder with the following content -
using System;
namespace BridgeReactTutorial.API
{
public static class RequestIdExtensions
{
public static bool IsEqualToOrComesAfter(this RequestId source, RequestId other)
{
if (source == null)
throw new ArgumentNullException("source");
// If the "other" reference is no-RequestId then the "source" may be considered to
// come after it
if (other == null)
return true;
return (source == other) || source.ComesAfter(other);
}
}
}
And for the first two points, we can use some extension methods which are included in the Bridge / React bindings -
using System;
using System.Collections.Generic;
using Bridge.React;
using BridgeReactTutorial.Actions;
using BridgeReactTutorial.API;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Stores
{
public class MessageWriterStore
{
private RequestId _saveActionRequestId, _lastDataUpdatedRequestId;
public MessageWriterStore(IReadAndWriteMessages messageApi, AppDispatcher dispatcher)
{
if (messageApi == null)
throw new ArgumentNullException("messageApi");
if (dispatcher == null)
throw new ArgumentNullException("dispatcher");
Message = GetInitialMessageEditState();
MessageHistory = new Tuple<int, MessageDetails>[0];
dispatcher.Receive(a => a
.If<StoreInitialised>(
condition: action => (action.Store == this),
work: action => { }
)
.Else<MessageEditStateChanged>(action =>
{
Message = action.NewState;
ValidateMessage(Message);
})
.Else<MessageSaveRequested>(action =>
{
_saveActionRequestId = messageApi.SaveMessage(action.Message);
Message.IsSaveInProgress = true;
})
.Else<MessageSaveSucceeded>(
condition: action => (action.RequestId == _saveActionRequestId),
work: action =>
{
_saveActionRequestId = null;
Message = GetInitialMessageEditState();
_lastDataUpdatedRequestId = messageApi.GetMessages();
}
)
.Else<MessageHistoryUpdated>(
condition: action =>
action.RequestId.IsEqualToOrComesAfter(_lastDataUpdatedRequestId),
work: action =>
{
_lastDataUpdatedRequestId = action.RequestId;
MessageHistory = action.Messages;
}
)
.IfAnyMatched(OnChange)
);
}
public event Action Change;
public MessageEditState Message;
public IEnumerable<Tuple<int, MessageDetails>> MessageHistory;
private MessageEditState GetInitialMessageEditState()
{
// Note: By using the ValidateMessage here, we don't need to duplicate the "Untitled"
// string that should be used for the Caption value when the UI is first rendered
// or when the user has entered some Title content but then deleted it again.
// Similarly, we avoid having to repeat the validation messages that should be
// displayed when the form is empty, since they will be set by ValidateMessage.
var blankMessage = new MessageEditState
{
Caption = "",
Title = new TextEditState { Text = "" },
Content = new TextEditState { Text = "" },
IsSaveInProgress = false
};
ValidateMessage(blankMessage);
return blankMessage;
}
private void ValidateMessage(MessageEditState message)
{
if (message == null)
throw new ArgumentNullException("message");
message.Caption = string.IsNullOrWhiteSpace(message.Title.Text)
? "Untitled"
: message.Title.Text.Trim();
message.Title.ValidationError = string.IsNullOrWhiteSpace(message.Title.Text)
? "Must enter a title"
: null;
message.Content.ValidationError = string.IsNullOrWhiteSpace(message.Content.Text)
? "Must enter message content"
: null;
}
private void OnChange()
{
if (Change != null)
Change();
}
}
}
Hopefully it's clear enough how they work. The "If" and the "Else" functions both have a generic type parameter for the kind of action to match and may be called with a single "work" argument (meaning "do this if the action type is matched") or two arguments; "condition" and "work" (where "condition" looks at the action, typed to match the generic type parameter, and returns true or false depending upon whether the action should be considered or ignored). The "condition" argument is most clearly illustrated by the MessageHistoryUpdated, it ensures that any stale MessageHistoryUpdated action will be ignored. The "work" implementation for StoreInitialised is empty because all that is required when a StoreInitialised action is received (that targets the current store) is to call "OnChange" and the "IfAnyMatched" extension method calls "OnChange" if any of the actions are matched.
There's just one final thing to do now in order to make the application compile again, the entry point logic in App.cs needs updating -
using System.Linq;
using Bridge.Html5;
using Bridge.React;
using BridgeReactTutorial.Actions;
using BridgeReactTutorial.API;
using BridgeReactTutorial.Components;
using BridgeReactTutorial.Stores;
namespace BridgeReactTutorial
{
public class App
{
[Ready]
public static void Go()
{
var container = Document.GetElementById("main");
container.ClassName = string.Join(
" ",
container.ClassName.Split().Where(c => c != "loading")
);
var dispatcher = new AppDispatcher();
var messageApi = new MessageApi(dispatcher);
var store = new MessageWriterStore(messageApi, dispatcher);
React.Render(
new AppContainer(new AppContainer.Props
{
Dispatcher = dispatcher,
Store = store
}),
container
);
dispatcher.Dispatch(new StoreInitialised { Store = store });
}
}
}
Both the MessageWriterStore and the AppContainer need a reference to a shared Dispatcher, MessageWriterStore needs a Message API wrapper to talk to and the AppContainer needs a reference to the Store. In a lot of the Flux articles that I read at first, the Dispatcher was a static reference that everything had access to - but I much prefer this arrangement, where the places that explicitly need it (ie. the Stores and the top-level components) have it passed in as a constructor argument or through props. This makes a class' requirements much more explicit - if a class has implicit dependencies then it's more difficult to tell at a glance how it works. And not having a static Dispatcher means that unit testing is simpler, since there is no implicit shared state between elements within an application.
The only part of this code that may not be very intuitive is the need to send a StoreInitialised action to the Dispatcher immediately after setting up all of the references. This is required before the AppContainer won't render anything until it processes its first "Change" event from the MessageWriterStore (because, until that point, the AppContainer's state reference will be null). When the Store receives the StoreInitialised action, it will raise its "Change" event and the AppContainer will perform its first "real" render. If this was an application with a routing element, with a Store per page / form, then it would seem natural for the router to raise a StoreInitialised action for the Store that should be active for the current route (it is just a little odd-looking in a simple application like the example here unless you know why it is necessary).
With that, the change in architecture is complete. Hopefully it's easy to envisage how further functionality is enabled by adding further specialised components and communicating using different action types. Each action is handled in a very clear way in the Store(s) and so the overall complexity should (approximately) grow linearly with the essential complexity, rather than exponentially (which is what tends to happens a lot with more haphazard architectures, or where you have the "star-shaped event handling" that I described earlier).
The only adjustment that I'd like to make at this point is to the actions themselves - if there's a variation of the MessageEditStateChanged, MessageSaveRequested and MessageSaveSucceeded actions required for every kind of form where a user creates / edits data and tries to save it, then there's going to be a lot of action classes that are basically the same.
This seems like a perfect case for C# generics - a single generic class may be used to represent many types of user edit action without sacrificing type safety. Rename the "MessageEditStateChanged.cs" action class to "UserEditRequested.cs" and replace the content, which is currently:
using Bridge.React;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Actions
{
public class MessageEditStateChanged : IDispatcherAction
{
public MessageEditState NewState;
}
}
With this:
using Bridge.React;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Actions
{
public class UserEditRequested<T> : IDispatcherAction
{
public T NewState;
}
public static class UserEditRequested
{
public static UserEditRequested<T> For<T>(T newState)
{
return new UserEditRequested<T> { NewState = newState };
}
}
}
Now, anywhere that there is a reference to MessageEditStateChanged, you will need to change it to be a UserEditRequested<MessageEditState>.
The non-generic "For" function is just for convenience, it allows you to create a new UserEditRequested<T> instance without writing out the type of "T" - it will be inferred by the type of the "newState" reference passed for it. The "OnChange" lambda set on the MessageEditor props was previously
OnChange = newState => props.Dispatcher.Dispatch(
new MessageEditStateChanged { NewState = newState }
)
but should now become
OnChange = newState => props.Dispatcher.Dispatch(
UserEditRequested.For(newState)
)
(Since "newState" is a MessageEditState instance, the action that will be raised will be a UserEditRequested<MessageEditState>).
Now, the action-matching code in the MessageWriteStore needs to change from
.Else<MessageEditStateChanged>(action =>
{
Message = action.NewState;
ValidateMessage(Message);
})
to
.Else<UserEditRequested<MessageEditState>>(action =>
{
Message = action.NewState;
ValidateMessage(Message);
})
Similar changes should be made, so that "MessageSaveRequested.cs" is replaced with
using Bridge.React;
namespace BridgeReactTutorial.Actions
{
public class SaveRequested<T> : IDispatcherAction
{
public T Data;
}
public static class SaveRequested
{
public static SaveRequested<T> For<T>(T data)
{
return new SaveRequested<T> { Data = data };
}
}
}
And "MessageSaveSucceeded.cs" is replaced with
using Bridge.React;
using BridgeReactTutorial.API;
namespace BridgeReactTutorial.Actions
{
public class SaveSucceeded : IDispatcherAction
{
public RequestId RequestId;
}
}
And, finally, "MessageHistoryUpdated.cs" replaced with
using Bridge.React;
using BridgeReactTutorial.API;
namespace BridgeReactTutorial.Actions
{
public class DataUpdated<T> : IDispatcherAction
{
public RequestId RequestId;
public T Data;
}
public static class DataUpdated
{
public static DataUpdated<T> For<T>(RequestId requestId, T data)
{
return new DataUpdated<T> { RequestId = requestId, Data = data };
}
}
}
(Note that SaveSucceeded is not a generic class because the only information that it contains is the RequestId that corresponds to the save operation that it indicates has completed).
The action type-matching that occurs in the MessageWriteStore now needs to be changed to:
dispatcher.Receive(a => a
.If<StoreInitialised>(
condition: action => (action.Store == this),
work: action => { }
)
.Else<UserEditRequested<MessageEditState>>(action =>
{
Message = action.NewState;
ValidateMessage(Message);
})
.Else<SaveRequested<MessageDetails>>(action =>
{
_saveActionRequestId = messageApi.SaveMessage(action.Data);
Message.IsSaveInProgress = true;
})
.Else<SaveSucceeded>(
condition: action => (action.RequestId == _saveActionRequestId),
work: action =>
{
_saveActionRequestId = null;
Message = GetInitialMessageEditState();
_lastDataUpdatedRequestId = messageApi.GetMessages();
}
)
.Else<DataUpdated<IEnumerable<Tuple<int, MessageDetails>>>>(
condition: action =>
action.RequestId.IsEqualToOrComesAfter(_lastDataUpdatedRequestId),
work: action =>
{
_lastDataUpdatedRequestId = action.RequestId;
MessageHistory = action.Data;
}
)
.IfAnyMatched(OnChange)
);
The AppContainer now becomes:
using System;
using System.Collections.Generic;
using Bridge.React;
using BridgeReactTutorial.Actions;
using BridgeReactTutorial.ViewModels;
using BridgeReactTutorial.Stores;
namespace BridgeReactTutorial.Components
{
public class AppContainer : Component<AppContainer.Props, AppContainer.State>
{
public AppContainer(AppContainer.Props props) : base(props) { }
protected override void ComponentDidMount()
{
props.Store.Change += StoreChanged;
}
protected override void ComponentWillUnmount()
{
props.Store.Change -= StoreChanged;
}
private void StoreChanged()
{
SetState(new State
{
Message = props.Store.Message,
MessageHistory = props.Store.MessageHistory
});
}
public override ReactElement Render()
{
if (state == null)
return null;
return DOM.Div(null,
new MessageEditor(new MessageEditor.Props
{
ClassName = "message",
Message = state.Message,
OnChange = newState => props.Dispatcher.Dispatch(
UserEditRequested.For(newState)
),
OnSave = () => props.Dispatcher.Dispatch(
SaveRequested.For(new MessageDetails
{
Title = state.Message.Title.Text,
Content = state.Message.Content.Text
})
)
}),
new MessageHistory(new MessageHistory.Props
{
ClassName = "history",
Messages = state.MessageHistory
})
);
}
public class Props
{
public AppDispatcher Dispatcher;
public MessageWriterStore Store;
}
public class State
{
public MessageEditState Message;
public IEnumerable<Tuple<int, MessageDetails>> MessageHistory;
}
}
}
And the MessageApi:
using System;
using System.Collections.Generic;
using System.Linq;
using Bridge;
using Bridge.Html5;
using Bridge.React;
using BridgeReactTutorial.Actions;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.API
{
public class MessageApi : IReadAndWriteMessages
{
private readonly AppDispatcher _dispatcher;
private readonly List<Tuple<int, MessageDetails>> _messages;
public MessageApi(AppDispatcher dispatcher)
{
if (dispatcher == null)
throw new ArgumentException("dispatcher");
_dispatcher = dispatcher;
_messages = new List<Tuple<int, MessageDetails>>();
// To further mimic a server-based API (where other people may be recording messages
// of their own), after a 10s delay a periodic task will be executed to retrieve a
// new message
Window.SetTimeout(
() => Window.SetInterval(GetChuckNorrisFact, 5000),
10000
);
}
public RequestId SaveMessage(MessageDetails message)
{
return SaveMessage(message, optionalSaveCompletedCallback: null);
}
private RequestId SaveMessage(MessageDetails message, Action optionalSaveCompletedCallback)
{
if (message == null)
throw new ArgumentNullException("message");
if (string.IsNullOrWhiteSpace(message.Title))
throw new ArgumentException("A title value must be provided");
if (string.IsNullOrWhiteSpace(message.Content))
throw new ArgumentException("A content value must be provided");
var requestId = new RequestId();
Window.SetTimeout(
() =>
{
_messages.Add(Tuple.Create(_messages.Count, message));
_dispatcher.Dispatch(new SaveSucceeded { RequestId = requestId });
if (optionalSaveCompletedCallback != null)
optionalSaveCompletedCallback();
},
1000 // Simulate a roundtrip to the server
);
return requestId;
}
public RequestId GetMessages()
{
// ToArray is used to return a clone of the message set - otherwise, the caller would
// end up with a list that is updated when the internal reference within this class
// is updated (which sounds convenient but it's not the behaviour that would be
// exhibited if this was really persisting messages to a server somewhere).. and
// then AsEnumerable() is required since the store checks for an action of type
// DataUpdated<IEnumerable<Tuple<int, MessageDetails>>>>
var requestId = new RequestId();
Window.SetTimeout(
() => _dispatcher.Dispatch(DataUpdated.For(requestId, _messages.ToArray().AsEnumerable())),
1000 // Simulate a roundtrip to the server
);
return requestId;
}
private void GetChuckNorrisFact()
{
var request = new XMLHttpRequest();
request.ResponseType = XMLHttpRequestResponseType.Json;
request.OnReadyStateChange = () =>
{
if (request.ReadyState != AjaxReadyState.Done)
return;
if ((request.Status == 200) || (request.Status == 304))
{
try
{
var apiResponse = (ChuckNorrisFactApiResponse)request.Response;
if ((apiResponse.Type == "success")
&& (apiResponse.Value != null)
&& !string.IsNullOrWhiteSpace(apiResponse.Value.Joke))
{
// The Chuck Norris Facts API (http://www.icndb.com/api/) returns strings
// html-encoded, so they need decoding before be wrapped up in a
// MessageDetails instance
// - Note: After the save has been processed, GetMessages is called so
// that a MessageHistoryUpdate action is dispatched
SaveMessage(
new MessageDetails
{
Title = "Fact",
Content = HtmlDecode(apiResponse.Value.Joke)
},
() => GetMessages()
);
return;
}
}
catch
{
// Ignore any error and drop through to the fallback message-generator below
}
}
SaveMessage(new MessageDetails
{
Title = "Fact",
Content = "API call failed when polling for server content :("
});
};
request.Open("GET", "http://api.icndb.com/jokes/random");
request.Send();
}
private string HtmlDecode(string value)
{
if (value == null)
throw new ArgumentNullException("value");
var wrapper = Document.CreateElement("div");
wrapper.InnerHTML = value;
return wrapper.TextContent;
}
[IgnoreCast]
private class ChuckNorrisFactApiResponse
{
public extern string Type { [Template("type")] get; }
public extern FactDetails Value { [Template("value")] get; }
[IgnoreCast]
public class FactDetails
{
public extern int Id { [Template("id")] get; }
public extern string Joke { [Template("joke")]get; }
}
}
}
}
(I contemplated leaving it as an exercise for the reader to change all of the action instantiation code to use the new generic classes, but with so much code in this post already I thought I might as well go whole-hog and include the final version of the everything!)
No type-safety has been lost in this refactor but, hopefully, it's clear how action classes can scale with an application's complexity. Needing a lot of new actions every time that a new section is added to an application would be no fun and would just add to the code churn required, so being able to reuse actions like this is a boon.
We've covered a lot of ground here, so I'm going to draw things to a close. Flux is a big deal and I've read plenty of articles that make it sound mind-bending and difficult to grasp. Hopefully this has explored why it makes sense as much as how to think in terms of it (and write code in the style of it!).
Next time, now that React and Flux are dealt with, I want to look at how we can go further in the quest to make code easier to reason about and to test - there is a lot more that we can do with the C# type system to really express intent, requirements and limitations and I strongly believe that doing so will lead to higher quality code. As a side benefit, I'll also be showing how to make a simple tweak to components that will offer potentially huge performance benefits to highly-complex / deeply-nested UIs. I really think that using C# and Bridge can be more than just a possibility for writing React applications, it can be one of the best ways to do so!
Posted at 22:50
15 March 2016
(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! :)
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.
(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).
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).
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:
As more messages are written, more entries will appear in the "Message History". Seems simple.
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.
Almost all of the time, components that you use will be "controlled components" -
A Controlled component does not maintain its own internal state; the component renders purely based on props.
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.
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".
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).
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 -
(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).
public class TextInput : Component<TextInput.Props, TextInput.State>
{
public TextInput(Props props) : base(props) { }
public override ReactElement Render()
{
return DOM.Input(new InputAttributes
{
Type = InputType.Text,
Value = (state == null) ? props.InitialValue : state.Value,
OnChange = ev =>
{
var newValue = ev.CurrentTarget.Value;
SetState(new State { Value = newValue });
props.OnChange(newValue);
}
});
}
public class Props
{
public string InitialValue;
public Action<string> OnChange;
}
public class State
{
public string Value;
}
}
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).
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).
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.
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 -
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.
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) -
public class TextInput : StatelessComponent<TextInput.Props>
{
public TextInput(Props props) : base(props) { }
public override ReactElement Render()
{
return DOM.Input(new InputAttributes
{
Type = InputType.Text,
Value = props.Value,
OnChange = ev => props.OnChange(ev.CurrentTarget.Value)
});
}
public class Props
{
public string Value;
public Action<string> OnChange;
}
}
The component code is much more succinct this way, as well as helping us avoid the nightmare scenario described above.
It does leave one big question, though.. if these components don't update themselves, then what does?
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.
In the sample application here -
The component hierarchy will look something like this:
AppContainer
MessageEditor
Span ("Title")
ValidatedTextInput
Input
Span ("Content")
ValidatedTextInput
Input
Button
MessageHistory
Div
Span (Message Title)
Span (Message Content)
Div
Span (Message Title)
Span (Message Content)
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.
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<InputElement> callback that the input raises in a simpler Action<string>). 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).
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.
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.
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.
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).
(The Facebook tutorial Thinking in React 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).
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.
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.
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:
{
"output": "Bridge/output",
"combineScripts": true
}
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.
Now create an empty html file in the project root called "demo.html" and paste in the following:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>Bridge.React Tutorial</title>
<link rel="Stylesheet" type="text/css" href="styles.css" media="screen" />
</head>
<body>
<noscript>JavaScript is required</noscript>
<div id="main" class="loading">Loading..</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react-dom.js"></script>
<script src="Bridge/output/BridgeReactTutorial.js"></script>
</body>
</html>
(If you called your project something other than "BridgeReactTutorial" then you might have to change the filename in that last script tag).
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.
When you created the class library project, a Class1.cs file will have been added to the project. Change its contents -
using System.Linq;
using Bridge.Html5;
using Bridge.React;
namespace BridgeReactTutorial
{
public class Class1
{
[Ready]
public static void Main()
{
var container = Document.GetElementById("main");
container.ClassName = string.Join(
" ",
container.ClassName.Split().Where(c => c != "loading")
);
React.Render(
DOM.Div(new Attributes { ClassName = "welcome" }, "Hi!"),
container
);
}
}
}
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!
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-perceptible 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).
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) -
/* http://meyerweb.com/eric/tools/css/reset/ v2.0b1 | 201101 NOTE: WORK IN PROGRESS
USE WITH CAUTION AND TEST WITH ABANDON */
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote,
pre,a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s,
samp, small, strike, strong, sub, sup, tt, var,b, u, i, center, dl, dt, dd, ol, ul,
li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure, footer, header, hgroup, menu,
nav, section, summary, time, mark, audio, video
{
margin: 0;
padding: 0;
border: 0;
outline: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav,
section { display: block; }
body { line-height: 1; }
ol, ul { list-style: none; }
blockquote, q { quotes: none; }
blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; }
/* remember to highlight inserts somehow! */ ins { text-decoration: none; }
del { text-decoration: line-through; }
table { border-collapse: collapse; border-spacing: 0; }
div#main.loading { color: #f1f1f1; }
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.
That's the theory and the project scaffolding out of the way. Now to create a form that actually does something.
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 -
using System;
using Bridge.Html5;
using Bridge.React;
namespace BridgeReactTutorial.Components
{
public class TextInput : StatelessComponent<TextInput.Props>
{
public TextInput(Props props) : base(props) { }
public override ReactElement Render()
{
return DOM.Input(new InputAttributes
{
Type = InputType.Text,
ClassName = props.ClassName,
Value = props.Content,
OnChange = e => props.OnChange(e.CurrentTarget.Value)
});
}
public class Props
{
public string ClassName;
public string Content;
public Action<string> OnChange;
}
}
}
(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).
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 -
namespace BridgeReactTutorial.ViewModels
{
public class MessageDetails
{
public string Title;
public string Content;
}
}
Now add another file to the "Components" folder; "MessageEditor.cs" and paste in this:
using System;
using Bridge.React;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Components
{
public class MessageEditor : StatelessComponent<MessageEditor.Props>
{
public MessageEditor(Props props) : base(props) { }
public override ReactElement Render()
{
return DOM.FieldSet(new FieldSetAttributes { ClassName = props.ClassName },
DOM.Legend(null, string.IsNullOrWhiteSpace(props.Title) ? "Untitled" : props.Title),
DOM.Span(new Attributes { ClassName = "label" }, "Title"),
new TextInput(new TextInput.Props
{
ClassName = "title",
Content = props.Title,
OnChange = newTitle => props.OnChange(new MessageDetails
{
Title = newTitle,
Content = props.Content
})
}),
DOM.Span(new Attributes { ClassName = "label" }, "Content"),
new TextInput(new TextInput.Props
{
ClassName = "content",
Content = props.Content,
OnChange = newContent => props.OnChange(new MessageDetails
{
Title = props.Title,
Content = newContent
})
})
);
}
public class Props
{
public string ClassName;
public string Title;
public string Content;
public Action<MessageDetails> OnChange;
}
}
}
Now things are getting interesting!
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 TextInputs raises an on-change event, the MessageEditor raises its on-change events with a new MessageDetails instance.
Note that there's no validation yet. We'll get this rough version working first and then add that later.
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" -
using Bridge.Html5;
using Bridge.React;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Components
{
public class AppContainer : Component<object, AppContainer.State>
{
public AppContainer() : base(null) { }
protected override State GetInitialState()
{
return new State
{
Message = new MessageDetails { Title = "", Content = "" }
};
}
public override ReactElement Render()
{
return new MessageEditor(new MessageEditor.Props
{
ClassName = "message",
Title = state.Message.Title,
Content = state.Message.Content,
OnChange = newMessage => SetState(new State { Message = newMessage })
});
}
public class State
{
public MessageDetails Message;
}
}
}
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<TProps, TState> base class is just "object".
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.
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.
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" -
using System.Linq;
using Bridge.Html5;
using Bridge.React;
using BridgeReactTutorial.Components;
namespace BridgeReactTutorial
{
public class App
{
[Ready]
public static void Go()
{
var container = Document.GetElementById("main");
container.ClassName = string.Join(
" ",
container.ClassName.Split().Where(c => c != "loading")
);
React.Render(new AppContainer(), container);
}
}
}
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" -
body
{
font-family: 'Segoe UI';
padding: 8px;
}
fieldset
{
padding: 8px;
border: 1px solid #f1f1f1;
border-radius: 4px;
}
fieldset legend
{
color: blue;
padding: 0 8px;
}
fieldset.message span.label { padding: 0 8px; }
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.
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!
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 artificial delays to mimic server-calling time.
Create a new folder in the project root called "API" and add a new .cs file "IReadAndWriteMessages.cs", the contents of which should be:
using System.Threading.Tasks;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.API
{
public interface IReadAndWriteMessages
{
Task SaveMessage(MessageDetails message);
}
}
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.
Add another file into the "API" folder, "MessageApi.cs" -
using System;
using System.Threading.Tasks;
using Bridge.Html5;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.API
{
public class MessageApi : IReadAndWriteMessages
{
public Task SaveMessage(MessageDetails message)
{
if (message == null)
throw new ArgumentNullException("message");
if (string.IsNullOrWhiteSpace(message.Title))
throw new ArgumentException("A title value must be provided");
if (string.IsNullOrWhiteSpace(message.Content))
throw new ArgumentException("A content value must be provided");
var task = new Task<object>(null);
Window.SetTimeout(
() => task.Complete(),
1000 // Simulate a roundtrip to the server
);
return task;
}
}
}
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.
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.
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 instantaneously 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.
The MessageEditor should now looks like this:
using System;
using Bridge.React;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Components
{
public class MessageEditor : StatelessComponent<MessageEditor.Props>
{
public MessageEditor(Props props) : base(props) { }
public override ReactElement Render()
{
return DOM.FieldSet(new FieldSetAttributes { ClassName = props.ClassName },
DOM.Legend(null, string.IsNullOrWhiteSpace(props.Title) ? "Untitled" : props.Title),
DOM.Span(new Attributes { ClassName = "label" }, "Title"),
new TextInput(new TextInput.Props
{
ClassName = "title",
Disabled = props.Disabled,
Content = props.Title,
OnChange = newTitle => props.OnChange(new MessageDetails
{
Title = newTitle,
Content = props.Content
})
}),
DOM.Span(new Attributes { ClassName = "label" }, "Content"),
new TextInput(new TextInput.Props
{
ClassName = "content",
Disabled = props.Disabled,
Content = props.Content,
OnChange = newContent => props.OnChange(new MessageDetails
{
Title = props.Title,
Content = newContent
})
}),
DOM.Button(
new ButtonAttributes { Disabled = props.Disabled, OnClick = e => props.OnSave() },
"Save"
)
);
}
public class Props
{
public string ClassName;
public string Title;
public string Content;
public Action<MessageDetails> OnChange;
public Action OnSave;
public bool Disabled;
}
}
}
The "Disabled" flag needs to be able to be applied to the TextInput components, so TextInput needs to look like this:
using System;
using Bridge.Html5;
using Bridge.React;
namespace BridgeReactTutorial.Components
{
public class TextInput : StatelessComponent<TextInput.Props>
{
public TextInput(Props props) : base(props) { }
public override ReactElement Render()
{
return DOM.Input(new InputAttributes
{
Type = InputType.Text,
ClassName = props.ClassName,
Disabled = props.Disabled,
Value = props.Content,
OnChange = e => props.OnChange(e.CurrentTarget.Value)
});
}
public class Props
{
public string ClassName;
public bool Disabled;
public string Content;
public Action<string> OnChange;
}
}
}
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.
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.
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 -
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.
The AppContainer will now become the following -
using Bridge.React;
using BridgeReactTutorial.API;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Components
{
public class AppContainer : Component<AppContainer.Props, AppContainer.State>
{
public AppContainer(AppContainer.Props props) : base(props) { }
protected override State GetInitialState()
{
return new State
{
Message = new MessageDetails { Title = "", Content = "" },
IsSaveInProgress = false
};
}
public override ReactElement Render()
{
return new MessageEditor(new MessageEditor.Props
{
ClassName = "message",
Title = state.Message.Title,
Content = state.Message.Content,
OnChange = newMessage => SetState(new State
{
Message = newMessage,
IsSaveInProgress = state.IsSaveInProgress
}),
OnSave = async () =>
{
SetState(new State { Message = state.Message, IsSaveInProgress = true });
await props.MessageApi.SaveMessage(state.Message);
SetState(new State
{
Message = new MessageDetails { Title = "", Content = "" },
IsSaveInProgress = false
});
},
Disabled = state.IsSaveInProgress
});
}
public class Props
{
public IReadAndWriteMessages MessageApi;
}
public class State
{
public MessageDetails Message;
public bool IsSaveInProgress;
}
}
}
You will need to update App.cs to pass a props reference with a MessageApi instance to the AppContainer constructor -
using System.Linq;
using Bridge.Html5;
using Bridge.React;
using BridgeReactTutorial.API;
using BridgeReactTutorial.Components;
namespace BridgeReactTutorial
{
public class App
{
[Ready]
public static void Go()
{
var container = Document.GetElementById("main");
container.ClassName = string.Join(
" ",
container.ClassName.Split().Where(c => c != "loading")
);
React.Render(
new AppContainer(new AppContainer.Props { MessageApi = new MessageApi() }),
container
);
}
}
}
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 stateless 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.
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..
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 -
using System;
using Bridge.React;
namespace BridgeReactTutorial.Components
{
public class ValidatedTextInput : StatelessComponent<ValidatedTextInput.Props>
{
public ValidatedTextInput(Props props) : base(props) { }
public override ReactElement Render()
{
var className = props.ClassName;
if (!string.IsNullOrWhiteSpace(props.ValidationMessage))
className = (className + " invalid").Trim();
return DOM.Span(new Attributes { ClassName = className },
new TextInput(new TextInput.Props
{
ClassName = props.ClassName,
Disabled = props.Disabled,
Content = props.Content,
OnChange = props.OnChange
}),
string.IsNullOrWhiteSpace(props.ValidationMessage)
? null
: DOM.Span(
new Attributes { ClassName = "validation-message" },
props.ValidationMessage
)
);
}
public class Props
{
public string ClassName;
public bool Disabled;
public string Content;
public Action<string> OnChange;
public string ValidationMessage;
}
}
}
This allows the MessageEditor to be changed to use these ValidatedTextInputs instead of regular TextInputs, setting the "ValidationMessage" values according to whether the "Content" string has a value -
using System;
using Bridge.React;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Components
{
public class MessageEditor : StatelessComponent<MessageEditor.Props>
{
public MessageEditor(Props props) : base(props) { }
public override ReactElement Render()
{
var formIsInvalid =
string.IsNullOrWhiteSpace(props.Title) ||
string.IsNullOrWhiteSpace(props.Content);
return DOM.FieldSet(new FieldSetAttributes { ClassName = props.ClassName },
DOM.Legend(null, string.IsNullOrWhiteSpace(props.Title) ? "Untitled" : props.Title),
DOM.Span(new Attributes { ClassName = "label" }, "Title"),
new ValidatedTextInput(new ValidatedTextInput.Props
{
ClassName = "title",
Disabled = props.Disabled,
Content = props.Title,
OnChange = newTitle => props.OnChange(new MessageDetails
{
Title = newTitle,
Content = props.Content
}),
ValidationMessage = string.IsNullOrWhiteSpace(props.Title)
? "Must enter a title"
: null
}),
DOM.Span(new Attributes { ClassName = "label" }, "Content"),
new ValidatedTextInput(new ValidatedTextInput.Props
{
ClassName = "content",
Disabled = props.Disabled,
Content = props.Content,
OnChange = newContent => props.OnChange(new MessageDetails
{
Title = props.Title,
Content = newContent
}),
ValidationMessage = string.IsNullOrWhiteSpace(props.Content)
? "Must enter message content"
: null
}),
DOM.Button(
new ButtonAttributes
{
Disabled = props.Disabled || formIsInvalid,
OnClick = e => props.OnSave()
},
"Save"
)
);
}
public class Props
{
public string ClassName;
public string Title;
public string Content;
public Action<MessageDetails> OnChange;
public Action OnSave;
public bool Disabled;
}
}
}
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!
(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).
To keep things looking pretty, adding the following to "styles.css" -
fieldset.message span.title, fieldset.message span.content { position: relative; }
fieldset.message span.validation-message
{
position: absolute;
top: -6px;
right: 2px;
padding: 2px 4px;
font-size: 70%;
background: #FFF9D8;
border: 1px solid #EFE9CB;
border-radius: 2px;
color: #A8A390;
}
fieldset.message button { margin-left: 8px; }
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" -
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.API
{
public interface IReadAndWriteMessages
{
Task SaveMessage(MessageDetails message);
Task<IEnumerable<Tuple<int, MessageDetails>>> GetMessages();
}
}
This needs implementing in MessageApi -
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bridge.Html5;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.API
{
public class MessageApi : IReadAndWriteMessages
{
private readonly List<Tuple<int, MessageDetails>> _messages;
public MessageApi()
{
_messages = new List<Tuple<int, MessageDetails>>();
}
public Task SaveMessage(MessageDetails message)
{
if (message == null)
throw new ArgumentNullException("message");
if (string.IsNullOrWhiteSpace(message.Title))
throw new ArgumentException("A title value must be provided");
if (string.IsNullOrWhiteSpace(message.Content))
throw new ArgumentException("A content value must be provided");
var task = new Task<object>(null);
Window.SetTimeout(
() =>
{
_messages.Add(Tuple.Create(_messages.Count, message));
task.Complete();
},
1000 // Simulate a roundtrip to the server
);
return task;
}
public Task<IEnumerable<Tuple<int, MessageDetails>>> GetMessages()
{
// ToArray is used to return a clone of the message set - otherwise, the caller would
// end up with a list that is updated when the internal reference within this class
// is updated (which sounds convenient but it's not the behaviour that would be
// exhibited if this was really persisting messages to a server somewhere)
var task = new Task<IEnumerable<Tuple<int, MessageDetails>>>(null);
Window.SetTimeout(
() => task.Complete(_messages.ToArray()),
1000 // Simulate a roundtrip to the server
);
return task;
}
}
}
Now, we'll need a way to render this information -
using System;
using System.Collections.Generic;
using System.Linq;
using Bridge.React;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Components
{
public class MessageHistory : StatelessComponent<MessageHistory.Props>
{
public MessageHistory(Props props) : base(props) { }
public override ReactElement Render()
{
var className = props.ClassName;
if (!props.Messages.Any())
className = (className + " zero-messages").Trim();
// Any time a set of child components is dynamically-created (meaning that the
// numbers of items may vary from one render to another), each must have a unique
// "Key" property set (this may be a int or a string). Here, this is simple as
// each message tuple is a unique id and the contents of that message (and the
// unique id is ideal for use as a unique "Key" property).
var messageElements = props.Messages
.Select(idAndMessage => DOM.Div(new Attributes { Key = idAndMessage.Item1 },
DOM.Span(new Attributes { ClassName = "title" }, idAndMessage.Item2.Title),
DOM.Span(new Attributes { ClassName = "content" }, idAndMessage.Item2.Content)
));
return DOM.FieldSet(new FieldSetAttributes { ClassName = className },
DOM.Legend(null, "Message History"),
DOM.Div(null, messageElements)
);
}
public class Props
{
public string ClassName;
public IEnumerable<Tuple<int, MessageDetails>> Messages;
}
}
}
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".
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:
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).
The quote above is from Facebook's docs about Dynamic Children - and so "clobbered" must be an official term!
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).
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 this sample app, but it's also often easy when persisting by sending the data over the wire to a database somewhere; it's common for the database to generate unique ids for each record, and this would be returned in the data from "GetMessages".
Now we have to return to the AppContainer to tie everything together; we need to add the message history data to the AppContainer's state, we need to read the message history after we save and we need to render the message history -
using System;
using System.Collections.Generic;
using Bridge.React;
using BridgeReactTutorial.API;
using BridgeReactTutorial.ViewModels;
namespace BridgeReactTutorial.Components
{
public class AppContainer : Component<AppContainer.Props, AppContainer.State>
{
public AppContainer(AppContainer.Props props) : base(props) { }
protected override State GetInitialState()
{
return new State
{
Message = new MessageDetails { Title = "", Content = "" },
IsSaveInProgress = false,
MessageHistory = new Tuple<int, MessageDetails>[0]
};
}
public override ReactElement Render()
{
return DOM.Div(null,
new MessageEditor(new MessageEditor.Props
{
ClassName = "message",
Title = state.Message.Title,
Content = state.Message.Content,
OnChange = newMessage => SetState(new State
{
Message = newMessage,
IsSaveInProgress = state.IsSaveInProgress,
MessageHistory = state.MessageHistory
}),
OnSave = async () =>
{
// Set SaveInProgress to true while the save operation is requested
SetState(new State {
Message = state.Message,
IsSaveInProgress = true,
MessageHistory = state.MessageHistory
});
await props.MessageApi.SaveMessage(state.Message);
// After the save has completed, clear the message entry form and reset
// SaveInProgress to false
SetState(new State {
Message = new MessageDetails { Title = "", Content = "" },
IsSaveInProgress = false,
MessageHistory = state.MessageHistory
});
// Then re-load the message history state and re-render when that data arrives
var allMessages = await props.MessageApi.GetMessages();
SetState(new State {
Message = state.Message,
IsSaveInProgress = state.IsSaveInProgress,
MessageHistory = allMessages
});
},
Disabled = state.IsSaveInProgress
}),
new MessageHistory(new MessageHistory.Props
{
ClassName = "history",
Messages = state.MessageHistory
})
);
}
public class Props
{
public IReadAndWriteMessages MessageApi;
}
public class State
{
public MessageDetails Message;
public bool IsSaveInProgress;
public IEnumerable<Tuple<int, MessageDetails>> MessageHistory;
}
}
}
The logic in this component is getting more complicated now, which is down to the event-handling; what needs to happen when this happens and then this happens and then this happens (eg. the user clicks save, we initiate a save request, the API completes the request, we update the UI to clear the form and then start loading the updated message history, then we update the UI with the new message content).
One of the goals going forward will be to separate out this logic, away from the React components. The reason that I've made a couple of mentions of moving towards "dumb components" is that it makes unit testing much easier - everything outside of the React components will be simple C# / JavaScript code, which is always easier to test than UI elements, even when the UI is created using the React library. Another benefit of separating application logic from the UI components is that it makes both sides easier to reason about - and this is another theme that I'll be re-visiting during this mini-series.
It's worth mentioning that, even though it's gotten more complicated, the AppContainer (the only stateful component in the application) still adheres to the stateful component guidelines:
All of the data required to render the UI is present in the state. The props data is only required within "Render" in order to process some of the callbacks from the child components. Any changes that must then be reflected in the UI come through a call to SetState - at the point of the SetState-triggered re-render, all of the data required to generate the child components will, once again, be present entirely within the state data.
To keep things look nice, add the following to "styles.css" -
fieldset.history
{
opacity: 1;
transition: opacity .5s ease-in-out;
}
fieldset.history.zero-messages { opacity: 0; }
fieldset.history span.title
{
padding: 0 8px;
font-weight: bold;
}
This will have the MessageHistory invisible to begin with, fading in when the first message is available to display.
I think this makes a good point at which to draw the first part of this series to a close. To be honest, we haven't got very close at all yet to the "The Dan Way" of writing React applications - so far, it's been fairly straight-forward and in-line with the basic React guidelines from Facebook.
Which isn't to save that we haven't covered a lot of good ground! This will serve as a good base from which we can improve things. But we haven't seen the "Flux architecture" at all yet, and have only hinted at why we would want it. I'm not happy with how many of the properties on the various props, state and other data types are presented - one of my pet peeves with APIs is not knowing what can and can't be null; on the TextInput's Props class, the "ClassName" string may be null but the "OnChange" callback must not be. These facts are not clear from just looking at the class. Similarly, it would be nice to know whether or not there are any guarantees about the "Title" and "Content" strings on the MessageDetails class (is it ever really acceptable for them to be null?). Finally, the reading and writing of messages through the MessageApi implementation we have here works fine for one person doing all the writing, but how could we deal with it if the MessageApi simulated a server-based API that received new messages from other uses, either through some sort of polling or through a push mechanism? This is an important question for systems that have to support multiple users.
All of these questions will be answered in later posts, along with further advice to try to help you do what I think React does best - write code that is easier to reason about, and thus easier to read, maintain and extend.
Posted at 23:55
Dan is a big geek who likes making stuff with computers! He can be quite outspoken so clearly needs a blog :)
In the last few minutes he seems to have taken to referring to himself in the third person. He's quite enjoying it.