In a previous post, we were introduced to the Recall app that was built with Cycle.js. A couple of people were interested to know how to break an app down into its components. Towards this, in this post, let’s take a look at a few ways to make simple changes to the Recall app that make it more modular, in the sense that it works with a lot of components working together, rather than a single app with lots of logic.
Recall — A little context
A sample to anatomize solutions over
It is a simple app, with a square grid of 25 squares. When a new game starts, 9 cells are selected at random, and shown to the user for a few seconds. Afterwards, the question is hidden, and the user has to select the 9 cells correctly, by clicking on them again. Once the user selects 9 cells, we compare them with our question. If he got all of the 9 cells right, he is awarded a score of 1.
This should provide some context when we go through some code.
Moving towards components
What if every small part of your app, was an app in itself?
Cycle.js is a wonderful, powerful, and sensible framework from what I have experienced. Since every Cycle.js app is a pure function, these apps are highly composable. What this means is that an app can live and work from inside another app — and to do that is very natural. Such small apps that work as a part of a larger app are usually referred to as components in Cycle.js.
Identifying the spare parts of our cycle
What are the pieces that make the Recall app work?
Whenever taking up any type of refactoring work, the first task that we have to take up is to identify what to change. In this context, we will identify what pieces actually make up our puzzle, quite literally.
From the short bio about the game and the screenshot, it is easy to identify the individual components we can start to work on:
The score board
The score board is a simple component to take up. It is simple, because it has no complex log. Whenever a score is provided, it can display it. The app’s model (which has the logic) decides the score. Whenever the result of the game is a win, the score has to be incremented by 1. Then it can be passed to the score board component which can display it.
We do not use the DOM driver as a source, the only source is a stream of scores which the score board is responsible for. We use the component’s stream of virtual nodes with the dom property in the sink, and render the final view.
The new game button
Whenever the new game button is pressed, a new puzzle is to be created. But the new game button alone cannot be a component. That would mean no scope for reusability. What if we wanted to change the style or the content? What we can instead have as a component is a generic button component that we can use. We will also need to return a stream of clicks on the button so that other consumers can react to it.
Notice that we needed to use isolate. This is because we want to isolate our dom source to this component. Since we are selecting a tags, we want the isolation, otherwise we would pick up on the click events of all the a tags in the dom. We have also parameterized the content and the selector for customization. If we need more options, we can add them to the ButtonSources later. We also return the stream of click events so that others can subscribe to it.
Using this button component to create the new game button is going to be simple. We just set the streams, give it the dom source, and we can expect the virtual dom from the dom property in its sink, and we can also expect the stream of mouse click events on the button.
The game grid
The previous 2 components needed little to no logic. However, we can see that the grid might have some logic. It has to do a lot of things, after all:
- Whenever a new puzzle is created, should be shown for a few seconds.
- Then the user must be able to select/deselect any of the 25 cells by clicking on them.
- The selected cells should appear in green.
- When 9 cells have been selected, the result should be displayed graphically, with the correct, wrong, and missed cells displayed distinctly.
We can immediately notice that each cell can be modeled as a component in itself — any code we write for a cell is bound to be replicated if we don’t. So let’s take a look at what we can do with a game cell, and then get back to the grid with fresher ideas.
The game cell
Right away, we know that a game cell should emit a stream of click events. It should have an enabled/disabled state. It should also have some ui states, for showing the puzzle, to support showing selected cells, and to show the results. Let’s start with the states.
Now that we know all this, it’s easier to write the cell component for this particular app.
We use isolate to isolate the sources and the sinks every time, implicitly, without the caller having to isolate our sources and sinks for us.
The game grid — revisited
If you haven’t noticed yet, we have slowly moved from view-only components (like the score board that just displays what it’s getting — the score) to actionable components (like the button and the cell, which give us actions to react to). The next move is obviously to something more complex — nested components. That’s right, we will have several cells inside a grid, and we must compose these cells into a logical unit. Let’s get started.
That is a whole lot of code, so we will be going into bits of code in detail — to get the whole idea. There are things which can be done differently, this is just one take.
We can take a look at the sources and sinks first, they determine what the component is responsible for. So I want the game grid to have a source for puzzles, and results of the game — these it uses for rendering the states of the cells. And then we want the current selections in the grid, so that we can use it to calculate results. We could also have a source to say whether the grid is the enabled or disabled state, but we have chosen to avoid it — we could have done that, and it would have been perfectly fine.
The stream of number is nothing but the indices of the cells — both in the sources and the sinks — to denote the indices of the cells in the puzzle, and then those that have been selected. Again, the passing of this information could be done in any number of ways, we just chose to do it this way. Any other way of passing in and out this same information is perfectly fine.
Let’s take a look into how the grid component actually works. It must return a stream of indices of the selected cells. This is how we do it. We use a stream of reducer functions and then apply a filter function to make it a stream instead of a memory stream (because we will want to imitate this stream — but more on that later). We use a proxy stream called cellClickProxy$ which should be a stream of the index of the cell which has been clicked. Right now we have no cell components yet, so we create a proxy stream.
The reduce, has, remove and add are utility functions that we wrote, to share some common utilities. We can see the source here:
Now that we have the logic ready, we should start working on creating and using the cell components, and we should have the sources ready. We already created an array of numbers from 0 to 24 in the constant named grid, and an empty array named nothing. For each element in the grid array, we need to create a cell at that index. Let’s create the cells. But they need inputs, so let’s create the sources/inputs for the cells first:
Every time a new puzzle is created, it is shown for 3 seconds (we just picked that number now) during which the user is not allowed to click on a cell. Also, when the result is displayed, the user is not allowed to click on a cell. This logic we will use to create an enabled$ that we can pass to the cell component’s function.
The state$ for the cell is dependent on the index of the cell. Whenever a new puzzle is created, the state can be selected or normal, depending upon whether clicking on a cell to select/deselect it is enabled. When not enabled, the state of the cell depends upon the result. But the initial state is decided by the puzzle, for 3 seconds. This is encapsulated in the map function written below, for the cells array.
We have created the cells and stored it in a local cells array. But we still need to use the outputs from the cells, namely the virtual dom and the stream of clicks. Let’s do that.
We create the cellClick$ by mapping every click event from a cell to the index of the cell, and by merging the resulting streams from all the cells. We then make the cellClickProxy$ imitate the actual cellClick$ so that the logic we wrote above for the selected$ will work. The only thing that’s left is to combine the doms and create a dom for the grid. This we have done with the dom$. We just return the sinks.
Assembling our spare parts into the cycle
Using the components in our app
Now that we have the spare parts ready, we have to integrate them into our app. Let’s take on that challenge. We will first rewrite our main function to make use of the components:
We are employing the same proxy stream mechanism to wire up the streams. It is to be noted that we only use proxy streams to wire up action streams, and not state streams (mostly because state streams are usually remembered, as memory streams, and memory streams cannot be imitated predictably).
Using this new data, our intent function changes like so:
The new game button is never disabled. Also, we could have used a prevent default driver here, but we haven’t done that. Let’s note that down as an improvement to make.
Our model function will also be relieved of a few responsibilities, like worrying about the selections logic. We now have a simpler state, that contains a puzzle$, result$, and a score$.
The view has to be updated to take into account the new doms from the other components, and also the scoreboard which we saw earlier.
We’ve come a long way, but there is always room for improvement
You might have noticed a few things — and maybe even noted them down. Like, the enabled$ could be passed as a source to the grid component, instead of it using its own logic. Also, maybe we should’ve created a header and footer component with static data. I’m sure you have more suggestions, and many of them should be taken up.
NOTE: The recall app is live and has an associated github repo. Be sure to drop in your suggestions, feedback, and issues at the issues section of the repo. The current snapshot of the code, at the time of writing, can be located here.
What do you think? Could I have done something better? Have I missed something? Please share your thoughts using comments on this post. Also let me know if there are particular things in or about Cycle.js that you would enjoy reading further.