1. 程式人生 > >Writing TodoMVC on Mrr and Ramda

Writing TodoMVC on Mrr and Ramda

Writing TodoMVC on Mrr and Ramda

Mrr is a functional reactive state management library for React. It allows you to write reliable apps with a small amount of code. Using mrr, you describe the logics of your app as a set of cells, calculated with pure functions, so Ramda.js will be a good choice to help us with this.

We will write classic TodoMVC application step by step, learning some new aspects of mrr(such as data flow between components) and Ramda.This article is the second one devoted to Mrr(the first is MRR: Make React Reactive). It also requires basic understanding of functional techniques and Ramda. If you are new to Ramda, I recommend to read this awesome series of articles:

http://randycoulman.com/blog/categories/thinking-in-ramda/

For starters, we can render a simple hardcoded list of todos, and then add counting uncompleted items.

Note that along with todos we added $start to the arguments list of openNumber cell to perform initial calculation when the component is created.

Now, let’s add some Ramda.

We can add filtering items in the same fashion.

The second(optional) argument to mrr’s “$” function is a value passed to cell each time the handler is triggered(by default it’s HTML DOM Event object). We used a trick here: we display only those todos, which completed property is NOT equal to the value of show cell. So that when “*” is selected, all todos will be shown.

In the comments above the Ramda functions I wrote corresponding “simple” JS function to make it easier to understand.

Unfortunately, we cannot move to completely “pointfree” style here. In this example using Ramda doesn’t look very helpful for our app, but it’s only the beginning of the deeper usage of Ramda functions.

Now it’s time to implement adding new items.

We need to handle pressing “Enter” on input. Passing a function as a second argument to “$” allows us to filter and transform values passed to cell. Our function checks what was the actual key: if it’s not Enter, it returns special mrr value — skip. If the formula returns skip value, it prevents any further updates and calculations, like the cell wasn’t changed at all.

Each time newTodoItem is generated, we append it to the list of todos, using Ramda’s append. We clear input field after new todo is added, using Ramda always function. We create new todo item using __. In Ramda it is called empty placeholder.As you know, in Ramda every function is curried. If we pass two arguments to a function of three arguments, it will return another function of (3–2=) one argument. It’s like we “filled” first two arguments, leaving the third one “free”. That’s great, but what if we need to fill the first and third argument and leave the second as “free”? That’s where Ramda’s placeholder helps us.So, the above expression will return the function of one argument. This function, in turn, returns new object with an argument as a “text” property.

We need to make our todos cell more sophisticated as we implement removing completed todos.

Now our todos cell is assembled from two anonymous streams, so we should replace “^” reference with “-todos”. It’s crucial to use passive listening here to avoid infinite loop(whereas “^“ is passive by definition). Calculating todos depending on different cells reminds of Redux reducers, though in a more consise way. We’ll switch to more effective solution for this later.

Checking and unchecking all todos is done in the similar way.

When the user click on “Check/uncheck all” button, the value passed to the toggleCompleted stream is the current value of checkbox: true if checked and false if not.So if all todos are done, toggleCompleted will be true and make all todos uncompleted. If there is at least one completed todo, the button will make all todos completed.

Ramda’s useWith is a bit tricky. It recieves a function and an array of callbacks, each for one corresponding argument. So if you pass two arguments, the first will be passed to the first callback and so on.Then the results of executing all calbacks will be passed to a function, in our case map. E.g., if toggleCompleted is true (we are making all todos completed) the first callback (assoc(‘completed’)) is passed true and returns a function which returns an object with completed field of true. The second callback (identity) simply returns the same array of todos. Then the results (function and array) are passed to map function, which returns new array where all items have completed = true.

Intercomponent communication

We did as much as possible in the root component, now it’s time to create a separate component for todo item, wrapped with mrr.

To allow our components to communicate, we should add some properties, generated by mrr’s connectAs function. After this, we may refer to the child component’s cells from the parent component and vice versa. Notice that we pass “text” and “completed” as simple React props to our child, and also an index “i”.

Each time user clicks on remove button, we put a value on remove cell. This value is the index of current todo.We have remove cell in child component, and we refer to it on parent component as */remove. Here asterisk means “any child”, so we are listening for changes in all todos items. For example, when user clicks “Remove” on the second todo:

removeItem cell in second TodoItem component receives the value of click event object,  — remove cell in second TodoItem component receives the value of 1,  — */remove cell in Todos also becomes 1 — todos cell is being recalculated.

Therefore, mrr allows to exchange data streams between relative components, which makes local state management more flexible and powerful.

To implement editing todo’s name, first we need a cell isEdited which will be responsible for whether todo is edited or not. When user clicks on todo’s name, isEdited becomes true, but when he finishes editing and by default it’s false.

This is a very common pattern, so in mrr we have a macros called “toggle”, which sets the cell value to true when first argument changes, and to false when second argument changes(no matter what are the actual values of startEdit and finishEdit cells).If the new name is blank, we need to remove the todo.

