Daniel Earwicker Chief Software Architect
FISCAL Technologies Ltd

Immuto - Working with React (An Example)


TYPESCRIPT IMMUTO 2016-09-16

UPDATE - I'm in the move-fast-and-break-things phase so a couple of details in here are already out of date. In particular, properties are now unified with cursors. See the various repos for details.

In Immuto - Strongly Typed Redux Composition I introduced the Immuto library by coyly describing a wish-list of features, as if I hadn't already written the darn thing. Shucks!

What I didn't do was show how to make a working UI in React, using Immuto to define all the actions and the structure of the store. The missing piece is another package:

However, the majority of this document is just plain immuto used in conjunction with react. We won't need immuto-react until the very last step.

Also there is nothing DOM-specific at all. This should all work with React Native, or in server-side rendering, or what-have-you.

Example app

There's a running example here:

The source is here:

Clone it and get it running like this:

git clone https://github.com/danielearwicker/immuto-example
cd immuto-example
npm install
webpack
http-server

You'll need to global install the last two tools from npm also if you don't have them already. When it's up and running you can hit localhost:8080.

In the TypeScript editor of your choice (I've been using alm recently, because AWESOME) one neat thing to look at is exampleData.ts (link). It first glance it looks like a pretty boring copy/paste of a JSON log of actions. Which is what it is. Maybe I just like boring stuff?

But what's really cool is that if you edit the type property of any action, including the nested ones, or change a payload from a string to a number, TypeScript starts moaning. It's type-checking the shizzle out of it. Every action in the log is of type typeof Shop.reduce.actionType. This means that if I restructure the app, changing even the slightest detail of the actions supported by my reducers, TypeScript pipes up and says "You need to update your example data!"

It's a neat demonstration of how the deep the type checking goes in Immuto. It doesn't just rely on you going through a special API. Even if you hand-build your actions, they can (if you wish) be fully validated at compile time. I know I shouldn't be surprised by this, having designed it to work this way from the ground up, but I genuinely did change the type string of one of my actions and TypeScript did at once tell me to fix the example data, making me say "Well, I'll be a monkey's uncle" or similar earthy reflexive sobriquet.

Two-Way Binding? Inconceivable!

I've continued with the book/shelf/shop theme. Let's start with the simplest piece, the Book (slightly simplified):

interface Book {
    readonly title: string;
    readonly price: number;
}

Of course I like to gather related useful stuff for a type in a namespace with the same name:

namespace Book {

    export const title = property("TITLE", (book: Book) => book.title);

    export const price = property("PRICE", (book: Book) => book.price);

    export const empty: Book = { title: "", price: 0, authors: [] };

    export const reduce = reducer<Book>(empty)
        .action(title)
        .action(price);

    export type Cursor = typeof reduce.cursorType;
}

And here's a really minimal component that lets the user edit a book:

const BookEditor = ({ book }: { book: Book.Cursor }) => (
    <div>        
        <TextInput property={Book.title(book)} />                
        <DecimalInput property={Book.price(book)} decimalPlaces={2} />
    </div>
);

This looks a lot like "two-way binding", and so it is, but it's all layered over pure Redux actions that are submitted to a single store. And yet nothing in that definition has been bound to a specific store at this stage. It's a perfectly reusable component.

TextInput and DecimalInput are components that wrap the <input> element. They both take a prop called property, which can be anything conforming to this extremely simple interface:

interface Property<P> {
    readonly state: P;
    (newValue: P): void;
}

That is, a function that can be passed a new value, and the function also has a property state containing the current value. And so a minimal definition of TextInput would be:

function TextInput({ property }: { property: Property<string> }) {
    return <input type="text" value={property.state}
                  onChange={e => property(e.currentTarget.value)} />;
}

As you can see, it wouldn't be that much bother to use <input> directly. But text input fields are pretty commonplace and so why not shrink them down to something neat and remove as much noise as possible from your myriad views in JSX? I plan to add a generalised form of TextInput, CheckBox and so on to immuto-react.

All About Properties

So what is property and how does it work? If you look at how Book.title is used in BookEditor, you'll see that we call it (so it's a function), passing it a Book cursor, and we must get back a Property<string> because we pass that sucker directly into TextInput's property prop.

If you made it through the last episode, you'll have seen the most general way of declaring an action:

export const setTitle = action("SET_TITLE",
    (book: Book, title: string) => amend(book, { title }));

It defines a Redux action creator: a function setTitle that can be called with a string argument "Something" and returns a strongly typed action: { type: "SET_TITLE", payload: "Something" }.

