Most developers will claim that TDD is part of their daily routine. But most of us fail to mention our actual favorite methodology, SDD.
Stackoverflow Driven Development is a very good thing. Sharing knowledge and experience moves everything forward, and we should keep going. But some issues are too large or too important to quick-fix through an upvoted stackoverflow answer. The "best practice" might not be the best for you.
One very common Stackoverflow question for React developers is how to get React/Redux/Re-frame/etc to load data from the server at the correct time (when your view component is about to be rendered). You will usually be advised to use the React lifecycle methods. In re-frame it looks like this:
(defn customer-list [_]
(r/create-class
{:component-did-mount #(dispatch [:fetch-customers])
:reagent-render (fn [customers]
(if (seq customers)
[:ul (map (fn [customer]
[:li (:name customer)]))]
[:div "Loading customers"]))}))
This "best practice" works fairly well, but it has a couple of disadvantages:
Clojure developers know what a smooth development environment means for productivity and quality. It is not optional or nice to have, it is something you gladly pay for up front. But the way you organize your code also affects your workflow in a big way. Figwheel is possible because Clojure encourages you to push the mutable state towards the edge of your system, allowing most of your functions to be pure and thereby easily reloadable.
Most people get that part right. But it gets trickier from there. On the client you might have things like websockets,polling loops or web workers. When figwheel reloads, you need the lifecycle of those to be handled gracefully. It's not very productive to end up with either one dead socket or 20 duplicated ones, forcing you to refresh your browser. You can get a long way using mount, but I don't think it's the right tool for this.
Figwheel shines when it renders the exact same screen as before, only with your code changes updated. No extra duplicated go-loops, no jumping to another tab in your app, no missing values in state. But for this to work you need to set up a lot of non-trivial stuff. It makes it unnecessarily hard to get started making SPAs in Clojurescript. And if you skip this work in the beginning, you build a lot of complexity and pain for the longer run.
The goal of kee-frame is to make it easy to get started quickly with a nice re-frame setup, while providing tools that make it easier to do the right thing in common scenarios. It does so by putting the URL in charge.
What does that mean, putting the URL in charge? It means putting the web back in web app . A friendly web app should:
These are usability features, but they also help you towards a cleaner design for your SPA. If the view decides when to load some data, your view and your model are tightly connected. But if you put the URL in charge, the view and the model are disconnected and the URL controls them both.
Kee-frame doesn't force you to do this, it just provides you with the tools that make the best solutions the easiest ones.
Most of your re-frame code can stay just the way it is within kee-frame. Kee-frame just provides a smoother startup experience. With the code below, you skip the ceremony of setting up routing and rendering:
(kee-frame/start! {:routes ["/" {"" :index
"customers" :customers}]
:initial-db {:loading? false
:logged-in-user nil
:customers nil}
:root-component [main-view]})
What we have now is a routing system that shields you from the dirty details of URLs and navigation. Your app only operates on route data . The rest of kee-frame builds on this core feature, providing the following:
<a href="something">
, kee-frame generates that link for you.Ok, that's not a real acronym. But let's revisit the data loading example. With kee-frame, this is how you do it:
(kee-frame/reg-controller :customers
{:params (fn [{:keys [handler]}]
(when (= handler :customers) true))
:start (fn [ctx _]
[:fetch-customers])})
(defn customer-list [customers]
(if (seq customers)
[:ul (map (fn [customer]
[:li (:name customer)]))]
[:div "Loading customers"]))
The reagent component is very simple now, just a pure function of its parameter. You could easily sprinkle some unit tests on that.
The params
function is invoked every time the route changes. When it returns a non-nil value different from the previous value, the start
function is invoked. If the start
function returns a re-frame event vector, it does a dispatch
.
So, what did we achieve? A few things, but they're significant:
This blog post is a minimal and simplified description of kee-frame. Go to the kee-frame github page for a more in-depth introduction.