We put a new name of todo user enters to the newText cell. As user finishes editing, we check whether the new name is not empty. If not, we fire the afterEdit.remove stream, otherwise afterEdit.update. This is done with the help of mrr’s split operator, which puts values in different subcells according to the results of calculations for corresponding functions. We also modified our remove cell, as now it should be fired in one more case.If the new name is not empty, we need to lift it up to the root component to commit the changes.We have to transfer the new name and the index of the todo item.

We can create commonly used function for translating arguments to array using Ramda:

In root component we again modify our todos cell:

The same we do for changing completed status of the todo item:

Reactive collections

Now our TodoMVC is almost done. Let’s try to improve our code a bit. First of all, our todos cell calculation is rather hardly readable. We need some abstraction here.

All the operations we do with a collection are of three types: create, update, delete. There is a macros in mrr, which simplifies managing collections, using these operations. It’s called “coll”. It does all the modifications with the collection, the only thing you need is to provide source cells for each three operations with appropriate data.

Adding is the simplest case: each value in create stream will be appended to the list. So, create stream may contain any kind of data, in our case — objects.

If you want to delete some items, you should specify either an index of items you have to delete, or a “selector” — some mask to filter items you want to delete. E.g., { completed: true }, passed to delete cell will remove all completed items.

When performing “update” operation, however, you need two pieces: “changes”(how to change the selected items) and “selector”(which items should be updated — index or mask). Hence “update” stream should accept tuples like [{ changes }, { selector }]. E.g., [{ completed: true }, {}] will set “completed” field to true in all items.

This code looks much clearer, still we see some useless repetition in */updateCompleted and */updateName. Let’s create a single stream of updates in TodoItem component.

Last thing we need to add is persisting to localStorage

Of course, we should avoid making side effects in mrr’s functions as much as possible, however in some cases it’s inevitable.

Full example is available on JsFiddle.

Going global

Now our implementation of TodoMVC is finished. The logics is contained in the components itself. But what if we need our todos to be available globally? Imagine that there are some other pages that use our todos. We can store them on the application level.

Along with mrr’s grids, which are bound to components, you can create a global grid and connect it with other grids. In general, global grid can refer to cells in all other grids like the parent grid to it’s child, and vice versa.

To create global grid, we use createMrrApp function. It allows to describe global grid, custom macros and even data types(will be the topic for future articles).

Global grid has the same syntax as components’ grids, except for it doesn’t have render function.

Now in every component of our app we should import withMrr(and others) from mrrApp.js file, so that all our components will be connected to global grid.

We moved all the todos manipulation logics to the global grid. Now let’s look how our Todo components will look like.

To make cells available in the global grid, we should map them in special $expose field. Here we give them aliases for better naming on the global level. E.g. we expose remove cell of TodoItem as removeTodoItems and refer to it in global grid.On the other hand, we don’t need additional efforts to listen to some global grid’s cell in local grid. It’s because global grid cells are implied to have consistent names and be accessible from any part of the app. We can refer to them as “^/%cellname%”.Notice that names in “Todos” component are very similar to how we expose them as. So we can simplify this:

Now we just enumerate the cells we want to be available globally without mapping their names. Using camel case for global cells is just an option, you may use LONG_SNAKE_CASE_NAMES if you are used to.

Full example on JsFiddle.

Conclusions

Mrr really makes functional programming with React easy and fun. Moreover, one can write robust functional code even without deep knowledge of FP! You only need to follow simple rules, like not making side-effect in formulas, if possible.

Mrr allows you to write clean code, offering only minimal set of abstractions(cells, cell types, active and passive listening) and small API(a few macros). Our complete TodoMVC has only 35 LOC(if not including JSX templates). This is much less than most(any?) of other solutions, like Redux or MobX for React, Vue etc. And what’s more important, our code is readable and structured!

This result is achieved with the help of Ramda — an awesome FP swiss knife. It’s suitable for most of data transformations you might need in your app. However in some cases using full Ramda power might be an overkill. In our small app Ramda fits perfectly for simple formulae and formulae of one argument, while being rather unweildy in case of two or more arguments.

Mrr allows you two approaches: in-component logics(aka local state management) and global state management. Local state management is easier in development(everything in one file). It also helpful if you want to create reusable autonomous widgets(containing both logics and representation). You can combine these two approaches in your own proportion(I prefer to handle most of the logics on the local level, storing globally as less as possible).

The data flow between components and the global grid is simple and reliable. Unlike the Flux approach, there is a single interface for data flow in both directions: from local components to the global grid and vice versa. However, there are some restrictions to avoid fragility.

Mrr has declarative and very effective solution for managing collections — “coll” macros. It uses a kind of internal “query language”, which allows to perform mutations(create, update, delete) in a declarative manner(e.g. pouring [{completed: true}, {}] value to “update” cell to make all todos completed).

Mrr is young and ambitious library, if you like this article please star mrr repo at: https://github.com/mikolalex/mrr

In the following articles we will consider creating different kinds of forms with vanilla mrr and mrr form components.

P.S. There is even shorter implementation of TodoMVC with mrr. It uses “monolithic” approach — todo items are rendered by the main component, so there is no need in intercomponent communication. It has 38 LOC(including hash routing).