Recently in my learning process I created and published my most sophisticated React app thus far. Now, bear in mind I did make it all by myself. It’s not that sophisticated. But it is more sophisticated than what I had done before. I call this app “Drop”. You can see the source code here: https://github.com/xerocross/drop-react. You can play with a hosted version of the widget on my portfolio web site here: https://widgetwonk.azurewebsites.net/react/drop. In this post I’ll walk you through some of the stuff I learned along the way.
As of this writing, actually I’m still working on Drop. If you’ve read any of my writing before, you know that I tend to write as I work so its kind of a discovery process. You get to see into the thinking of a somewhat experienced developer as he learns new stuff.
The first real departure from the norm for Drop was that I wanted it to have a backend data store. My other widgets recently have no backend at all. They only store data client side. I decided on a thin backend. When I say “thin” what I mean is that the backend does nothing or almost nothing but persist data. Having various options for this, I decided early on to write a tiny, thin backend in Node using Express. It persists the data to a MongoDB database. The API is REST-like, but I did not bend over backward to make it conform to REST. I used Heroku to host the backend. This may or may not be obvious, but the frontend and backend of a website do not have to be on the same server. More on that in a moment, but my main focus is on the frontend.
Drop is a utility for storing, searching, and retrieving little bits of text data. I don’t know about youse, but I often find myself needing to save some ad-hoc bit of data that I know I’ll need later. I’ve never been any good at collecting that data into a single place where I know I can find it later. Sometimes I send an email to myself. Sometimes I write it on a business card and put it in the drawer by my bed. But anyway that is the kind of data I’m talking about—personal, small, on any subject.
The main idea of Drop is to tag this data using hashtags in the text. You choose what the keywords of the data will be just by making them hashtags. Then, you search among existing data entries by hashtag. If you type “#apple” you get all the entries that have the “#apple” hashtag in their text. A really important detail is how the app handles two more hashtags. In the search bar if you type two hashtags, you get the intersection—you get data entries that have both, not data entries that have one or the other or both.
I don’t claim there is anything particularly clever about this, but I’ve never seen a widget that does exactly this before, so here’s mine.
The idea sounds simple, but implementing this and wiring it up with the backend was rather sophisticated. Or at least it was a little more sophisticated than the typical to-do list example.
I’m a fan of dumb view components that do nothing but receive props and lay them out. That is, the job of a dumb view is nothing but layout details. It does not perform any logic. There are those who would argue that a component should perform its own logic. I am not one of them. If you are a #codenewbie, you might want to bear that in mind. My way is perhaps not an example of what you should do. But it works quite well.
So in writing my Drop app, I wrote quite a view dumb view components. This has a side effect we have to deal with. It pushes all the logic up into one big component. In my case, I have a top-level dumb view. It’s just the top-most dumb view. All it contains as child components are smaller dumb views. Also the top-level dumb view itself does not perform any logic either. It just receives a lot of props: like, all of them. So where does the logic happen?
When I first started designing the architecture of this app, I was not using Redux. So right now we are talking about a no-Redux architecture. I’ll talk more about Redux later.
Using dumb views means the logic all gets pushed up. Handling interactivity has to be done with props. The component that actually handles updateFormText passes that function down as a prop, down, down, down, to the component that actually contains the form element.
In VueJS, by the way, the architecture would be a little different. In Vue, it’s more customary to have child components emit events and have the parents catch and handle those events.
It’s not a great idea to have all the application logic in one giant, unwieldy component, but it’s a place to start and from there refine. Basically that’s where I was. I had a component called something like DropMainLogic. This is where the functionality was. All the view stuff got passed directly into the DropMainDumbView child component below.
For an application of even modest complexity, having all of the application logic in one file is bad for at least a few reasons I can name off the top of my head.
- Long files are hard to work with because when dealing with one aspect of the logic you have to keep scanning your eyes over other, unrelated functions and variables.
- You might be tempted to write unholy interactions between unrelated parts of application logic.
- It’s harder to test the different parts of the application logic in isolation if they are all in one big component.
- Different files, practically speaking, means different programmers can work on them independently without stepping on each other.
That said, I decided on an architecture where the different layers of application logic are encapsulated into React components that are then composed with relevant props passed through.
In my case, I have an application structure layered something like this AppLayer -> LoginLayer -> BackendCommunicationLayer -> MainBusinessLogicLayer -> MainDumbViewLayer. These “layers” are distinct React components, and they are composed. The AppLayer component simply contains the LoginLayer component as a child. The LoginLayer component contains all of the logic related to login and it pipes all the relevant props through to its child component BackendCommunicationLayer.
AppLayer is the one that actually contains application state. The other components interact with state using props.
I found this architecture very conducive to testing. For example, I can test the BackendCommunicationLayer in isolation. It doesn’t even have direct access to the backend. That is something that gets piped in as a prop. Nor does it contain state. It contains only logic. Also, the only way it interacts with state is by calling functions that get passed in as props. So I can feed it certain state props, fire an event, and I can see which prop functions it calls. This makes it possible to test the layer’s logic independently of anything else in the app.
My MainBusinessLogicLayer is the layer that handles the rather simple logic related to what the app actually does: it handles parsing text for hashtags and searching and such. The simplicity of this logic lends itself to being fully described in a single “business logic” layer, but I hope it’s clear at this point that we could just as well have decomposed the business logic into 10 layers of independent processing if the app were sophisticated enough to call for that.
I implemented my app this way and wrote a full battery of unit tests. It worked very well. The only place where I had to do any sort of “mocking” (using Jest mocks) was at the very top application layer where no props get passed in. But since that layer doesn’t perform any logic itself, testing that layer was relatively easy. One only has to test that it’s calling and rendering the correct things and passing them the right data.
In case you are curious about my “thin” backend, I’ll comment briefly on that now. I decided right away that I did not want to do any kind of user registration. I hate doing that. It’s boring. So I decided on a technique similar to Yopmail. Type any username you want and it’s yours. If you want something relatively private, then you should choose a username nobody else would think of. The backend is just a data store. You can add text entries. You can delete them. And you can fetch them. The backend performs no application logic at all.
I want to continue this discussion, but now I want to talk more about the logic of the app itself and how that relates to my decision to switch over to using Redux, continued in the next part.