Actions are the only way anything changes in Redux, period. They should be as high-level as possible, and descriptive of the change being made. If we have data that has some invariant that must apply to it, for example:

then we shouldn't expose a way to independently set those values. We should instead expose high-level actions that change both values together, in ways that preserve the invariant, so that it is simply not possible to break it.

But this typically still leaves a lot of situations where we have a primitive value, such as a string, that is independently chosen by the user. It's the "bread and butter" of a lot of data models: bunches of named properties that can be independently modified.

As I've noted before in another context, a "property", an addressable value that can be read and written, is a powerful idea that should be reified, turned into an object that can be stored in a variable, passed as a parameter, etc. The Property interface is that concept in Immuto.

An Immuto cursor implements a variant that idea in which we express "writing" by accepting an action to perform on the value. But for some simple values there is only one action: to set it to a new value carried in the payload. In such cases, requiring you to create and pass that action explicitly is pure ceremony with no purpose.

So in these simple and ubiquitous cases, where it makes sense, instead of action we can use property:

export const title = property("TITLE",
    (book: Book) => book.title,
    (book, title) => amend(book, { title }));

This assigns to title an object that is both:

So when we call that function passing a new title "1984" for the book, it builds an action { type: "TITLE", payload: "1984" } and dispatches it to the Book cursor, which will take it from there, producing an action path and ultimately dispatching it to the big store in the sky.

(Note that I've used the name TITLE rather than SET_TITLE; of course, action names are entirely up to you. I think if there is nothing else you can do to something apart from SET it, then there's no need to qualify it.)

In the call to property, the second argument is the same reducer definition we originally passed to action in the earlier setTitle example. The first argument is the new part: a "getter" for the same property.

But what a shame to have to effectively say the same thing twice. In a simple example like this, we just want to specify a property name. In a future version of TypeScript (see keysof operator) we may be able to do something super-succinct. But for now it seems we're stuck with ugly repetition. Ugly, ugly repetition. Or are we? Or are we?

There is one clever trick we can play, so we only have to pass the getter:

export const title = property("TITLE", (book: Book) => book.title);

If called in this way, the source of the getter function is parsed, and if it matches a simple enough pattern, then the corresponding reducer can be automatically generated! If the getter is too complex then this parsing fails at runtime, which sounds bad. But it's not too bad, because it's going to fail at load time, and fail noisily (throwing an Error). This is the kind of runtime error that is only a gnat's whisker away from static checking. But this feature does involve sniffing something that is not yet completely standard across browsers, so it's somewhat experimental. Please help me fix my Regex.

So, properties give us a binding capability for simple independent values, and that enables two-way binding layered over type-checked pure Redux actions. Happy Birthday!

Rendering a collection of books

Let's now move out to the next layer, a shelf containing multiple books:

export interface Shop {
    readonly name: string;
    readonly shelves: { [name: number]: Shelf };
}

export namespace Shop {

    export const name = property("NAME", (shop: Shop) => shop.name);

    export const shelves = collection("SHELVES", Shelf.reduce,
        numberMapOperations<Shelf>(), (shop: Shop) => shop.shelves);

    export const empty: Shop = { name: "", shelves: {} };

    export const reduce = reducer(empty)
        .action(name)
        .action(shelves);

    export type Cursor = typeof reduce.cursorType;
}

The extra interesting part here is the collection of books. As we've just implemented a component that can render a Book, let's use it to render all the books in our collection:

export const ShelfEditor = ({ shelf }: { shelf: Shelf.Cursor }) => (
    <div>
        {
            getIds(shelf.state.books).map(book =>
                <div key={book}>
                    <BookEditor book={Shelf.books(shelf, book)} />
                </div>
            )
        }
    </div>
);

In fact there's nothing here I haven't previously discussed. Shelf.books is the collection definition, and it's a way to get a Book cursor if you have a Shelf cursor and the key of the book you're interested in.

So these are the basics of how you build components that bind to Immuto models. Note that you are of course free to have other props besides a cursor on a component, and you could even pass multiple cursors in different props.

Binding to the store

The final mystery is how to start the whole thing off at the root. We need a way to tie our tree of components to the store. In this example the root of our model is a Shop. And at last we're going to use something from immuto-react!

Assuming we have a ShopEditor along the expected lines, requiring a prop of type Cursor<Shop>, what we need is an App component that requires no props and can be rendered as-is.

const store = Shop.reduce.store();

const App = bindToStore(store, s => <ShopEditor shop={s} />);

ReactDOM.render(<App />, document.querySelector("#root")!);

