Use your Cerebral — from imperative to declarative

Damian Płaza
9 min readDec 21, 2018

--

Photo by Ali Yahya on Unsplash

In previous post I shared my understanding of Cerebral and I did a small review of basic terminology used while working with this library. I deeply believe that language we’re using in problem solving process strongly influences our way of thinking and perception. In this post I’ll try to express expressiveness of Cerebral.

Previously, I mentioned that in Cerebral, actions are main building blocks when implementing business logic. We will do tiny exercise for our brains (or cerebrals, hehe) to see how Cerebral helps building apps declaratively. You will see some code so bear with me.

Small note, I will not show any UI, because it will only bring noise — we want to focus on business logic. So please, use your imagination and experience with similar workflows to create mental sketch for UI of such application :-)

Defining goal

Suppose we were asked to prepare login page that would lead to main page — displaying some crucial data for the end user, for instance list of items (sounds familiar, isnt’ it?).

We need to use custom authentication & authorization library to identify user and get access roles. After successful workflow, we need to get mentioned data from company’s api. Seems easy, right?

Action!

Let us start with defining our action.

I told you — actions are functions with context object automatically passed by Cerebral. Here you can see that we’re extracting three things from context — store, get and props.

Store enables interaction with your application state tree — it focuses on performing mutations. For the rest of the exercise, you need to know that it has method called set which — under specific path — wait for it, sets passed value.

Get is a function that focuses only on getting values from state by given path. Simple as that.

Props are true arguments that your sequences and actions are fed with. You can relate them to some data coming from your component. In this particular case, we’re waiting for three values: username, password and team. So imagine that there are three text inputs for each property.

You can think about context as an aggregate of multiple arguments, where most of them were preapplied (in functional terminology you can refer to partial application) during Cerebral’s setup. So for this exercise, you can treat store and get as dependencies while props are truly function arguments grouped under “props” object.

Here comes the beast

Don’t complain I didn’t warn you.

Long, cognitively complex, introducing branching, async and other neat things.

To slighlty motivate you, here you can see what we’re aiming for after our refactoring process.

Almost twice times shorter, readable and expressing domain logic quite well. I bet that during reading initial version, after line 20, you forgot why the hell you’re reading this ugly piece of s…code. In contradiction, final form of the code pleases not only our eyes, but also gives so much understanding!

We’re using custom auth client which exposes async API using Promises. This forces us to “lift” our rest code to Promise world (one can notice I could use async/await syntax for this exercise, but almost the same can be achieved by extracting then methods’ callbacks to separate methods — we’ll achieve better readability, but still we would need to pass store and get parts).

Synchronous asynchronicity

Sounds like a paradox, isn’t it? Well, maybe. Cerebral makes us winners, again! You can return a Promise from your action and Cerebral will take care for handling it’s asynchronous nature. — FOR FREE. It means you don’t need to install any additional plugins/packages and what’s the biggest advantage — you can reason about the code in a synchronous way. I’m digressing. Sorry. Let’s get back to refactoring!

Can anyone provide some support?

First step would be to eliminate impure client instance creation. We didn’t write any tests yet (<Joke>because we’re using ITDD — Inversed TDD, firstly write code then test it</Joke>), but it makes it hard and/or impossible to do so without any hacky works.

Let’s create new provider that would take care for auth client — we’ll hide implementation details and handle client’s lifecycle, like setting it up and tearing it down (if needed).

We simplified it a bit — there’s no parametrization (i.e. flexibility). But you get it — we created thin layer for providing us auth capabilities. Now our initialize action looks as follows:

Instead of creating new instance of custom auth client, we’re using auth provider which under the hood is our good fellow — auth client.

Provide me some action, please

Now let’s compose two things — let us create actions exposing some auth provider’s functionalities. We’re going to start with authenticate method.

We extracted first two chained methods into separate action. It accepts two values in props — username and password. What’s more, we’re using our new friend — path.

Path introduces branching in your sequence. You can freely name your cases, which pumps domain language into your code.

As you can see, we have two cases: success and failure. Additionally, we can pass an object with some properties and they will be available further in the sequence under respective props (Cerebral merges returned objects into props flowing through sequence).

Refactor time!

Our auth action is ready to go, so let’s refactor our initialize action (we love refactoring, right?):

Chnges, changes, changes — luckily for us, interesting ones. What happened? Why we’re exporting an array? Why after our new, shiny authenticate action an plain object was placed? Great you’re asking! (because you are asking, right?).

As I mentioned, our action enables branching in our logic, so we can handle different scenarios. Let’s repeat — to make path object available inside an action, after this action an object must be sequenced.

What’s more, now we can use our action with its branching skills whenever we want in our application — we can consume success/failure cases depending on our needs, that’s neat! Everything was beautifully encoded in human readable language.

Also we needed to change Payload type, because username and password are handled by authenticate action. New property was added — token. It will be available inside props only in success case, but that’s what we wanted.

