Avoid using subscriptions only as app-state getters
Published by Manuel Rivero on 03/01/2019
Subscriptions in re-frame or re-om are query functions that extract data from the
app-state and provide it to view functions in the right format. When we use subscriptions well, they provide a lot of value, because they avoid having to keep derived state the
app-state and they dumb down the views, that end up being simple “data in, screen out” functions.
However, things are not that easy. When you start working with subscriptions, it might happen that you end up using them as mere getters of
app-state. This is a missed opportunity because using subscriptions in this way, we won’t take advantage of all the value they can provide, and we run the risk of leaking pieces of untested logic into our views.
We’ll illustrate this problem with a small real example written with re-om subscriptions (in an app using re-frame subscriptions the problem would look similar). Have a look at this piece of view code in which some details have been elided for brevity sake:
this code is using subscriptions written in the
The misuse of subscriptions we’d like to show appears on the following piece of the view:
Notice how the only thing that we need to render this piece of view is
next-generation. To compute its value the code is using several subscriptions to get some values from the
app-state and binding them to local vars (
date-modes). Those values are then fed to a couple of private functions also defined in the view (
delay->interval) to obtain the value of
This is a bad use of subscriptions. Remember subscriptions are query functions on
app-state that, used well, help to make views as dumb (with no logic) as possible. If you push as much logic as possible into subscriptions, you might achieve views that are so dumb you nearly don’t need to test, and decide to limit your unit tests to do only subcutaneous testing of your SPA.
Refactoring: placing the logic in the right place.
We can refactor the code shown above to remove all the leaked logic from the view by writing only one subscription called
next-generation which will produce the only information that the view needs. As a result both
delay->interval functions will get pushed into the logic behind the new
next-generation subscription and dissappear from the view.
This is the resulting view code after this refactoring:
and this is the resulting pure logic of the new subscription. Notice that, since
get-next-generation function wasn’t pure, we had to change it a bit to make it pure:
After this refactoring the view is much dumber. The previously leaked (an untested) logic in the view (the
delay->interval functions) has been removed from it. Now that logic can be easyly tested through the new
next-generation subscription. This design is also much better than the previous one because it hides how we obtain the data that the view needs: now both the view and the tests ignore, and so are not coupled, to how the data the view needs is obtained. We might refactor both the
app-state and the logic now in
delay->interval functions without affecting the view. This is another example of how what is more stable than how.
The idea to remember is that subscriptions by themselves don’t make code more testable and maintainable. It’s the way we use subscriptions that produces better code. For that the logic must be in the right place which is not inside the view but behind the subscriptions that provide the data that the view needs. If we keep writing “getter” subscriptions” and placing logic in views, we won’t gain all the advantages the subscriptions concept provides and we’ll write poorly designed views coupled to leaked bunches of (very likely untested) logic.
Many thanks to André Stylianos Ramos and Fran Reyes for giving us great feedback to improve this post and for all the interesting conversations.
Originally published in Manuel Rivero's blog.