In my previous article, MVVM to Flux and Back Again, I wrote about my journey with Model-View-ViewModel (MVVM) in .NET applications developed with Windows Presentation Foundation (WPF). It provides good support for MVVM through binding and resource instantiation. You can still use MVVM with WinForms. The same interfaces work the same way. The issue with it is that you need to write your binding expression in code-behind and instantiate the binding objects themselves instead of having a nice expressive way like we have with eXtensible Application Markup Language (XAML).
I believe this pattern can be applied with ReactJS as well in order to maintain a clean and easy-to-understand code. And I think it is a better alternative to Flux.
This article aims to present the pattern and its core concepts alongside an implementation to better understand how it works. And later on help you decide if it is a good fit for your application. The core implementation, with a few more features, is available through the react-model-view-viewmodel library. There is a tutorial available on the project site to help familiarise with the library as well as documentation.
Pattern Overview
Before we start to code, we will go through a brief presentation and cover the core concepts.
Model
The model can have multiple meanings. It could be the database model. It could be the types that describe the business model, and not the types that describe the database schema. It could be objects that perform business logic. Depending on the context, the Model can be one thing or another.
For MVVM, the Model means the objects that do things or represent things that have nothing to do with with the UI. These objects can go about their business without much concern about how methods are being called or data is being shown. These are objects that represent data, access data, manipulate data, perform computation, and do other things. Basically, there are Views, there are ViewModels and everything else is referred to as the Model which can be broken down further into Data Transfer Objects, Data Access Objects, Services, and so on.
View
The View is responsible with user interaction and nothing more. The View does not perform any action, what it does instead is pass these requests to the ViewModel. Generally, when a button is clicked, a method of the ViewModel is called. An exception would be displaying a confirmation dialog when deleting an item. That falls under the UI responsibility to ask for user confirmation. The actual delete is performed by the ViewModel. But the View keeps track of the visibility of the dialog, not the ViewModel. The latter does not need to be concerned whether a confirmation was provided or not. That is part of user interaction and has little to do with the actual deleting of an item.
The View is made out of components, some are more complex than others. Some request the instantiation of a ViewModel while others may request one and then we have the case where components have no idea about the existence of any ViewModel. They just display a text or translate a label. The View is responsible with applying translations and formatting as well. The localization is part of user interaction and not performing user-requested actions.
Routing, or navigation, is part of the View. ViewModels do not have much context on how they are used. The lifecycle management of a ViewModel is known to the Views thus allowing them to instantiate one where it is necessary. It can be at the top level so the same ViewModel is available in all parts of the application.
Notifications are a good example of this. Instantiation can happen at the page level, this is probably the most common case as one page deals with just one main entity, in most cases, or a list of entities of the same type. This will ensure that a new instance is created each time the user navigates to that page meaning that we start with a clean state each time this happens. When the user leaves the page, the reference to the ViewModel is lost allowing it to be reclaimed by the garbage collector. All of this is done automatically for us.
ViewModel
The ViewModel is the link between the View and the Model. It sits between the two. It loads data from the Model and provides it to the View, the View calls commands, or methods, exposed by the ViewModel that in turn update the Model. Whenever the ViewModel changes, it notifies the View about this. When the View has control, it reads data from the ViewModel or calls one of its commands or methods. When the ViewModel has control and wants to relay information to the View, it raises events so that the View becomes aware that something has happened.
In most cases, an event is raised by a ViewModel when one of its properties has changed. Essentially, ViewModels implement the Observer pattern. When working with asynchronous code, it may be difficult for the View to know when the operation has been completed. We could use Promises, await them, and perform changes when they complete.
An alternative, in addition to using Promises, is to expose events that represent the completion of the operation. If we have a save operation on our ViewModel, it can be easier for the View to subscribe to a “saved” event that is raised when the respective operation completes. This will allow multiple components to watch for this event and update accordingly. At the same time, we are not tied to awaiting the asynchronous operation. We are not obliged to await the Promise from the View, we know the operation was completed when the event was raised.
It can get tricky when we try to offload View responsibilities on the ViewModel. Reiterating the confirmation dialog scenario, a ViewModel can maintain a flag that indicates whether a dialog is open or not. This will allow us to pass that ViewModel around and dismiss the dialog from multiple places. This is wrong because the ViewModel is not responsible with what the View is currently showing in terms of layout. The responsibility of the ViewModel is to provide data and commands, not tell which dialogs are open on the View. The View is responsible for that. The reason I am insisting on this is because it is easy to mix the two and then get a murky ViewModel design. The biggest challenge with MVVM is designing ViewModels.
Getting Started with MVVM in ReactJS
In this article, I will be using React with Hooks as this is the latest version, and TypeScript as this provides type safety. And we will know what we are dealing with.
Let’s start with mapping the concepts to React and TypeScript elements.
The View consists of all the React components that we define and use.
A ViewModel is any object that can be observed. Namely, they implement the INotifyPropertiesChanged interface which is based on INotifyPropertyChanged from .NET or they implement the INotifyCollectionChanged interface which is based on the interface with the same name in .NET, INotifyCollectionChanged.
The Model is everything else. Any additional objects we use in an application.
An Event System
JavaScript does not have language support for defining events, even though HTML elements come with a variety of events. And they have a system of their own on how the subscribed callbacks are being called.
There are a number of libraries that bring events to JavaScript. For simplicity, we will define the related types for enabling this mechanism and, of course, it is going to be based on .NET.
The event subscription in .NET is done rather simple. It is based on delegates which are references to methods. In other words, callbacks. Subscription and unsubscription are done by adding or removing a delegate (callback) to the invocation list. Only the object declaring an event can raise it. An event has three methods, out of which only two are publicly accessible: the add and remove methods. While the last method is available only the object declaring the event, an invoke, notify, or raise method.
interface IEvent<TEventArgs> { subscribe(eventHandler: IEventHandler<TEventArgs>): void; unsubscribe(eventHandler: IEventHandler<TEventArgs>): void; } interface IEventHandler<TEventArgs> { handle(subject: any, eventArgs: TEventArgs): void; }
The first interface is for objects defining an event. This is how they expose one while the second interface is for event handlers, objects that handle the event.
Next, we will define a default implementation of an event that will allow us to notify observers.
class DispatchEvent<TEventArgs> implements IEvent<TEventArgs> { private _eventHandlers: IEventHandler<any>[] = []; public subscribe(eventHandler: IEventHandler<TEventArgs>): void { this._eventHandlers = this._eventHandlers.concat(eventHandler); } public unsubscribe(eventHandler: IEventHandler<TEventArgs>): void { const eventHandlerIndex = this._eventHandlers.indexOf(eventHandler); if (eventHandlerIndex >= 0) this._eventHandlers = this ._eventHandlers .filter((_, index) => index !== eventHandlerIndex); } public dispatch(subject: object, args: TEventArgs): void { this._eventHandlers.forEach(eventHandler => { if (this._eventHandlers.indexOf(eventHandler) >= 0) eventHandler.handle(subject, args); }); } }
We can use a DispatchEvent instance within objects that define events and expose them through the IEvent interface so that other objects cannot raise the event.
class MyClass { private readonly _myEvent: DispatchEvent<any> = new DispatchEvent<any>(); public get myEvent(): IEvent<any> { return this._myEvent; } public myMethod(): void { this._myEvent.dispatch(this, undefined); } } const myObject = new MyClass(); const myEventHandler: IEventHandler<any> = { handle(): void { /* ... */ } }; myObject.myEvent.subscribe(myEventHandler); myObject.myMethod(); myObject.myEvent.unsubscribe(myEventHandler);
ViewModels
The next step is to define our ViewModels as they are a central part in this pattern. The View depends on them thus it needs to know what base abstractions it can use. Mostly, this means a number of core interfaces that the View is aware of. Or better said, a few custom ReactJS Hooks are aware of so that we can easily apply MVVM.
We have already defined the event interfaces. Next, we will define the core interfaces that make our objects observable thus turning them into ViewModels and observable collections.
interface INotifyPropertiesChanged { readonly propertiesChanged: IEvent<readonly string[]>; } interface INotifyCollectionChanged<TItem> { readonly collectionChanged: IEvent<ICollectionChange<TItem>>; } interface ICollectionChange<TItem> { readonly addedItems: readonly TItem[]; readonly removedItems: readonly TItem[]; } interface IReadOnlyObservableCollection<TItem> extends Readonly<TItem[]>, INotifyPropertiesChanged, INotifyCollectionChanged<TItem> { } interface IObservableCollection<TItem> extends IReadOnlyObservableCollection<TItem> { push(...items: readonly TItem[]): number; pop(): TItem | undefined; unshift(...items: readonly TItem[]): number; shift(): TItem | undefined; get(index: number): TItem; set(index: number, item: TItem): void; splice(start: number, deleteCount?: number): TItem[]; clear(): TItem[]; reset(...items: readonly TItem[]): number; }
Any object that implements INotifyPropertiesChanged can be considered a ViewModel. Any object implementing INotifyCollectionChanged can be considered an observable collection that can be read-only or read-write. This is more easily represented through the corresponding IReadOnlyObservableCollection and IObservableCollection interfaces which both implement the read-only array interface. This is to make these collections more array-like in their usage. We can use all the known methods that do not modify the source array with observable collections such as map, forEach, reduce, concat, and so on.
IObservableCollection exposes most methods that an array has that mutate the collection, the only exception being Array.copyWithin. This is a method mainly used to improve performance and might have fewer use cases when it comes to MVVM. Nonetheless, it can be a good exercise to implement this method and raise the collectionChanged event with the right information.
The other four methods that we have added are more or less syntactic sugar, optimizations, or just out of necessity. The clear method will remove all items from the collection and return them in an array, the same way as we would do when we call array.splice(0, array,length), they are the same. The reset method, on the other hand, does two things. It clears the collection of items and sets the provided items as the new contents. And it raises the collectionChanged event only once. This is a small optimization to avoid having the same event raised twice which can lead to two renders of a component.
The last two methods that we have added are the get and set pair which mimic the array indexer. This is not an issue for the get indexer, when we read data from a given index. But it is when we want to set an item at a given index. Unfortunately, we cannot override the indexer operator in JavaScript. We need to notify observers when we update an item in our collection. We do not need to raise any events when we read data. This works nicely with our read-only array interface. The get method exists only to match the set method and to avoid confusion. If there is a set method, there should be a get method to enable our code to be clean. And this is only necessary for a read-write observable collection.
Now that we have our interfaces, we can provide some default implementations, similar to how we did for the IEvent interface. It is likely that we will have a lot of ViewModels in our application and all will need to raise the propertiesChanged event. We can simplify this and provide a base class that exposes a protected method that does exactly that. It notifies observers that the properties we list have been changed so they can do (or not do) something in that regard.
class ViewModel implements INotifyPropertiesChanged { private readonly _propertiesChangedEvent: DispatchEvent<readonly string[]> = new DispatchEvent<readonly string[]>(); public get propertiesChanged(): IEvent<readonly string[]> { return this._propertiesChangedEvent; } protected notifyPropertiesChanged( changedProperty: string, ...otherChangedProperties: readonly string[] ): void { this._propertiesChangedEvent.dispatch( this, [changedProperty, ...otherChangedProperties] ); } }
The notifyPropertiesChanged will always require at least one property name that has changed. But we can specify more if we need to.
Next up is the observable collection. For simplicity, I will add only the code for two methods that we are likely to use the most, push and splice. All other methods can be implemented similarly. This is just to illustrate how each method would be implemented.
class ObservableCollection<TItem> extends Array<TItem> implements IObservableCollection<TItem> { private readonly _propertiesChangedEvent = new DispatchEvent<readonly string[]>(); private readonly _collectionChangedEvent = new DispatchEvent<ICollectionChange<TItem>>(); public constructor(...items: readonly TItem[]) { super(); super.push(...items); } public get propertiesChanged(): IEvent<readonly string[]> { return this._propertiesChangedEvent; } public get collectionChanged(): IEvent<ICollectionChange<TItem>> { return this._collectionChangedEvent; } public push = (...items: readonly TItem[]): number => { const addedItems = new Array(this.length).concat(items); const result = super.push(...items); this._collectionChangedEvent.dispatch( this, { addedItems, removedItems: [] } ); this._propertiesChangedEvent.dispatch(this, ['length']); return result; } public splice = (start: number, deleteCount?: number): TItem[] => { if (this.length > 0) { const removedItems = super.splice(start, deleteCount); this._collectionChangedEvent.dispatch( this, { addedItems: [], removedItems: new Array(start).concat(removedItems) } ); this._propertiesChangedEvent.dispatch(this, ['length']); return removedItems; } else return []; } // ... }
We use a nice little feature that JavaScript gives us. We can offset arrays by specifying the length of the array when we create it and then concatenating the actual contents. When we iterate with forEach, or any other method, we will be able to get both the item and the index of that item which will include the offset. We can have non zero-based arrays in JavaScript.
var array = new Array(5).concat(["1", "2", "3"]); array.forEach(function (item, index) { console.log("item:", item, "index:", index); }); // item 1, index 5 // item 2, index 6 // item 3, index 7
This works on Internet Explorer 11 as well.
Aside from that, our observable collection extends the standard JavaScript array. This will provide us with most of the methods that our observable collection exposes as most have to do with reading the array (forEach, map, reduce and so on) as well as with the indexer operators. The only issue here is that if we instantiate an ObservableCollection, we get both the set indexer and the copyWithin methods through inheritance. A simple way to walk around this is to wrap the initialization in a function and export it instead of exporting the class. The definition will be hidden, but the initialization will be available, similar to a factory method.
export function observableCollection<TItem>( ...items: readonly TItem[] ): IObservableCollection<TItem> { return new ObservableCollection<TItem>(...items); }
This will conclude all of the ViewModel core interfaces and implementations. When we want to define a ViewModel or use an observable collection, we only need to inherit from the base class (or provide our own custom implementation) or call the observableCollection function to initialize one. We can have the same approach with collections as we do with events, store them in read-write member variables, and expose them as read-only. This will communicate to users that they should not call any methods that mutate the collection and only read and subscribe to change events. The object exposing the collection is the only one that should be making changes to the collection.
Views
It is time to move to ReactJS components. First, we will go through a simple yet very common scenario. Our ViewModel will call the https://dinoipsum.herokuapp.com API that will generate lorem ipsum text. While the request is being processed the ViewModel will be marked as busy, once we get the result we set the paragraphs and reset the flag.
class LoremIpsumViewModel extends ViewModel { private _isBusy: boolean = false; private _paragraphs: readonly string[] = []; public get isBusy(): boolean { return this._isBusy; } public get paragraphs(): readonly string[] { return this._paragraphs; } public loadAsync(): Promise<void> { this._isBusy = true; this.notifyPropertiesChanged("isBusy"); return fetch("https://dinoipsum.herokuapp.com" + "/api/?format=json¶graphs=3") .then(response => response.text()) .then(JSON.parse) .then(paragraphs => paragraphs.map( (words: readonly string[]) => words.join(" ") )) .then(paragraphs => { this._paragraphs = paragraphs; this._isBusy = false; this.notifyPropertiesChanged("paragraphs", "isBusy"); }) .catch(console.error) } }
The component is rather simple. It shows a text when the ViewModel is busy. Otherwise, it will show the paragraphs. And at the top, we will have a button that loads data.
function LoremIpsumComponent(): JSX.Element { const viewModelRef = useRef<LoremIpsumViewModel | null>(null); if (viewModelRef.current === null) viewModelRef.current = new LoremIpsumViewModel(); const { current: viewModel } = viewModelRef; const loadCallback = useCallback(() => viewModel.loadAsync(), [viewModel]); return ( <> <button onClick={loadCallback}>Click me for some awesome text</button> { viewModel.isBusy ? "Getting some really awesome text" : viewModel.paragraphs.map( (paragraph, index) => <p key={index}>{paragraph}</p> ) } </> ); }
We can spot a few things we may want to improve from the get-go. For instance, the instantiation of the ViewModel looks like something we will be doing a lot if we use MVVM in our application. It would be useful to have a custom ReactJS Hook that reduces some of the boilerplate code.
A second thing we may want to improve is the content itself. It looks a bit ugly and may be difficult to follow. This is a simple example. There may be more complex scenarios where rendering each item is not as straightforward. It would be useful to have a component that either renders the child components or displays a “busy” message depending on a flag.
function useViewModel<TViewModel>( ViewModelType: { new(): TViewModel } ): TViewModel { const viewModelRef = useRef<TViewModel | null>(null); if (viewModelRef.current === null) viewModelRef.current = new ViewModelType(); return viewModelRef.current; } interface IBusyComponentProps { readonly isBusy: boolean; } function BusyComponent( { isBusy, children }: PropsWithChildren<IBusyComponentProps> ): JSX.Element { if (isBusy) return <>Processing...</>; else return <>{children}</>; }
This should make things easier for our main component.
function LoremIpsumComponent(): JSX.Element { const viewModel = useViewModel(LoremIpsumViewModel); const loadCallback = useCallback(() => viewModel.loadAsync(), [viewModel]); return ( <> <button onClick={loadCallback}>Click me for some awesome text</button> <BusyComponent isBusy={viewModel.isBusy}> { viewModel.paragraphs.map( (paragraph, index) => <p key={index}>{paragraph}</p> ) } </BusyComponent> </> ); }
Ok, that’s better. We could probably make improvements to the BusyComponent. Since when the isBusy flag is set to true, the children do not get displayed and in most cases when the flag changes so will the content meaning that we create a subtree of paragraph elements that will never be displayed. We can optimize this by passing a callback through the children prop and invoke that when the flag is set to false. For now, we will keep it like this for simplicity.
This is all great, with one exception. When we run the above code, it does not work. Regardless of how many times we click the load button, nothing changes. This is because we did not do any form of event subscription. The ViewModel may be telling subscribers that something has changed, but there is nobody listening to these changes.
Before we continue, a word of warning about event subscription. While they are great and event-driven programming is extremely useful for user interfaces, we need to be aware of potential memory leaks we may get if we do not unsubscribe from events that our components subscribed to. The subject maintains a reference to each observer (in this case, a ReactJS component) in order to be able to notify it of changes. If we do not unsubscribe from an event when the component is unmounted, the subject (in our case, the ViewModel) will maintain that reference thus blocking the JavaScript engine from reclaiming memory. ReactJS will display a warning message in the console when we try to make changes to unmounted components. This is a good way to spot when we forget to clean up after ourselves.
With that in mind, we will proceed with our event subscription. Whenever the ViewModel changes we want to trigger a re-render of the component so that it picks up the changes. A way to do this is through setState.
LoremIpsumComponent(): JSX.Element { const [_, setState] = useState<any>(); const viewModel = useViewModel(LoremIpsumViewModel); useEffect( () => { const eventHandler: IEventHandler<readonly string[]> = { handle(): void { setState({}); } }; viewModel.propertiesChanged.subscribe(eventHandler); return () => viewModel.propertiesChanged.unsubscribe(eventHandler); }, [viewModel] ); const loadCallback = useCallback(() => viewModel.loadAsync(), [viewModel]); return ( <> <button onClick={loadCallback}>Click me for some awesome text</button> <BusyComponent isBusy={viewModel.isBusy}> { viewModel.paragraphs.map( (paragraph, index) => <p key={index}>{paragraph}</p> ) } </BusyComponent> </>
This is a bit cheeky what we have here. Whenever the ViewModel notifies that properties may have changed, we set the state to a new empty object effectively re-rendering the component whenever this happens. We can improve this by storing the view model properties as we get notified about them and only call setState when there is an actual change. This optimization will not only reduce the number of re-renders. But it will exclude the possibility of infinite loops in case we always call a ViewModel method when the component renders which in turn notifies that a property has changed which in turn may trigger a re-render, and so on. This optimization is included in the react-model-view-viewmodel library.
If we run the code now, we can see that the user interface updates and we are getting our lorem ipsum paragraphs. Same as before, the event subscription can be extracted into a custom ReactJS Hook to simplify this procedure. Not to mention that it will make it more difficult for us to forget to unsubscribe from a ViewModel. Automated clean-up.
function watchViewModel(viewModel: INotifyPropertiesChanged): void { const [_, setState] = useState<any>(); useEffect( () => { const eventHandler: IEventHandler<readonly string[]> = { handle(): void { setState({}); } }; viewModel.propertiesChanged.subscribe(eventHandler); return () => viewModel.propertiesChanged.unsubscribe(eventHandler); }, [viewModel] ); } function LoremIpsumComponent(): JSX.Element { const viewModel = useViewModel(LoremIpsumViewModel); watchViewModel(viewModel); const loadCallback = useCallback(() => viewModel.loadAsync(), [viewModel]); return ( <> <button onClick={loadCallback}>Click me for some awesome text</button> <BusyComponent isBusy={viewModel.isBusy}> { viewModel.paragraphs.map( (paragraph, index) => <p key={index}>{paragraph}</p> ) } </BusyComponent> </> ); }
We could integrate the code from watchViewModel into useViewModel. But we would like to have these as a separate ReactJS Hooks. In some cases a component may receive a ViewModel through its props, there no instantiation, only event subscription. On the other hand, the main component may never be interested in ViewModel changes because all it does is pass down the ViewModel to other components. An AlertsViewModel is a good example of this. The instantiation is done by the root component so that there is only one instance available throughout the application. We can use React Context to pass it around leaving any component that wants to display alerts responsible for event subscription. The root component is not interested if the AlertsViewModel changed or not, it is responsible with providing a single instance per application. This use case is very similar to Flux Stores.
We now have the basic flow covered. Extending our ViewModels from here should be easy as the above code provides one of the core pattern of most applications which is displaying data from an API. The other core pattern is form processing. This is the part where most JavaScript is run because have validation on these inputs.
Forms
When we are dealing with user input we almost always need to validate it. We need to ensure that what the user has written is valid. If not, we need to provide useful information as to why they are providing the incorrect data. Generally, we want this as a user fills a form and not only when they click (or tap) the save button. It enhances the user experience and provides quicker feedback while the user is still on the same field.
We will be covering form definition, user input, validation, and form submission for the DinoIpsum API. We will have options for generating lorem ipsum text.
Generally, we want our form definitions to be exposed through ViewModels, and not have fields tied to which components are mounted. This will give us flexibility at the View layer. If we have a really large form, not all input components need to be mounted. The state of the form is maintained outside of them and when we do need to display them we already have all the data stored in the ViewModel. For this, we will need a FormFieldViewModel that stores our input data. This time we will use two-way binding, or in other words, the value property will be both readable and writable. When the user types something in the input, we will set the value, and when we render the input we will read the value.
class FormFieldViewModel<TValue> extends ViewModel { private _value: TValue; public constructor(name: string, initialValue: TValue) { super(); this.name = name; this._value = initialValue; } public readonly name: string; public get value(): TValue { return this._value; } public set value(value: TValue) { if (this._value !== value) { this._value = value; this.notifyPropertiesChanged("value"); } } }
This is the ViewModel part, now for the component. Even though we need to send numbers to the API, we will be using text inputs for simplicity. The user will be able to write anything, but we will submit the form only when we have numbers.
interface ITextInputProps { readonly field: FormFieldViewModel<string>; } function TextInput({ field }: ITextInputProps): JSX.Element { watchViewModel(field); const fieldId = field.name.replace(/\s/g, "-"); const onChangeCallback: ChangeEventHandler<HTMLInputElement> = useCallback( event => field.value = event.target.value, [field] ); return ( <div> <label htmlFor={fieldId}>{field.name}: </label> <input type="text" id={fieldId} name={field.name} value={field.value} onChange={onChangeCallback} /> </div> ); }
Next, we will define our FormViewModel, for each form we would have a custom ViewModel that exposes the fields and registers validation for each. Essentially, the form definition. For now we will have just the fields without validation, we will add that later.
class FormViewModel { public readonly numberOfParagraphs = new FormFieldViewModel<string>("Number of Paragraphs", ""); public readonly numberOfWordsPerParagraphs = new FormFieldViewModel<string>("Number of Words per Paragraph", ""); }
One final step, we need to add the form as part of the LoremIpsumViewModel and render the input fields through the main component.
class LoremIpsumViewModel extends ViewModel { private _isBusy: boolean = false; private _paragraphs: readonly string[] = []; public readonly form = new FormViewModel(); public get isBusy(): boolean { return this._isBusy; } public get paragraphs(): readonly string[] { return this._paragraphs; } public submitAsync(): Promise<void> { this._isBusy = true; this.notifyPropertiesChanged("isBusy"); return fetch("https://dinoipsum.herokuapp.com" + "/api/?format=json" + "¶graphs=" + this.form.numberOfParagraphs.value + "&words=" + this.form.numberOfWordsPerParagraphs.value) .then(response => response.text()) .then(JSON.parse) .then(paragraphs => paragraphs.map( (words: readonly string[]) => words.join(" ") )) .then(paragraphs => { this._paragraphs = paragraphs; this._isBusy = false; this.notifyPropertiesChanged("paragraphs", "isBusy"); }) .catch(console.error) } } function LoremIpsumComponent(): JSX.Element { const viewModel = useViewModel(LoremIpsumViewModel); watchViewModel(viewModel); const submitCallback = useCallback(() => viewModel.submitAsync(), [viewModel]); return ( <> <TextInput field={viewModel.form.numberOfParagraphs} /> <TextInput field={viewModel.form.numberOfWordsPerParagraphs} /> <button onClick={submitCallback}> Click me for some awesome text </button> <BusyComponent isBusy={viewModel.isBusy}> { viewModel.paragraphs.map( (paragraph, index) => <p key={index}>{paragraph}</p> ) } </BusyComponent> </> ); }
This will get our form working. When we specify the number of paragraphs or words per paragraph that we desire, and submit the form, we will get the paragraphs that we wanted.
If we provide invalid data such as negative numbers or words instead of numbers, we don’t get a result. We will add validation on each field and disable the submit button when there are errors.
Form Validation
In most cases, if not all, validation is configured for each field in particular. Each field has its own set of validation rules. Sometimes they may become so complex that the validity of one field is dependent on the value of another field. You’ll read more on this a few paragraphs later.
Following this idea, we need to be able to configure validation rules on each field. We can tie this to the propertiesChanged event. When the value changes we trigger a validation. For this to work, we need to add validation-related properties on the field itself.
class FormFieldViewModel<TValue> extends ViewModel { private _value: TValue; private _error: string | undefined; public constructor(name: string, initialValue: TValue) { super(); this.name = name; this._value = initialValue; } public readonly name: string; public get value(): TValue { return this._value; } public set value(value: TValue) { if (this._value !== value) { this._value = value; this.notifyPropertiesChanged("value"); } } public get isValid(): boolean { return this._error === undefined; } public get isInvalid(): boolean { return this._error !== undefined; } public get error(): string | undefined { return this._error; } public set error(value: string | undefined) { if (this._error !== value) { this._error = value; this.notifyPropertiesChanged("error", "isValid", "isInvalid"); } } }
Next, we will update the TextInput component, if there is an error we will display it.
function TextInput({ field }: ITextInputProps): JSX.Element { watchViewModel(field); const fieldId = field.name.replace(/\s/g, "-"); const onChangeCallback: ChangeEventHandler<HTMLInputElement> = useCallback( event => field.value = event.target.value, [field] ); return ( <div> <label htmlFor={fieldId}>{field.name}: </label> <input type="text" id={fieldId} name={field.name} value={field.value} onChange={onChangeCallback} /> {field.isInvalid && <div>Error: {field.error}</div>} </div> ); }
Our next step is to define a function that does the event registration and validates our field.
type ValidatorCallback<TValue> = (field: FormFieldViewModel<TValue>) => string | undefined; function registerValidator<TValue>( field: FormFieldViewModel<TValue>, validatorCallback: ValidatorCallback<TValue> ): void { const eventHandler: IEventHandler<readonly string[]> = { handle(_, propertiesChanged): void { if (propertiesChanged.includes("value")) field.error = validatorCallback(field); } }; field.propertiesChanged.subscribe(eventHandler); field.error = validatorCallback(field); }
Finally, we will define our validator and subscribe our fields.
function numberValidator(field: FormFieldViewModel<string>): string | undefined { if (field.value === undefined || field.value === null || field.value === "") return undefined; const number = Number(field.value); if (Number.isNaN(number) || number < 1 || !Number.isInteger(number)) return "Please provide a positive number (integer)."; } class FormViewModel { public constructor() { registerValidator(this.numberOfParagraphs, numberValidator); registerValidator(this.numberOfWordsPerParagraphs, numberValidator); } public readonly numberOfParagraphs = new FormFieldViewModel<string>("Number of Paragraphs", ""); public readonly numberOfWordsPerParagraphs = new FormFieldViewModel<string>("Number of Words per Paragraph", ""); }
We need to do one more thing. To disable the button when our form is invalid. We need to watch each field for changes and determine whether there are any fields that are invalid.
function LoremIpsumComponent(): JSX.Element { const viewModel = useViewModel(LoremIpsumViewModel); watchViewModel(viewModel); watchViewModel(viewModel.form.numberOfParagraphs); watchViewModel(viewModel.form.numberOfWordsPerParagraphs); const submitCallback = useCallback(() => viewModel.submitAsync(), [viewModel]); const isFormValid = viewModel.form.numberOfParagraphs.isInvalid || viewModel.form.numberOfWordsPerParagraphs.isInvalid; return ( <> <TextInput field={viewModel.form.numberOfParagraphs} /> <TextInput field={viewModel.form.numberOfWordsPerParagraphs} /> <button onClick={submitCallback} disabled={isFormValid}> Click me for some awesome text </button> <BusyComponent isBusy={viewModel.isBusy}> { viewModel.paragraphs.map( (paragraph, index) => <p key={index}>{paragraph}</p> ) } </BusyComponent> </> ); }
We can already see that we can refactor some of this and have an explicit form component and have the FormViewModel actually watch each field for changes and provide isValid and isInvalid flags at the form level, maybe even include an error message for generic validation errors, or display an error from the API when we cannot tie it to a specific field.
class FormViewModel extends ViewModel { private _error: string | undefined; private readonly _fields: FormFieldViewModel<any>[]; private readonly _fieldPropertiesChangedEventHandler: IEventHandler<readonly string[]>; public constructor() { super(); this._fields = []; this._fieldPropertiesChangedEventHandler = { handle: (_, changedProperties): void => { if (changedProperties.includes("isValid") || changedProperties.includes("isInvalid")) this.notifyPropertiesChanged("isValid", "isInvalid"); } }; this.numberOfParagraphs = this._registerField("Number of Paragraphs", ""); this.numberOfWordsPerParagraphs = this._registerField("Number of Words per Paragraph", ""); registerValidator(this.numberOfParagraphs, numberValidator); registerValidator(this.numberOfWordsPerParagraphs, numberValidator); } public get isValid(): boolean { return this._error === undefined && this._fields.every(field => field.isValid); } public get isInvalid(): boolean { return this._error !== undefined || this._fields.some(field => field.isInvalid); } public get error(): string | undefined { return this._error; } public set error(value: string | undefined) { if (this._error !== value) { this._error = value; this.notifyPropertiesChanged("error", "isValid", "isInvalid"); } } public readonly numberOfParagraphs; public readonly numberOfWordsPerParagraphs; private _registerField<TValue>( name: string, initialValue: TValue ): FormFieldViewModel<TValue> { const field = new FormFieldViewModel<TValue>(name, initialValue); field.propertiesChanged.subscribe( this._fieldPropertiesChangedEventHandler ); this._fields.push(field); return field; } }
From here, we can observe a base ViewModel for forms coming together as well. Our form is a collection of fields. Whenever the validity of a field changes, so does the validity of the form itself. We can further expand this by providing an unregisterField method that will handle all the event unsubscription. The same can be done for the registerValidator, the function can return a callback that handles all the event unsubscriptions which, in turn, allows us to add and remove validation through custom ReactJS Hooks based on different flags. This falls outside of the scope of this article, but can easily be implemented. Next, we will update our ReactJS components.
interface IFormComponentProps { readonly form: FormViewModel; submit(): void; } function FormComponent({ form, submit }: IFormComponentProps): JSX.Element { watchViewModel(form); return ( <> {form.error && <div>Error: {form.error}</div>} <TextInput field={form.numberOfParagraphs} /> <TextInput field={form.numberOfWordsPerParagraphs} /> <button onClick={submit} disabled={form.isInvalid}> Click me for some awesome text </button> </> ) } function LoremIpsumComponent(): JSX.Element { const viewModel = useViewModel(LoremIpsumViewModel); watchViewModel(viewModel); const submitCallback = useCallback(() => viewModel.submitAsync(), [viewModel]); return ( <> <FormComponent form={viewModel.form} submit={submitCallback} /> <BusyComponent isBusy={viewModel.isBusy}> { viewModel.paragraphs.map( (paragraph, index) => <p key={index}>{paragraph}</p> ) } </BusyComponent> </> ); }
This is coming together nicely, we have our usual flow, we have a simple form that demonstrates most common validation scenarios. The other two validation scenarios are when the validity of a field depends on the value of another field, we will get to this in a moment, and the validation for uniqueness (relative to what is stored in the database).
The latter case requires us to extend the field, or the form ViewModel as the field may go through different states when we are asynchronously validating it. For instance, we may want to display a progress ring while this is happening, or disable the field. We can do this at the field level in which case we need to extend the form itself so it knows that fields are busy thus the form cannot be submitted, or we can do this at the form level in which case we still need to extend the form, only that it might be easier to do so. The former case is more scalable as we can do this per field, while the latter case is less scalable because we need to do similar changes for each field that validates asynchronously every time.
In case we want to check the uniqueness of an item in a collection of items without any API calls, this is more simple as it falls back to the case when the validity of a field is based on the value of another field. Something that will be presented now.
Form Validation: Dependent Fields
There is more than one way of doing this, what we want is to have a method of ensuring that the form is validated when we have such dependencies and we want to do this in a way that is easy to understand and extend. We do not want to write complicated boilerplate code each time we run into such a dependency.
One way to handle this is to validate the entire form, whenever one field changes. This will ensure than any dependencies that exist between fields will be satisfied when we re-validate the entire form. The validation callback receives the entire form, giving us access to each field. We can have common validation callbacks and combine them into one and reuse them in different forms. This works great when we have common sets of fields.
The issue with the above solution is that we re-validate everything. If one field changed then we make the assumption that most other fields are affected and need to be re-validated. The assumption is that most of our fields have dependencies with one another when this is rarely the case. The larger the form the more validation code we run. And we do want our application to provide a lean user experience, we would like to tell the user that the value they entered is invalid as they type in the field meaning that our entire form validation callback is executed on every key press. That can be a lot, especially with large forms. Small, maybe even medium forms will not have a noticeable performance impact unless the hardware is slow, but in that case the user may have other issues than our slow form.
If we look closer at what we are dealing with, we will notice that we have some dependencies between fields, but not all. We want to run the validation just for those fields. What we want to do, and this leads to the second solution, is trigger validation for particular fields when another, specific, field changes. Basically, we want to make our dependency explicit by specifying additional validation triggers. A validation trigger can be any other ViewModel, not just a field actually
The second solution consists of what we have thus far, we want to configure validation callbacks on each field, and in addition to that we want the ability to specify additional triggers for when that validation happens. The triggers will most likely be the fields we depend on. In this case, we have the form structure first, and validation second.
What I mean by this is that we first need to initialize our fields, and then configure validation so that we can use whichever field we need to configure additional validation triggers for particular fields. We can improve code clarity by having both the field initialization and related validation configuration one after the other if the field does not have any dependencies or if all dependent fields have already been initialized. In case we have co-dependent fields, which effectively means showing an error message on both fields when they mutually invalidate themselves, we need to configure validation for them after both have been initialized.
In our example, we will extend the validation for the number of words per paragraphs field. For no reason at all we will restrict the value of this field to be greater than the number of paragraphs. This is just to demonstrate how validation triggers work.
function registerValidator<TValue>( field: FormFieldViewModel<TValue>, validatorCallback: ValidatorCallback<TValue>, validationTriggers?: readonly INotifyPropertiesChanged[]): void { const eventHandler: IEventHandler<readonly string[]> = { handle(_, propertiesChanged): void { if (propertiesChanged.includes("value")) field.error = validatorCallback(field); } }; const validationTriggerEventHandler: IEventHandler<readonly string[]> = { handle(): void { field.error = validatorCallback(field); } }; field.propertiesChanged.subscribe(eventHandler); validationTriggers && validationTriggers.forEach( validationTrigger => validationTrigger .propertiesChanged .subscribe(validationTriggerEventHandler) ); field.error = validatorCallback(field); }
Next we will update the validation for the number of words per paragraphs field.
registerValidator( this.numberOfWordsPerParagraphs, () => numberValidator(this.numberOfWordsPerParagraphs) || ( this.numberOfParagraphs.isValid && this.numberOfWordsPerParagraphs.value && Number(this.numberOfWordsPerParagraphs.value) < Number(this.numberOfParagraphs.value) ? "The number of words per paragraph needs to be greater " + "than the number of paragraphs" : undefined ), [this.numberOfParagraphs] );
These are all the changes we need to do. This solution is more scalable and most likely more performant than the approach where we validate the entire form. That’s because only fields whose validity may have changed are re-validated. And, in most cases fields do not have such dependencies, only a few of them may do.
Conclusions
While this article is intended as an introduction to the Model-View-ViewModel pattern and how it can be applied with ReactJS applications, we have touched some of the core aspects of a Single Page Application. We have seen that we have two main flows, one for loading and displaying data (view flow) and one for loading, editing, and saving data (edit flow).
The code that was presented can be extended with more features to cover other common usages. I have done this with the react-model-view-viewmodel library which is available on npmjs.org. The aim is to provide all the necessary tooling for developing applications using MVVM with ReactJS out of the box, so there is little to no effort in writing the infrastructure to do so. On the project site I have added a tutorial for the famous ToDo List application.
I hope that with this article I have convinced you to give MVVM a try, even if it is just for pet projects before you may consider adopting it in a project at work. This pattern is an alternative to Flux, which is implemented by different libraries such as Redux and MobX. Both patterns are good. They have different approaches when it comes to the User Interface, similar in some aspects, but different in others. Having more concrete tools gives us flexibility for developing applications. And it does not constrain us with just one way of doing things.