Here, bindToStore is a function imported from immuto-react. You pass it a store to bind to, and also a function which will be called back with a cursor of the same type as the store, which you can then pass into your unbound component.

And that's all there is to it, on a logical level. All data is passed down in pure props. Everything in the entire app is immutable except for one variable, hidden inside the implementation of the Redux store. Components are stateless functions.

Optimization by minimal rendering

There is one other matter that might be of concern in large apps. If we proceed as described here, we'll end up re-rendering the entire page every time anything changes, because that's the default behaviour in React; whenever props are passed in to a component, even the same ones, the component's is re-rendered. React has to do this because it doesn't mandate that props are immutable.

To avoid this, immuto-react has another function called optimize. This wraps a stateless component and returns a new component which is more reluctant to render itself. The rules it uses are quite straightforward.

Each time it has the opportunity to re-render, it compares the values of its props to see if they've changed. To ensure this happens properly, it compares them with ==, not with ===. This causes the JS engine to call the valueOf method on each prop; it's a built-in feature of JavaScript. All objects have a valueOf method, although most just inherit the default which returns this. But it can be overridden to return the value that an object represents.

And guess what? Immuto's cursors and properties override valueOf to return their state. This means that if you use optimize, where you have props that are cursors, they will correctly only cause a re-render if the value they refer to has changed, not just because a new cursor has been generated that wraps the same value.

Sadly we're not quite done. A very common pattern in React is to pass functions as props, to be used as callbacks typically when the user clicks something. These will likely be lambdas that get allocated every time, and yet in many uses they have no effect on rendering. They will completely ruin your attempts to optimize. So to handle this, optimize also has to allow you to pass an array of string names for the props you want it to ignore. This is the one concession to dynamic typing so far! If you add/remove properties, you will not get any compiler errors if you forget to update your ignore array. On the other hand, if you do specify a name that is not actually from your props interface, this will not cause a repaint bug. But if you misspell a prop that should be ignored then you'll get unnecessary repaints.

That keysof operator we may see in a future version of TypeScript would work well here; if that is added, we'll be able to ensure that everything on the ignore array is a true prop name.

Next steps

One topic I'd like to cover is polymorphic components. For example, suppose a shelf could contain a mixture of items (books, food), but all having a price. In mutable OO there would be a Product base class with a price property and the other two would derive from it. But here we have UI in stateless React, we have immutable data which needs to be easily persisted, and we have reducers taking actions. This seems like an interesting thing to figure out a nice solution to.

I've had a request already to look into how async should work (given that I've pooh-poohed Redux middleware), and that's definitely worth getting into.

It would probably be good to do a full comparison with regular Redux, re-implementing the Redux tutorial.

Feel free to suggest more.

Time reversible events 2023-04-07
Language Smackdown: Java vs. C# 2023-03-07
Domesday '86 Reloaded (Reloaded) 2021-02-07
The Blob Lottery 2020-09-27
Abstraction is a Thing 2020-03-07
Unfortunate Bifurcations 2019-11-24
Two Cheers for SQL 2019-08-26
Factory Injection in C# 2019-07-02
Hangfire - A Tale of Several Queues 2019-05-24
How Does Auth work? 2018-11-24
From Ember to React, Part 2: Baby, Bathwater, Routing, etc. 2018-03-18
From Ember to React, Part 1: Why Not Ember? 2017-11-07
json-mobx - Like React, but for Data (Part 2) 2017-02-15
Redux in Pieces 2017-01-28
Box 'em! - Property references for TypeScript 2017-01-11
TypeScript - What's up with this? 2017-01-01
MobX - Like React, but for Data 2016-12-28
Eventless - XAML Flavoured 2016-12-24
Immuto - Epilogue 2016-12-20
Immuto - Radical Unification 2016-09-22
Immuto - Working with React (An Example) 2016-09-16
Immuto - Strongly Typed Redux Composition 2016-09-11
TypeScript - What is a class? 2016-09-11
TypeScript and runtime typing - EPISODE II 2016-09-10
TypeScript and runtime typing 2016-09-04
What's good about Redux 2016-07-24
TypeScript multicast functions 2016-03-13
Introducing doop 2016-03-08
TypeScript is not really a superset of JavaScript and that is a Good Thing 2015-07-11
A new kind of managed lvalue pointer 2014-04-27
Using pointer syntax as a shorthand for IEnumerable 2014-04-26
Adding crazily powerful operator overloading to C# 6 2014-04-23
Introducing Carota 2013-11-04
Want to comment on anything? Create an issue!