How to use re-frame with Biff

In Understanding htmx I discussed some of the trade-offs of building a primarily server-side-rendered app. If you'd like to use Biff but for whatever reason you've decided that you'd prefer to use ClojureScript and React instead of htmx, I've created an example repo that demonstrates how to integrate re-frame with Biff. The steps are split up into separate commits.

Read on for commentary. You'll want to create a new Biff project before you start.

NOTE: This how-to was made with a pre-v1.8.0 version of Biff, so some of the steps are different now. I'll try to update this before too long.

1. Migrate the re-frame template app to Biff

Diff. I generated package.json and package-lock.json by running npm install react react-dom; npm install --save-dev shadow-cljs. Then I generated a separate re-frame project via lein new re-frame reframe-template and mostly just copied and pasted the contents of that into my Biff project.

At this point, you can run bb dev to start the backend, and then in a separate terminal tab run npx shadow-cljs watch app to compile the CLJS code. Open localhost:8080, sign in, and then you should see "Hello from re-frame." (I've structured the app so we still use server-side rendering for the landing page and authentication flows, and then after you've signed in you're taken to the re-frame app.)

In a subsequent step we'll have bb dev start Shadow CLJS for us.

2. Rewrite the Biff example app

Diff. Ta-da... the app is now fully functional, including the websocket-powered chat. I went through the contents of app.clj and re-implemented it in CLJS.

I'm no re-frame expert; I'd studied it a bit several years ago but have never built anything with it. So there may be better ways to do things, but this should at least get you started.

The websocket integration in particular is not very robust. If you're actually planning to build something with websockets, you'll at least want to add reconnect logic, and maybe use Sente.

Although Biff provides an /auth/signout endpoint by default, that one redirects to the landing page afterward, which causes the browser to do an additional, unnecessary GET / when you sign out. So I added a different signout endpoint that just returns an empty response.

3. Copy over the implementations for bb dev/deploy/soft-deploy

Diff. We'll need to modify bb dev, bb deploy and bb soft-deploy so that they also compile the CLJS code. But to do that, we'll first need to copy-and-paste the relevant functions from com.biffweb.tasks into our project so we can modify them.

It feels a bit clunky that we have to copy over almost 100 lines of code just so we can add some CLJS compilation in three spots. Maybe in the future I'll make the default tasks more configurable so this isn't necessary. For now we'll roll with it.

Don't forget to update bb.edn so that bb uses the tasks in your project instead of the original ones defined in com.biffweb.tasks.

4. Update bb dev/deploy/soft-deploy so they run Shadow CLJS

Diff. Now that we have the task source where we want it, we just need to insert a few shell calls to npx shadow-cljs. The default tasks already have a CSS compilation step (with Tailwind), so we can just look for the spots where CSS gets compiled and then do the same for CLJS.

We'll also need to remove the bb prod-dev command from bb.edn. Normally, that command watches the filesystem and then runs bb soft-deploy whenever you hit save. However, npx shadow-cljs release app is a bit too slow (20 seconds on my machine) for that to be a useful workflow. Instead you'll want to do your development locally with bb dev and then run bb deploy or bb soft-deploy manually. You can still get an nREPL connection to the server with bb prod-repl.

(If you did want to still use bb prod-dev, you would need to copy even more code over to your project so that the command would call our custom soft-deploy function.)

Published by Jacob O'Bryant on 5 Dec 2023

Sign up for Biff: The Newsletter
Announcements, blog posts, et cetera et cetera.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.