Probably you’re wondering what the heck is this set guy and why we’re importing state (?!).

Set is an operator. It allows declaratively set some value under given path in state tree. When you’ll spent some time staring at this line, your mind will start vocalizing it as “set state user authorized as false”. When you will put an scientist hat on top of your head and do some research, you would notice that under the hood it’s a function accepting two arguments and returning action, where store.set method is used — you can recall this method, right?

State is the example of a tag. It’s our pointer to the state tree path where information regarding user authorization is placed. Both tag and operator are good friends, and they like to be close to each other.

If you’re not feeling convinced enough for the syntax — that’s perfectly fine. The good thing is that entire expression is a function — you can assign it to a variable, give some meaningful name, etc. Supposingly, you want to make it more readable so you extract entire line and create a variable “userUnauthorized”. Cool, huh?

The only constant is change

Have you noticed one particular problem with our authenticate action? You need to know/remember that it requires object after. Even if you will recall that in next half of an year, teammate sitting next to you might not know that. But it’s his fault, because he’s not familiar with your code enough, isn’t it?

What if I told you we can make it even MORE readable while gaining some compile time safety points? Sounds tempting?

In this refactor step, we’re exporting function that accepts three arguments: two tags and object with two properties, and returns sequence! Our goal here is to make it reusable and safe enough — reusable in Cerebral’s spirit.

The only new thing here is imported props variable. Guess what — it’s also a tag! It represents props flowing through the sequence. We’re using it to ensure username and password available under props, becuase our action requires that.

Let’s check how our main sequence — with initializeWithAuthorization action — looks like.

Real noticable change is usage of authenticate action. Wait, wait. It’s not action anymore. It is a function, undeniably yes. What is returned? A sequence. Let me act as a teacher in a room full of students (I also represent students here, but suppose you’ll be answering in your head)

— What have we created?

— A factory. — one pupil answered.

— When a function returns an action or a sequence, how do we call that, in Cerebral’s dictionary?

Operator! — students yelled at the teacher.

Yup, we created an operator. Verbose, readability-increasing, type safer and reusable. Using it, we’re clearly communicating our intentions, we’re giving the context with required data. What’s more, whenever we would forget the “failure” case, our good’n’old friend TypeScript will start yelling at us, reminding that we didn’t cover all the cases (or add all required properties strictly speaking). I hope you also have as much fun as I have during our interesting journey :-)

Eat. Sleep. Code. Repeat.

It seems like we can also create another operator — this time for authorize method. We’re good at creating operators so this time I will omit showing implemenation details of that, because it’s almost 1:1 copy of authenticate operator. Here’s our main sequence after refactoring process.

We extracted respective part responsible for authorizing the user from our initial action into its own operator. Now initial action is responsible for fetching data and keeping the state consistent with the progress of getting the data. Namely, it has too much responsiblities. Let’s try to split them so data fetching part will be free from any logic related to consuming results of acquisition.

Refactor time no.2!

To split responsibilities we would need to create one provider and one operator. Our profficiency in doing so only encourages us to jump into the code, right? ;-) Let’s start with provider first. We’ll call him a http provider.

Nothing fancy, isn’t it?

Here comes the operator. I think httpGet is a good name here.

Again, nothing new, nothing scary. Now our initial action would consist only of updating state for indicating if we’re in loading phase, if there was any error or if we got the data. Finally, with all the respect, I can remove the initial action, because everything can be stored in exported sequence.

Home. Sweet home.

We’re almost there. I promise. We’re arriving to the inn, you and your brain would be able to rest. Hang on. Check how our sequence is looking right now.

As you can see, we stored so many information in this little piece of code. We clearly documented our intentions, expressed some business logic, even we created some shareable parts! Not only we finished our task, but also we increased the probability of other to become more productive, yay!

But there’s one small change I would introduce. A cherry on the top of the cake. I would suggest that we could extract entire sequence responsible for getting data — together with updating isLoading path. We would encapsulate some logic and give it meaningful name — what would you say about “getImportantUserData”? Sounds crucial enough? This sequence would be composed from tiny bits and it would be really easy to reason about. Feel free to think about it, how it would look like.

It’s the final countdown.

Phew! This was long, tiring journey, but so interesting. We explored Cerebral’s way of bringing declarativeness to the code. In previous post I explained some terminology, and in this post we really used it in writing code. I’m glad I could serve you during this journey and discover new stuff.

What we’ve learned?

  • actions are functions with context provided by Cerebral
  • providers are our friends for taming effectful code
  • operators are great for reusability, type safety (relatively of course, remember we’re living in JS/TS world, I warned ya)
  • tags are like pointers, they can be passed around as variables
  • asynchronicity is made easy by default — you know how promises are working, correct?
  • Cerebral is awesome!

I hope you enjoyed this post and I also hope that it gave you new way of looking at your code. Even if you’re not using Cerebral, it’s a great opportunity to bend a mind a bit while learning new stuff. See you soon!

--

--