<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="http://www.karimarttila.fi/feed.xml" rel="self" type="application/atom+xml" /><link href="http://www.karimarttila.fi/" rel="alternate" type="text/html" /><updated>2026-04-23T09:47:58+00:00</updated><id>http://www.karimarttila.fi/feed.xml</id><title type="html">Kari Marttila Blog</title><subtitle>Kari Marttila&apos;s Blog (e.g. cloud and Clojure related articles)</subtitle><author><name>Kari Marttila</name></author><entry><title type="html">Using LLM in GenAI Projects - Part 2 (Pipeline)</title><link href="http://www.karimarttila.fi/genai/2026/03/08/using-llm-in-genai-projects.html" rel="alternate" type="text/html" title="Using LLM in GenAI Projects - Part 2 (Pipeline)" /><published>2026-03-08T00:00:00+00:00</published><updated>2026-03-08T00:00:00+00:00</updated><id>http://www.karimarttila.fi/genai/2026/03/08/using-llm-in-genai-projects</id><content type="html" xml:base="http://www.karimarttila.fi/genai/2026/03/08/using-llm-in-genai-projects.html"><![CDATA[<p><img src="/img/2026-03-08-using-llm-in-genai-projects_img_1.png" alt="A generative AI pipeline" /></p>

<p><em>A generative AI pipeline.</em></p>

<h2 id="introduction">Introduction</h2>

<p>In <a href="/genai/2026/03/04/qdrant-vector-database-in-genai-projects.html">Part 1</a>, I described how I prepared and ingested historical comments and ground truth documents into Qdrant.</p>

<p>In this second part, I focus on the LLM stage: how the system classifies unseen comments, applies expert-level reasoning for complex cases, validates quality, and exports results for SME review.</p>

<h2 id="from-ingestion-to-llm-workflow">From Ingestion to LLM Workflow</h2>

<p>After ingestion, the pipeline runs in this order:</p>

<ol>
  <li>Router Agent classifies comments to various categories (easy cases / hard cases). Router Agent handles easy cases.</li>
  <li>Expert Agent handles all hard cases more thoroughly, and also makes a decision draft whether the comment is accepted vs rejected.</li>
  <li>Validation compares generated categories to baseline labels.</li>
  <li>Export converts final JSON output into an Excel file for SME to review the results.</li>
</ol>

<h2 id="router-agent-fast-first-pass">Router Agent: Fast First Pass</h2>

<p>The Router Agent is a lightweight gatekeeper.</p>

<p>It uses an LLM prompt plus few-shot examples from historical comments (in Qdrant vector database) to classify each test comment quickly into practical routing buckets. The goal is speed and stable structure, not deep scientific judgment yet.</p>

<p>In this project, the router uses Claude Sonnet 4.6 (hosted in <a href="https://aws.amazon.com/bedrock">AWS Bedrock</a>) and also writes a short AI reasoning trace for each decision. For easy cases, it can already produce a first draft change suggestion, so the downstream review output is not just a label.</p>

<h2 id="expert-agent-deep-reasoning-for-hard-cases">Expert Agent: Deep Reasoning for Hard Cases</h2>

<p>Hard cases are the most demanding. They require grounding against the ground truth main document text and other references.</p>

<p>For those comments, the Expert Agent retrieves context from Qdrant and decides:</p>

<ul>
  <li>accepted content suggestion</li>
  <li>rejected suggestion with rationale</li>
</ul>

<p>At this stage, the same Sonnet model produces richer outputs for SME review: updated AI reasoning, a draft reply to the commenter, and a concrete draft change proposal. This gives the reviewer both the decision and the model’s justification path.</p>

<h2 id="validation-and-sme-friendly-output">Validation and SME-Friendly Output</h2>

<p>Once results are generated, validation measures how well model predictions match the baseline categories.</p>

<p>After validation, results are exported to Excel as a practical review package for SMEs, not just as a raw data dump.</p>

<p>The export includes three sheets:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">results</code>: row-level model output side by side with baseline labels, including an explicit SAME/DIFFERENT comparison flag.</li>
  <li><code class="language-plaintext highlighter-rouge">summary</code>: automatically calculated category-level metrics (target rates, false positives, false negatives, actual success rates, pass/fail).</li>
  <li><code class="language-plaintext highlighter-rouge">results-explanation</code>: a compact guide explaining each field and how to interpret the metrics.</li>
</ul>

<p>The workbook is also formatted for fast manual review: mismatches are highlighted, key AI output columns are emphasized, and long text is wrapped for readability.</p>

<p>An important implementation detail is a strict structured output: both agent stages produce JSON outputs that are easier to validate and analyze systematically.</p>

<h2 id="practical-lessons-from-part-2">Practical Lessons from Part 2</h2>

<p>A few practical takeaways:</p>

<ul>
  <li>Split responsibilities between agents: fast routing first, deep analysis only where needed.</li>
  <li>Keep outputs strictly structured (JSON).</li>
  <li>Use retrieval context for difficult categories instead of relying on model memory.</li>
  <li>Always include a human-readable export step for domain expert review.</li>
</ul>

<h2 id="special-thanks">Special Thanks</h2>

<p>I want to thank my AI mentor Tuomas Jokela for providing excellent requirement analysis for this project. It was really easy to implement the generative AI pipeline based on the clear and structured requirements provided. This made the development process smooth and efficient, allowing me to focus on building the solution rather than clarifying what needed to be built.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Part 1 made the data retrievable. Part 2 made it actionable.</p>

<p>This two-step approach (ingestion first, then LLM orchestration) has worked well in this POC: it keeps the pipeline understandable, measurable, and easier to improve iteratively.</p>

<p><em>The writer is working at a major international IT corporation building cloud infrastructures and implementing genAI applications on top of those infrastructures.</em></p>

<p>Kari Marttila</p>

<p><sub> Kari Marttila’s Home Page in LinkedIn: <a href="https://www.linkedin.com/in/karimarttila/">https://www.linkedin.com/in/karimarttila/</a> </sub></p>]]></content><author><name>Kari Marttila</name></author><category term="[&quot;genai&quot;]" /><category term="genai" /><category term="ai" /><category term="rag" /><category term="llm" /><category term="aws" /><category term="bedrock" /><category term="python" /><category term="qdrant" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Qdrant Vector Database in GenAI Projects - Part 1 (Ingestion)</title><link href="http://www.karimarttila.fi/genai/2026/03/04/qdrant-vector-database-in-genai-projects.html" rel="alternate" type="text/html" title="Qdrant Vector Database in GenAI Projects - Part 1 (Ingestion)" /><published>2026-03-04T00:00:00+00:00</published><updated>2026-03-04T00:00:00+00:00</updated><id>http://www.karimarttila.fi/genai/2026/03/04/qdrant-vector-database-in-genai-projects</id><content type="html" xml:base="http://www.karimarttila.fi/genai/2026/03/04/qdrant-vector-database-in-genai-projects.html"><![CDATA[<p><img src="/img/2026-03-04-qdrant-vector-database-in-genai-projects_img_1.png" alt="Qdrant Console" /></p>

<p><em>Qdrant Console.</em></p>

<h2 id="introduction">Introduction</h2>

<p>This year, I have been working on a new and very interesting generative AI Proof of Concept (POC).</p>

<p>The purpose of this POC is to validate that we can use <strong>historical data</strong> and <strong>ground truth documents</strong> stored in <a href="https://qdrant.tech/">Qdrant</a> to generate similar structured data for new documents for one of our customer. The customer needs to automate a certain manual comment reviewing process which has previously consumed a lot of subject-matter experts’ work.</p>

<p>This will be a two-part blog post series:</p>

<ol>
  <li><strong>Part 1 (this post):</strong> Ingestion phase - how we prepare data and load it into Qdrant.</li>
  <li><strong>Part 2:</strong> How we use Qdrant collections together with an LLM to generate new data for incoming content.</li>
</ol>

<p>In this first part, I focus on practical setup and ingestion steps that run on a developer machine.</p>

<h2 id="what-is-qdrant-vector-database">What is Qdrant Vector Database?</h2>

<p>A vector database stores data as high-dimensional numeric vectors (embeddings) instead of only plain text fields or relational rows. In practice, text is first converted into vectors by an embedding model, and then those vectors are indexed for fast similarity search. In this POC, I use <a href="https://qdrant.tech/">Qdrant</a> vector database (as a docker container).</p>

<p>In this POC, each comment or document chunk is stored as a vector plus metadata payload (for example: category, source file, section index). This allows the system to retrieve semantically similar historical items and relevant ground truth snippets, even when the wording is different.</p>

<p>Vector databases are commonly used in RAG systems, semantic search, recommendation engines, and duplicate/near-duplicate detection.</p>

<p>For this POC, Qdrant has worked well because it is easy to run locally, has a clear API, and supports metadata filtering together with vector search. The trade-offs are that collection design and chunking strategy require careful tuning, and running everything locally means resource limits can appear when datasets or query volume grow.</p>

<h2 id="source-material">Source Material</h2>

<p>The input data in this POC comes from two types of sources:</p>

<ul>
  <li><strong>Historical feedback data</strong> (for example, comments collected earlier in spreadsheet format).</li>
  <li><strong>Ground truth documents</strong> (authoritative reference documents, later converted to markdown).</li>
</ul>

<p>The historical feedback acts as the “memory” of previous decisions. The ground truth documents act as the “source of truth” for current recommendations and constraints.</p>

<h2 id="comment-processing-poc-overview">Comment Processing POC Overview</h2>

<p>At a high level, the pipeline has two goals:</p>

<ol>
  <li><strong>Comment categorization</strong>: classify incoming comments to predefined categories.</li>
  <li><strong>Automated response/change generation</strong>: suggest structured outputs for those comments.</li>
</ol>

<p>In this phase, we are not yet generating final outputs with the LLM. We are building the retrieval foundation first: high-quality vector collections in Qdrant.</p>

<h2 id="setup">Setup</h2>

<h3 id="create-python-virtual-environment">Create Python Virtual Environment</h3>

<p>I use Python 3.13 in this POC.</p>

<p>In practice, this means creating an isolated Python environment for the project, activating it, upgrading package tooling, and installing the project dependencies from the requirements file.</p>

<p>Main libraries include:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">qdrant-client</code></li>
  <li><code class="language-plaintext highlighter-rouge">boto3</code></li>
  <li><code class="language-plaintext highlighter-rouge">pandas</code></li>
  <li><code class="language-plaintext highlighter-rouge">openpyxl</code></li>
  <li><code class="language-plaintext highlighter-rouge">fastembed</code></li>
</ul>

<h2 id="ingestion">Ingestion</h2>

<h3 id="convert-comments-in-excel-files-to-one-json-file">Convert Comments in Excel Files to One JSON File</h3>

<p>The first ingestion step is to normalize source spreadsheets into one JSON structure.</p>

<p>After conversion, I also run a statistics step to validate the distribution and basic quality of the generated data.</p>

<p>This gives me a simple sanity check before moving to embeddings and vector storage.</p>

<h3 id="convert-pdf-documents-to-markdown">Convert PDF Documents to Markdown</h3>

<p>Ground truth PDFs are converted to markdown for easier chunking and indexing.</p>

<p>Because PDF-to-markdown conversion can be resource intensive, I run that phase on a temporary cloud instance, then copy resulting markdown files back to the local project.</p>

<p>The result is two markdown documents representing the current draft material and a higher-level reference document.</p>

<h3 id="enrich-comments-json-file">Enrich Comments JSON File</h3>

<p>Next, I enrich the comment JSON with AI-assisted categorization.</p>

<p>This step adds fields such as category and AI reasoning that later help both retrieval and evaluation.</p>

<p>When needed, I also export the enriched dataset into a review-friendly format so domain experts can validate and refine the categorization quality.</p>

<h3 id="split-comments-to-historical-and-test-comments">Split Comments to Historical and Test Comments</h3>

<p>To evaluate the pipeline realistically, I split enriched comments into two datasets:</p>

<ul>
  <li><strong>Historical dataset (80%)</strong>: uploaded to Qdrant as prior examples.</li>
  <li><strong>Test dataset (20%)</strong>: used as unseen input for generation and validation.</li>
</ul>

<p>The test subset intentionally excludes generated fields (like final category/change/reasoning), because those are produced by the AI pipeline later.</p>

<h3 id="upload-data-to-qdrant-vector-database">Upload Data to Qdrant Vector Database</h3>

<p>In this step, I start the local Qdrant service and run the upload process from the Python environment.</p>

<p>When the upload finishes, Qdrant contains both historical memory and ground truth collections.</p>

<h2 id="qdrant-collections-overview">Qdrant Collections Overview</h2>

<h3 id="qdrant-collection-architecture">Qdrant Collection Architecture</h3>

<p>The architecture uses separate collections for distinct retrieval purposes:</p>

<ul>
  <li>One collection for <strong>historical examples</strong>.</li>
  <li>Two collections for <strong>ground truth documents</strong>.</li>
</ul>

<p>This separation keeps prompts focused and retrieval behavior explicit.</p>

<h3 id="1-historical-comments">1. Historical Comments</h3>

<p>This collection is the system memory.</p>

<ul>
  <li><strong>Purpose:</strong> Few-shot guidance from semantically similar past comments.</li>
  <li><strong>Typical payload:</strong> Various fields in the original Excel files, and new fields related to this POC.</li>
  <li><strong>Benefit:</strong> Helps keep generated outputs consistent with earlier decisions.</li>
</ul>

<h3 id="2-ground-truths">2. Ground Truths</h3>

<p>These collections store chunked sections from authoritative markdown documents.</p>

<ul>
  <li><strong>Purpose:</strong> Ground generation in the actual source text.</li>
  <li><strong>Typical payload:</strong> Content, source filename, section index, chunk length.</li>
  <li><strong>Benefit:</strong> Reduces hallucinations and makes decisions easier to justify.</li>
</ul>

<h2 id="qdrant-dashboard---quick-start-guide">Qdrant Dashboard - Quick Start Guide</h2>

<h3 id="1-access-the-dashboard">1. Access the Dashboard</h3>

<p>After Qdrant is running locally, I open the dashboard in a browser to verify collection status and payload quality.</p>

<h3 id="2-browsing-collections">2. Browsing Collections</h3>

<ol>
  <li>Open <strong>Collections</strong> from the left menu.</li>
  <li>Verify all expected collections exist.</li>
  <li>Open each collection and check point count and vector config.</li>
</ol>

<h3 id="3-inspecting-points-and-metadata">3. Inspecting Points and Metadata</h3>

<ol>
  <li>Open the <strong>Points</strong> tab.</li>
  <li>Expand a point payload.</li>
  <li>Validate key metadata fields (source, category, section index, etc.).</li>
</ol>

<h3 id="4-running-test-queries-console">4. Running Test Queries (Console)</h3>

<p>In <strong>Console</strong>, I run simple filter-based checks to verify that metadata fields are queryable (for example, retrieving points for one specific category). This is a quick way to confirm that ingestion produced the expected structure for downstream retrieval.</p>

<h2 id="conclusion">Conclusion</h2>

<p>In this first part, I showed how I built the ingestion foundation for a GenAI POC using Qdrant:</p>

<ul>
  <li>normalize source files,</li>
  <li>enrich and split datasets,</li>
  <li>upload historical and ground truth collections,</li>
  <li>validate everything in the Qdrant dashboard.</li>
</ul>

<p>In Part 2, I will show how we use these collections with an LLM to classify new items and generate structured outputs based on historical patterns and ground truth evidence.</p>

<p><em>The writer is working at a major international IT corporation building cloud infrastructures and implementing genAI applications on top of those infrastructures.</em></p>

<p>Kari Marttila</p>

<p><sub> Kari Marttila’s Home Page in LinkedIn: <a href="https://www.linkedin.com/in/karimarttila/">https://www.linkedin.com/in/karimarttila/</a> </sub></p>]]></content><author><name>Kari Marttila</name></author><category term="[&quot;genai&quot;]" /><category term="genai" /><category term="ai" /><category term="rag" /><category term="qdrant" /><category term="aws" /><category term="python" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Calva Multi-REPL</title><link href="http://www.karimarttila.fi/calva/2026/01/04/calva-multi-repl.html" rel="alternate" type="text/html" title="Calva Multi-REPL" /><published>2026-01-04T00:00:00+00:00</published><updated>2026-01-04T00:00:00+00:00</updated><id>http://www.karimarttila.fi/calva/2026/01/04/calva-multi-repl</id><content type="html" xml:base="http://www.karimarttila.fi/calva/2026/01/04/calva-multi-repl.html"><![CDATA[<p><img src="/img/2026-01-04-calva-multi-repl_img_1.png" alt="VSCode / Calva" /></p>

<p><em>VSCode / Calva.</em></p>

<h2 id="introduction">Introduction</h2>

<p>Since version <a href="https://github.com/BetterThanTomorrow/calva/releases/tag/v2.0.543">2.0.543</a> Calva supports arbitrary many Clojure REPLs of whatever kind. I wanted to test this new Multi-REPL Calva functionality in a minimal setup, so I created this minimal Calva multi-repl demo. I did this demo for myself to experiment with the new Calva multi-repl functionality, but if some other developer finds this setup useful, I thought I might write a short blog post about it.</p>

<p>The short demo I explain in this blog post is in my Clojure Github repo in directory <a href="https://github.com/karimarttila/clojure/tree/master/calva-multi-repl">calva-multi-repl</a>.</p>

<h2 id="calva-output-to-terminal">Calva Output to Terminal</h2>

<p>In the following chapters I write that you can read the REPL output in the VSCode terminal. I have this setting in my VSCode <code class="language-plaintext highlighter-rouge">settings.json</code>:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">    </span><span class="nl">"calva.outputDestinations"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">

        </span><span class="nl">"evalResults"</span><span class="p">:</span><span class="w"> </span><span class="s2">"terminal"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"evalOutput"</span><span class="p">:</span><span class="w"> </span><span class="s2">"terminal"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"otherOutput"</span><span class="p">:</span><span class="w"> </span><span class="s2">"terminal"</span><span class="w">
    </span><span class="p">}</span><span class="err">,</span><span class="w">
</span></code></pre></div></div>

<h2 id="profile-kari">Profile kari</h2>

<p>I have various tools in my personal profile <code class="language-plaintext highlighter-rouge">kari</code>, <code class="language-plaintext highlighter-rouge">~/.clojure/deps.edn</code>:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="no">:aliases</span><span class="w"> </span><span class="p">{</span><span class="no">:kari</span><span class="w"> </span><span class="p">{</span><span class="no">:extra-paths</span><span class="w"> </span><span class="p">[</span><span class="s">"scratch"</span><span class="p">]</span><span class="w">
                  </span><span class="no">:extra-deps</span><span class="w"> </span><span class="p">{</span><span class="c1">; NOTE: hashp 0.2.1 sci print bug.</span><span class="w">
                               </span><span class="n">hashp/hashp</span><span class="w"> </span><span class="p">{</span><span class="no">:mvn/version</span><span class="w"> </span><span class="s">"0.2.2"</span><span class="p">}</span><span class="w">
                               </span><span class="n">org.clojars.abhinav/snitch</span><span class="w"> </span><span class="p">{</span><span class="no">:mvn/version</span><span class="w"> </span><span class="s">"0.1.16"</span><span class="p">}</span><span class="w">
                               </span><span class="n">com.gfredericks/debug-repl</span><span class="w"> </span><span class="p">{</span><span class="no">:mvn/version</span><span class="w"> </span><span class="s">"0.0.12"</span><span class="p">}</span><span class="w">
                               </span><span class="n">djblue/portal</span><span class="w"> </span><span class="p">{</span><span class="no">:mvn/version</span><span class="w"> </span><span class="s">"0.62.1"</span><span class="p">}}}</span><span class="w">

           </span><span class="no">:reveal</span><span class="w"> </span><span class="p">{</span><span class="no">:extra-deps</span><span class="w"> </span><span class="p">{</span><span class="n">vlaaad/reveal</span><span class="w"> </span><span class="p">{</span><span class="no">:mvn/version</span><span class="w"> </span><span class="s">"1.3.312"</span><span class="p">}}</span><span class="w">
                    </span><span class="no">:ns-default</span><span class="w"> </span><span class="n">vlaaad.reveal</span><span class="w">
                    </span><span class="no">:exec-fn</span><span class="w"> </span><span class="n">repl</span><span class="p">}</span><span class="w">

           </span><span class="no">:outdated</span><span class="w"> </span><span class="p">{</span><span class="c1">;; Note that it is `:deps`, not `:extra-deps`</span><span class="w">
                      </span><span class="no">:deps</span><span class="w"> </span><span class="p">{</span><span class="n">com.github.liquidz/antq</span><span class="w"> </span><span class="p">{</span><span class="no">:mvn/version</span><span class="w"> </span><span class="s">"2.11.1276"</span><span class="p">}}</span><span class="w">
                      </span><span class="no">:main-opts</span><span class="w"> </span><span class="p">[</span><span class="s">"-m"</span><span class="w"> </span><span class="s">"antq.core"</span><span class="p">]}}}</span><span class="w">
</span></code></pre></div></div>

<p>I use this <code class="language-plaintext highlighter-rouge">kari</code> profile for testing that snitch works both in the backend and frontend code in this example. Remove the profile or use your own personal profile in file <a href="./.user.edn">.user.edn</a>.</p>

<h2 id="backend-clojure-repl">Backend (Clojure) REPL</h2>

<p>Open a terminal in this directory and run command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bb backend-repl
</code></pre></div></div>

<p>It prints something like:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cmd:  [clojure -M:dev:backend:calva-external-repl:kari -i bb-scripts/backendinit.clj -m nrepl.cmdline --middleware "[cider.nrepl/cider-middleware]"]
nREPL server started on port 34423 on host localhost - nrepl://localhost:34423
</code></pre></div></div>

<p>In VSCode/Calva: Command Palette =&gt; <code class="language-plaintext highlighter-rouge">Calva: Connect to Running REPL Server in the Project</code> =&gt; Choose: <code class="language-plaintext highlighter-rouge">deps.edn</code>
=&gt; This should print something like this in the Terminal window:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Latest available nREPL dependency versions found on Clojars:
  nrepl: 1.5.1
  cider-nrepl: 0.58.0
  cider/piggieback: 0.6.1
Please consider sponsoring Calva: https://calva.io/sponsors ♥️
Using host:port localhost:34423 ...
Hooking up nREPL sessions on port 34423...
Connected session: clj, port: 34423
Evaluating code from settings: 'calva.autoEvaluateCode.onConnect.clj'
</code></pre></div></div>

<p><strong>NOTE:</strong> The port should be the same.</p>

<p>Open file <a href="./src/clj/backend/bemain.clj">bemain.clj</a> in VSCode. Evaluate the S-expressions in the rich comment:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">comment</span><span class="w">
  </span><span class="p">(</span><span class="nb">+</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="mi">2</span><span class="p">)</span><span class="w">
  </span><span class="c1">;;=&gt; 3</span><span class="w">
  </span><span class="p">(</span><span class="nb">print</span><span class="w"> </span><span class="s">"Hello from backend"</span><span class="p">)</span><span class="w">
  </span><span class="c1">;;=&gt; nil</span><span class="w">
  </span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>You should see in the VSCode <code class="language-plaintext highlighter-rouge">Terminal</code>:</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; clj  backend.bemain 
(+ 1 2)
3
(print "Hello from backend")
Hello from backend
nil
</code></pre></div></div>

<p>Also try evaluating the snitch part of the file, and then evaluate the let bindings <code class="language-plaintext highlighter-rouge">x</code> and <code class="language-plaintext highlighter-rouge">y</code> =&gt; you should have the values printed in the VSCode terminal window.</p>

<p>You have now verified that your backend Clojure REPL is connected to Calva.</p>

<h2 id="frontend-clojurescript-repl">Frontend (Clojurescript) REPL</h2>

<p>NOTE. Peter Strömberg (the creator of Calva) told me this in the Clojurians slack:</p>

<blockquote>
  <p>Generally it is better to start shadow-cljs watchers from a clojure started REPL, than from a npx shadow-cljs started one. This because using shadow-cljs you will start two separate JVM processes, and dev tooling like Snitch  will not work as well as when there is only one JVM process from which the watchers and cljs repl is started. In Calva jack-in the former happens when using project type deps.edn + shadow-cljs and the latter with the shadow-cljs project type. There is some discussion about this here: https://blog.agical.se/en/posts/shadow-cljs-clojure-cljurescript-calva-nrepl-basics/</p>
</blockquote>

<p>So, we are starting the shadow-cljs watchers from a Clojure started REPL in this demo (see file <a href="./.user.edn">.user.edn</a>).</p>

<p>Open a terminal in this directory and run command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bb frontend-repl
</code></pre></div></div>

<p>It prints something like:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cmd:  [clojure -M:shadow-cljs:frontend:kari -m shadow.cljs.devtools.cli watch app]
...
shadow-cljs - HTTP server available at http://localhost:6080
shadow-cljs - server version: 3.3.4 running at http://localhost:9633
shadow-cljs - nREPL server started on port 36259
shadow-cljs - watching build :app
[:app] Configuring build.
[:app] Compiling ...
[:app] Build completed. (142 files, 0 compiled, 0 warnings, 1.25s)
</code></pre></div></div>

<p>Open browser in <code class="language-plaintext highlighter-rouge">http://localhost:6080</code>, you should see text: <code class="language-plaintext highlighter-rouge">If you see this text, index.html is served!</code></p>

<p>Open browser developer tools panel / Console to see the Console output. You should see the <code class="language-plaintext highlighter-rouge">(js/console.log "Hello, shadow-cljs!")</code> in the <a href="./src/cljs/frontend/femain.cljs">femain.cljs</a>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hello, shadow-cljs!
</code></pre></div></div>

<p>In VSCode/Calva: Command Palette =&gt; <code class="language-plaintext highlighter-rouge">Calva: Connect to Running REPL Server in the Project</code> =&gt; Choose: <code class="language-plaintext highlighter-rouge">shadow-cljs</code> =&gt; <code class="language-plaintext highlighter-rouge">Select which build to connect to</code> =&gt; Choose: <code class="language-plaintext highlighter-rouge">:app</code></p>

<p>You should see something like this in the VSCode <code class="language-plaintext highlighter-rouge">Terminal</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Connecting using "shadow-cljs" project type.
Connecting ...
Reading port file: file:///a/prs/github/clojure/calva-multi-repl/.shadow-cljs/nrepl.port ...
Using host:port localhost:36259 ...
Hooking up nREPL sessions on port 36259...
Connected session: clj:2, port: 36259
Evaluating code from settings: 'calva.autoEvaluateCode.onConnect.clj'
; clj:2  shadow.user 
(when-let [requires (resolve 'clojure.main/repl-requires)] (clojure.core/apply clojure.core/require @requires))
nil
Creating cljs repl session...
Connecting cljs repl: shadow-cljs...
Connected shadow-cljs runtime: 3, Chrome 537.36 [Linux x86_64], host: browser
Connected session: cljs:2, repl: :app
Evaluating code from settings: 'calva.autoEvaluateCode.onConnect.cljs'
; cljs:2  user 
(require '[cljs.repl :refer [apropos dir doc find-doc print-doc pst source]])
nil
shadow-cljs remote notificatuons not supported with shadow-cljs version &lt; 3.2.1
</code></pre></div></div>

<p><strong>NOTE:</strong> The port should be the same.</p>

<p>Open file <a href="./src/cljs/frontend/femain.cljs">femain.clj</a> in VSCode. Evaluate the S-expressions in the rich comment:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">comment</span><span class="w">
  </span><span class="p">(</span><span class="nb">+</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="mi">4</span><span class="p">)</span><span class="w">
  </span><span class="c1">;;=&gt; 7</span><span class="w">
  </span><span class="p">(</span><span class="nb">print</span><span class="w"> </span><span class="s">"Hello from frontend"</span><span class="p">)</span><span class="w">
  </span><span class="c1">;;=&gt; nil</span><span class="w">
  </span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>You should see in the VSCode <code class="language-plaintext highlighter-rouge">Terminal</code>:</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; cljs:2  frontend.femain 
(+ 3 4)
7
(print "Hello from frontend")
Hello from frontend
nil
</code></pre></div></div>

<p>Check in the browser Developer tools panel, that you also see <code class="language-plaintext highlighter-rouge">Hello from frontend</code> in the Console output.</p>

<p>Also try evaluating the snitch part of the file, and then evaluate the let bindings <code class="language-plaintext highlighter-rouge">x</code> and <code class="language-plaintext highlighter-rouge">y</code> =&gt; you should have the values printed in the VSCode terminal window.</p>

<p>You have now verified that your frontend Clojurescript REPL is connected to Calva.</p>

<h2 id="cljc-files">CLJC Files</h2>

<p>Open file <a href="./src/cljc/common/cmain.cljc">cmain.clj</a> in VSCode.</p>

<p>Check that you see <code class="language-plaintext highlighter-rouge">.cljc -&gt; clj</code> in the bottom bar new <code class="language-plaintext highlighter-rouge">REPL</code> icon. This means your cljc file will be evaluated in the backend Clojure REPL.</p>

<p>Evaluate the S-expressions in the rich comment:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">comment</span><span class="w">
  </span><span class="p">(</span><span class="nb">+</span><span class="w"> </span><span class="mi">5</span><span class="w"> </span><span class="mi">6</span><span class="p">)</span><span class="w">
  </span><span class="c1">;;=&gt; 11</span><span class="w">
  </span><span class="p">(</span><span class="nb">print</span><span class="w"> </span><span class="s">"Hello from common"</span><span class="p">)</span><span class="w">
  </span><span class="c1">;;=&gt; nil</span><span class="w">
  </span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>You should see in the VSCode <code class="language-plaintext highlighter-rouge">Terminal</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; clj  common.cmain 
(+ 5 6)
11
(print "Hello from common")
Hello from common
nil
</code></pre></div></div>

<p>Next, click the <code class="language-plaintext highlighter-rouge">.cljc -&gt; clj</code> text: <code class="language-plaintext highlighter-rouge">Click to show REPL sessions menu</code> =&gt; Choose <code class="language-plaintext highlighter-rouge">cljs</code> (frontend) REPL. Now you should see text <code class="language-plaintext highlighter-rouge">cljs</code> and <code class="language-plaintext highlighter-rouge">:app</code> in the bottom bar. This means your cljc file will be evaluated in the frontend Clojurescript REPL.</p>

<p>Evaluate again:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">comment</span><span class="w">
  </span><span class="p">(</span><span class="nb">+</span><span class="w"> </span><span class="mi">5</span><span class="w"> </span><span class="mi">6</span><span class="p">)</span><span class="w">
  </span><span class="c1">;;=&gt; 11</span><span class="w">
  </span><span class="p">(</span><span class="nb">print</span><span class="w"> </span><span class="s">"Hello from common"</span><span class="p">)</span><span class="w">
  </span><span class="c1">;;=&gt; nil</span><span class="w">
  </span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>This time you should see:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; cljs:2  common.cmain 
(+ 5 6)
11
(print "Hello from common")
Hello from common
nil
</code></pre></div></div>

<p>Check in the browser Developer tools panel, that you also see <code class="language-plaintext highlighter-rouge">Hello from common</code> in the Console output.</p>

<p>You have now verified that your cljc common files can be evaluated both in the backend Clojure REPL and in the frontend Clojurescript REPL.</p>

<h2 id="babashka">Babashka</h2>

<p>You can nowadays connect Calva to various REPLs. You can try e.g. Babashka. In Calva command palette: <code class="language-plaintext highlighter-rouge">Start a Project REPL and Connect...</code> and then choose project type: <code class="language-plaintext highlighter-rouge">Babashka</code>. Open the <a href="./bb-scripts/tasks.clj">tasks.clj</a> file and evaluate its namespace, and you can see that the REPL does not complain about requiring babashka specific dependency: <code class="language-plaintext highlighter-rouge">[babashka.process :as process]</code>.</p>

<h2 id="note-from-the-calva-creator">Note from the Calva Creator</h2>

<p>The Calva Creator Peter Strömberg reviewed this README and wanted to emphasize one thing:</p>

<blockquote>
  <p>FYI the demo so far does not need new multi-session Calva. You could configure backend and frontend as one connect sequence (which you don’t even need to configure, the built-in deps.edn + shadow-cljs works just fine), Calva has a command for creating a full stack shadow-cljs project if you’re curious what it would look like. This setup is to prefer over multi-session because then your frontend repl will be spawned from the backend repl. Only one JVM process, and repl dev tools will be happier.</p>
</blockquote>

<p>So, you should remember, that this demo created these two separate REPLs just for experimentation purposes. Follow Calva best practices when you do real Clojure development.</p>

<h2 id="conclusions">Conclusions</h2>

<p>Calva is a superb tool for programming Clojure, and it just keeps on getting better. Nowadays you can connect arbitrary many Clojure REPLs of whatever kind to Calva.</p>

<p><em>The writer is working at a major international IT corporation building cloud infrastructures and implementing applications on top of those infrastructures.</em></p>

<p>Kari Marttila</p>

<p><sub> Kari Marttila’s Home Page in LinkedIn: <a href="https://www.linkedin.com/in/karimarttila/">https://www.linkedin.com/in/karimarttila/</a> </sub></p>]]></content><author><name>Kari Marttila</name></author><category term="[&quot;calva&quot;]" /><category term="calva" /><category term="vscode" /><category term="programming" /><category term="languages" /><category term="clojure" /><category term="clojurescript" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Frontend Development with Clojurescript, Replicant, Nexus and Datascript</title><link href="http://www.karimarttila.fi/clojurescript/2025/12/31/clojurescript-with-replicant-nexus-datascript.html" rel="alternate" type="text/html" title="Frontend Development with Clojurescript, Replicant, Nexus and Datascript" /><published>2025-12-31T00:00:00+00:00</published><updated>2025-12-31T00:00:00+00:00</updated><id>http://www.karimarttila.fi/clojurescript/2025/12/31/clojurescript-with-replicant-nexus-datascript</id><content type="html" xml:base="http://www.karimarttila.fi/clojurescript/2025/12/31/clojurescript-with-replicant-nexus-datascript.html"><![CDATA[<p><img src="/img/2025-12-31-clojurescript-with-replicant-nexus-datascript_img_1.png" alt="Web app implemented using Replicant and Nexus, showing Dataspex tool" /></p>

<p><em>Web app exercise implemented using Replicant, Nexus and Datascript. The right upper panel is showing Dataspex tool with Datascript database datoms.</em></p>

<h2 id="introduction">Introduction</h2>

<p>In my previous blog post <a href="/clojurescript/2025/02/28/clojurescript-with-replicant.html">Frontend Development with Clojurescript and Replicant</a> I wrote about implementing a UI using the <a href="https://github.com/cjohansen/replicant">Replicant</a> UI rendering library. In that previous exercise I built a custom dispatch system and used a Clojure Atom as my application store for storing the state of the frontend application. In this new exercise I use <a href="https://github.com/cjohansen/nexus">Nexus</a> to do the heavy-lifting for creating the dispatch system, and instead of storing the application store in a Clojure Atom, I use <a href="https://github.com/tonsky/datascript">Datascript</a>. In the previous exercise I used the Gadget tool to visualize the data store. In this new exercise I use Christian Johansen’s new <a href="https://github.com/cjohansen/dataspex">Dataspex</a> tool.</p>

<p>The exercise I explain in this blog post is in my Clojure Github repo in directory <a href="https://github.com/karimarttila/clojure/tree/master/webstore-demo/replicant-nexus-datascript-webstore">replicant-nexus-datascript-webstore</a>.</p>

<p>Only the frontend code is new / has been refactored to use Nexus and Datascript. The backend code and tooling is the same as in the previous exercise (just bumped the newest library versions for the backend as well).</p>

<h2 id="reference-material">Reference Material</h2>

<p>I used <a href="https://github.com/cjohansen/replicant-state-datascript.git">replicant-state-datascript repo</a> as an example how to setup Nexus + Datascript. Christian Johansen also provided me good help in the Clojurians Slack - Thanks! Christian also kindly reviewed the frontend source code related how I use Nexus, and the Blog post I wrote.</p>

<p>Links to the libraries:</p>

<ul>
  <li><a href="https://github.com/cjohansen/replicant">Replicant</a></li>
  <li><a href="https://github.com/cjohansen/nexus">Nexus</a></li>
  <li><a href="https://github.com/tonsky/datascript">Datascript</a></li>
  <li><a href="https://github.com/cjohansen/dataspex">Dataspex</a></li>
</ul>

<p>Links to the example projects:</p>

<ul>
  <li><a href="https://github.com/cjohansen/replicant-state-datascript">replicant-state-datascript</a>: An excellent example project which provides a skeleton for using Replicant + Nexus + Datascript. This example is all you need to get going.</li>
</ul>

<h2 id="the-libraries">The Libraries</h2>

<p>A short introduction to the libraries. Read more about the libraries in the links provided above.</p>

<h3 id="replicant">Replicant</h3>

<p><a href="https://github.com/cjohansen/replicant">Replicant</a> is a data-driven rendering library for Clojure(Script). It renders hiccup to strings or DOM nodes.</p>

<p>I like the idea of Replicant. In the frontend world <a href="https://react.dev/">React</a> dominates the scene at the moment, and there are a few React wrappers in the Clojure scene like <a href="https://github.com/reagent-project/reagent">Reagent</a> and <a href="https://uix-cljs.dev/">UIx</a>. I have used Reagent and it is a good library to build React apps using Clojurescript. But when I was introduced to Replicant I realized that I hardly ever use the React ecosystem off-the-shelf components, and therefore I do not need to build the frontend using React. Replicant is a very good light-weight alternative to using React.</p>

<h3 id="nexus">Nexus</h3>

<p>In my previous <a href="https://www.karimarttila.fi/clojurescript/2025/02/28/clojurescript-with-replicant.html">Replicant exercise</a> I used a custom built dispatch mechanism (that I mostly borrowed from others, see the source code for more information). This is just fine. But now that we have <a href="https://github.com/cjohansen/nexus">Nexus</a>, why not let it handle the dispatching mechanism.</p>

<h3 id="datascript">Datascript</h3>

<p><a href="https://github.com/tonsky/datascript">Datascript</a> is an immutable in-memory database and Datalog query engine in Clojure and ClojureScript. Therefore it is a good match to store the frontend application events in Datascript database, and using <a href="https://en.wikipedia.org/wiki/Datalog">Datalog</a> query language to query the application state for generating the UI.</p>

<h3 id="dataspex">Dataspex</h3>

<p>In that previous Replicant exercise I used the <a href="https://github.com/cjohansen/gadget-inspector">Gadget extension</a> to visualize my data in the Chrome developer tools. In this new exercise I used <a href="https://github.com/cjohansen/dataspex">Dataspex</a>. Dataspex is essentially Gadget 2.0 implementation.</p>

<p>Dataspex is a Chrome / Firefox extension that you can use to visualize the data store of your frontend application, i.e. the Datascript database in this exercise. Dataspex is just amazing. You can see all datoms generated by your frontend application in the Dataspex window, and drill down to a specific datom. See the picture in the beginning of this blog post for an example.</p>

<h2 id="frontend-initialization">Frontend Initialization</h2>

<p>I borrowed from the excellent <a href="https://github.com/cjohansen/replicant-state-datascript">replicant-state-datascript</a> repo the solution how to register the <code class="language-plaintext highlighter-rouge">datascript</code> database for watching changes, and trigger <code class="language-plaintext highlighter-rouge">Replicant/render</code>. See <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-nexus-datascript-webstore/src/cljs/frontend/app.cljs">app.cljs</a> - <code class="language-plaintext highlighter-rouge">init!</code> function.</p>

<p>So, in the <code class="language-plaintext highlighter-rouge">add-watch</code> we create a triggering mechanism to watch changes in our Datascript database. And if there are changes, we create a view state (<code class="language-plaintext highlighter-rouge">view-state</code>) which has the application state for the Replicant to figure out what changes we need to make in the UI, and finally ask Replicant to render the UI (<code class="language-plaintext highlighter-rouge">(r/render !el (f-views/view view-state))</code>).</p>

<h2 id="nexus-actions-and-effects">Nexus Actions and Effects</h2>

<p>I mostly borrowed the skeleton for Nexus actions and effects from the excellent <a href="https://github.com/cjohansen/replicant-state-datascript">replicant-state-datascript</a> example application.</p>

<p>Read more about the difference between Nexus Actions and Effects in the <a href="https://github.com/cjohansen/nexus">Nexus documentation</a>. Just briefly the difference is that an effect is something that has a side-effect (e.g. making a http get or post, or changing the application store state), and an action just returns data, i.e. data related to other actions and effects to be executed by the dispatching mechanism.</p>

<p>An effect example:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nf">nxr/register-effect!</span><span class="w"> </span><span class="no">:backend/post</span><span class="w">
                      </span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="p">[</span><span class="n">_</span><span class="w"> </span><span class="n">system</span><span class="w"> </span><span class="n">params</span><span class="p">]</span><span class="w">
                        </span><span class="p">(</span><span class="nb">when</span><span class="w"> </span><span class="n">goog.DEBUG</span><span class="w"> </span><span class="p">(</span><span class="nf">f-util/clog</span><span class="w"> </span><span class="s">"effect :backend/post, params:"</span><span class="w"> </span><span class="n">params</span><span class="p">))</span><span class="w">
                        </span><span class="p">(</span><span class="nf">f-http/post</span><span class="w"> </span><span class="n">system</span><span class="w"> </span><span class="n">params</span><span class="p">)))</span><span class="w">
</span></code></pre></div></div>

<p>Make a http post with the parameters.</p>

<p>An action example:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nf">nxr/register-action!</span><span class="w"> </span><span class="no">:route/new</span><span class="w">
                      </span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="p">[</span><span class="n">_state</span><span class="w"> </span><span class="n">params</span><span class="p">]</span><span class="w">
                        </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">pg</span><span class="w"> </span><span class="p">(</span><span class="no">:pg</span><span class="w"> </span><span class="n">params</span><span class="p">)]</span><span class="w">
                          </span><span class="p">[[</span><span class="no">:action/clear-new-product-form</span><span class="w"> </span><span class="p">{</span><span class="no">:pg</span><span class="w"> </span><span class="n">pg</span><span class="p">}]</span><span class="w">
                           </span><span class="p">[</span><span class="no">:action/navigate</span><span class="w"> </span><span class="p">{</span><span class="no">:navigation</span><span class="w"> </span><span class="p">{</span><span class="no">:page</span><span class="w"> </span><span class="no">:new,</span><span class="w"> </span><span class="no">:pg</span><span class="w"> </span><span class="n">pg</span><span class="p">}}]</span><span class="w">
                           </span><span class="p">])))</span><span class="w">
</span></code></pre></div></div>

<p>Navigated to the new product page: clear the new product form and validation errors (if coming from another page), and register the <code class="language-plaintext highlighter-rouge">:page/navigated</code> and <code class="language-plaintext highlighter-rouge">page/previous</code> (as with all route actions).</p>

<h2 id="nexus-interceptors">Nexus Interceptors</h2>

<p>Nexus provides an easy way to add interceptors for effects and actions.</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">def</span><span class="w"> </span><span class="n">logger</span><span class="w">
  </span><span class="p">{</span><span class="no">:id</span><span class="w"> </span><span class="no">:logger</span><span class="w">

   </span><span class="no">:before-effect</span><span class="w">
   </span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="p">[{</span><span class="no">:keys</span><span class="w"> </span><span class="p">[</span><span class="n">effect</span><span class="p">]</span><span class="w"> </span><span class="no">:as</span><span class="w"> </span><span class="n">ctx</span><span class="p">}]</span><span class="w"> 
     </span><span class="p">(</span><span class="nf">f-util/clog</span><span class="w"> </span><span class="s">"Before effect: "</span><span class="w"> </span><span class="p">(</span><span class="nb">pr-str</span><span class="w"> </span><span class="n">effect</span><span class="p">))</span><span class="w">
     </span><span class="n">ctx</span><span class="p">)</span><span class="w">
</span><span class="c1">; ...</span><span class="w">

</span><span class="p">(</span><span class="nf">nxr/register-interceptor!</span><span class="w"> </span><span class="n">logger</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>I learned this from <a href="https://github.com/cjohansen/nexus?tab=readme-ov-file#example-logging">Nexus Interceptor example</a>.</p>

<h2 id="some-development-tricks-used-in-this-exercise">Some Development Tricks Used in This Exercise</h2>

<p>Here I document some development tricks that are not related to the libraries but more generic Clojure or Calva related development tricks I want to remember in the future.</p>

<h3 id="move-calva-output-to-a-new-window">Move Calva Output to a New Window</h3>

<p>Once you have connected Calva to your REPL, you get the output in the terminal / Calva Output. I like to move this window to another monitor (I have four monitors at my table) to see the Calva output window and VSCode editor at the same time (but in different monitors). Use VSCode command: <code class="language-plaintext highlighter-rouge">Terminal: Move Terminal into New Window</code>.</p>

<h3 id="browser-developer-tool-console-logging">Browser Developer Tool Console Logging</h3>

<p>I have quite a lot Browser Console logging in the code base like this:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nb">when</span><span class="w"> </span><span class="n">goog.DEBUG</span><span class="w"> </span><span class="p">(</span><span class="nf">f-util/clog</span><span class="w"> </span><span class="s">"register-action! :route/new, params:"</span><span class="w"> </span><span class="n">params</span><span class="p">))</span><span class="w">
</span></code></pre></div></div>

<p>The reason is that this is an exercise, I was curious to see what kind of parameters there is in those functions while developing the functions. I left the console logging intentionally there if someone uses this exercise as an example to learn how to use these libraries.</p>

<h3 id="other-development-tricks">Other Development Tricks</h3>

<p>You can read about other development tricks from the previous Replicant exercise - they are mostly the same in this exercise (using Babashka…).</p>

<h2 id="conclusions">Conclusions</h2>

<p>Replicant provides an excellent light alternative to mainstream heavy UI frameworks like React. And now with Nexus and Dataspex developing UI with Replicant is really enjoyable.</p>

<p><em>The writer is working at a major international IT corporation building cloud infrastructures and implementing applications on top of those infrastructures.</em></p>

<p>Kari Marttila</p>

<p><sub> Kari Marttila’s Home Page in LinkedIn: <a href="https://www.linkedin.com/in/karimarttila/">https://www.linkedin.com/in/karimarttila/</a> </sub></p>]]></content><author><name>Kari Marttila</name></author><category term="[&quot;clojurescript&quot;]" /><category term="programming" /><category term="languages" /><category term="clojure" /><category term="clojurescript" /><category term="full-stack" /><category term="web" /><category term="replicant" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">AWS Bedrock Agents First Impressions</title><link href="http://www.karimarttila.fi/aws/2025/11/27/aws-bedrock-agents-first-impressions.html" rel="alternate" type="text/html" title="AWS Bedrock Agents First Impressions" /><published>2025-11-27T00:00:00+00:00</published><updated>2025-11-27T00:00:00+00:00</updated><id>http://www.karimarttila.fi/aws/2025/11/27/aws-bedrock-agents-first-impressions</id><content type="html" xml:base="http://www.karimarttila.fi/aws/2025/11/27/aws-bedrock-agents-first-impressions.html"><![CDATA[<p><img src="/img/2025-11-27-aws-bedrock-agents-first-impressions_img_1.png" alt="A typical AWS Bedrock Agent architecture" /></p>

<p><em>A typical AWS Bedrock Agent architecture.</em></p>

<h2 id="introduction">Introduction</h2>

<p>In my previous Bedrock article <a href="/aws/2025/01/16/aws-bedrock-first-impressions.html">Generative AI with AWS Bedrock - First Impressions</a> I told my first experiences about working with <a href="https://aws.amazon.com/bedrock/">AWS Bedrock service</a> when creating a <a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation">Retrieval Augmented Generation (RAG)</a> chatbot.</p>

<p>This year, I had a chance to work with a new AWS Bedrock related project. We are building infrastructure and applications that use <a href="https://aws.amazon.com/bedrock/agents/">AWS Bedrock Agents</a> to automate certain customer service processes.</p>

<h2 id="what-are-aws-bedrock-agents">What Are AWS Bedrock Agents?</h2>

<p><a href="https://aws.amazon.com/bedrock/agents/">AWS Bedrock Agents</a> are agents in the AWS context, that can accomplish complex tasks using <a href="https://en.wikipedia.org/wiki/Large_language_model">Large Language Models</a>, and then call various AWS services, like <a href="https://aws.amazon.com/lambda/">Lambdas</a>. Agents can utilize <a href="https://aws.amazon.com/bedrock/knowledge-bases/">Bedrock Knowledge Bases</a> and they can remember conversation history and handle follow-up questions.</p>

<p>Instead of just providing information, Bedrock Agents can actually process various things like get the intent from a customer service request, query previous related resolutions and then request changes to company’s enterprise systems related to the customer request.</p>

<p>You don’t have to use Bedrock Agents for everything. You can process the customer intent using just Lambda and a <a href="https://en.wikipedia.org/wiki/Large_language_model">Language Model</a>, and let this Lambda orchestrate the agents. A bit like in the diagram above.</p>

<h2 id="bedrock-agent-components">Bedrock Agent Components</h2>

<p>A typical Bedrock Agent consists of these components:</p>

<ol>
  <li><strong>Foundation Model</strong>: The underlying Large Language model that agent uses for reasoning.</li>
  <li><strong>Instructions</strong>: Guidelines that define the agent’s role and behavior in the processing context.</li>
  <li><strong>Action Groups</strong>: These are APIs and functions the agent can call to perform tasks (like the “Agentic Lambdas in the diagram).</li>
  <li><strong>Knowledge Bases</strong>: Documents and data sources the agent can query for information.</li>
  <li><strong>Guardrails</strong>: These are safety controls to ensure appropriate responses.</li>
</ol>

<h2 id="terraform-code-example">Terraform Code Example</h2>

<p><a href="https://developer.hashicorp.com/terraform">Terraform</a> is my favorite Infrastructure as Code (IaC) tool since I can use it with multiple mainstream public clouds.</p>

<p>This is a simple code example to create an AWS Bedrock Agent resource:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"awscc_bedrock_agent"</span> <span class="s2">"customer_service_agent"</span> <span class="p">{</span>
  <span class="nx">agent_name</span>                  <span class="o">=</span> <span class="s2">"${local.res_prefix}-${local.module_name}-customer-service-agent"</span>
  <span class="nx">agent_resource_role_arn</span>     <span class="o">=</span> <span class="nx">aws_iam_role</span><span class="p">.</span><span class="nx">customer_service_agent</span><span class="p">.</span><span class="nx">arn</span>
  <span class="nx">idle_session_ttl_in_seconds</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">customer_service_agent_idle_session_ttl</span>
  <span class="nx">foundation_model</span>            <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">foundation_model</span>
  <span class="nx">description</span>                 <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">customer_service_agent_description</span>
  <span class="nx">instruction</span>                 <span class="o">=</span> <span class="nx">file</span><span class="p">(</span><span class="s2">"${path.module}/customer_service_agent/${var.customer_service_agent_config_directory}/customer_service_agent_instruction.txt"</span><span class="p">)</span>

  <span class="nx">orchestration_type</span> <span class="o">=</span> <span class="s2">"CUSTOM_ORCHESTRATION"</span>
  <span class="nx">custom_orchestration</span> <span class="o">=</span>  <span class="p">{</span>
    <span class="nx">executor</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nx">lambda</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">terraform_remote_state</span><span class="p">.</span><span class="nx">agentic_lambda</span><span class="p">.</span><span class="nx">outputs</span><span class="p">.</span><span class="nx">customer_service_agent_lambda_function_arn</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="c1"># See: https://github.com/hashicorp/terraform-provider-awscc/issues/2004</span>
  <span class="nx">auto_prepare</span> <span class="o">=</span> <span class="kc">true</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="nx">merge</span><span class="p">(</span><span class="nx">local</span><span class="p">.</span><span class="nx">default_tags</span><span class="p">,</span> <span class="p">{</span>
    <span class="nx">name</span> <span class="o">=</span> <span class="s2">"${local.res_prefix}-${local.module_name}-customer-service-agent"</span>
  <span class="p">})</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I am using here AWS Cloud Control Provider (awscc) since at the time of implementing the agent infrastructure the standard AWS provider did not support all the agent features we needed.</p>

<p>This is just one simple example of creating AWS Bedrock Agent resource using Terraform. Bedrock Agents support various configurations, like <a href="https://docs.aws.amazon.com/bedrock/latest/userguide/agents-multi-agent-collaboration.html">multi-agent collaboration</a> - check the latest AWS documentation how you can utilize Bedrock Agents in your projects.</p>

<h2 id="conclusions">Conclusions</h2>

<p>Generative AI is here. We are going to see a new information revolution in which various manual processes like handling customer service requests will be automated using Generative AI Agents.</p>

<p><em>The writer is working at a major international IT corporation building cloud infrastructures and implementing applications on top of those infrastructures.</em></p>

<p>Kari Marttila</p>

<p><sub> Kari Marttila’s Home Page in LinkedIn: <a href="https://www.linkedin.com/in/karimarttila/">https://www.linkedin.com/in/karimarttila/</a> </sub></p>]]></content><author><name>Kari Marttila</name></author><category term="[&quot;aws&quot;]" /><category term="aws" /><category term="bedrock" /><category term="agent" /><category term="genai" /><category term="terraform" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">AWS Batch Revisited</title><link href="http://www.karimarttila.fi/aws/2025/09/14/aws-batch-revisited.html" rel="alternate" type="text/html" title="AWS Batch Revisited" /><published>2025-09-14T00:00:00+00:00</published><updated>2025-09-14T00:00:00+00:00</updated><id>http://www.karimarttila.fi/aws/2025/09/14/aws-batch-revisited</id><content type="html" xml:base="http://www.karimarttila.fi/aws/2025/09/14/aws-batch-revisited.html"><![CDATA[<p><img src="/img/2025-09-14-aws-batch-revisited_img_1.png" alt="A typical AWS Batch architecture" /></p>

<p><em>A typical AWS Batch architecture.</em></p>

<h2 id="introduction">Introduction</h2>

<p>Almost ten years ago I was working in another corporation and we built a system for one customer using AWS Batch. I have documented that story in this blog post: <a href="/aws/2017/11/09/aws-batch-and-docker-containers.html">AWS Batch and Docker Containers</a>.</p>

<p>Now, after all these years, I got a similar use-case in my current project: batch processing in AWS infrastructure. A chance to implement the batch processing again using <a href="https://aws.amazon.com/batch/">AWS Batch service</a>.</p>

<h2 id="what-is-aws-batch">What Is AWS Batch?</h2>

<p><a href="https://aws.amazon.com/batch/">AWS Batch service</a> is a fully managed service that you can use to run various workloads in the AWS infrastructure. You don’t have to worry about the computing environment (to set up a complex orchestration e.g. using EKS or ECS) - you can configure AWS Batch to use <a href="https://aws.amazon.com/fargate/">Fargate</a> which provides a serverless computing environment for your workloads. AWS Batch offers queues, job definitions and job management as part of the solution.</p>

<p>If you need more complex orchestration, you should consider <a href="https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html">AWS Step Functions</a> or <a href="https://aws.amazon.com/managed-workflows-for-apache-airflow/">Amazon Managed Workflows for Apache Airflow (MWAA)</a>. I have written a couple of articles regarding these services:</p>

<ul>
  <li><a href="/aws/2023/05/11/aws-step-functions-first-impressions.html">AWS Step Functions - First Impressions</a></li>
  <li><a href="/aws/2023/11/24/airflow-vs-stepfunctions.html">AWS Managed Airflow vs AWS Step Functions</a></li>
</ul>

<h2 id="the-aws-batch-use-case">The AWS Batch Use Case</h2>

<p>The diagram above depicts a typical AWS Batch use case. In this scenario some external entity uploads some file to “New files” <a href="https://aws.amazon.com/s3/">S3 bucket</a>. The S3 bucket is configured to trigger <a href="https://aws.amazon.com/lambda/">Lambda</a> when a new object is stored into the S3 bucket. Lambda then triggers an AWS Batch job. We need Lambda since you cannot configure S3 directly to trigger an AWS Batch job.</p>

<p>When AWS Batch starts it pulls the <a href="https://www.docker.com/">Docker image</a> from <a href="https://aws.amazon.com/ecr/">ECR</a>, and starts the Docker container. The application in the docker container then fetches the uploaded file and starts processing it. After the processing is done, the application writes results to the “Results” S3 bucket and publishes a notification to <a href="https://aws.amazon.com/sns/">SNS</a> to notify various stakeholders regarding a new successful processing.</p>

<p>AWS resources also publish metrics to <a href="https://aws.amazon.com/cloudwatch/">CloudWatch</a>. You can also publish custom metrics to CloudWatch. And you should, since for some reason AWS Batch does not publish metrics regarding successful and failed jobs. See chapter “Custom Metrics” for more information.</p>

<p>If there are errors, you can configure various alerts and alert notifications to SNS for developers. You can also create a custom CloudWatch dashboard for developers to monitor the most important metrics of your infrastructure. Maybe I write another blog post regarding the custom CloudWatch dashboard later on.</p>

<h2 id="terraform-code">Terraform Code</h2>

<p>I created the AWS infrastructure solution using <a href="https://developer.hashicorp.com/terraform">Terraform</a>. I have used Terraform almost ten years now and I am pretty experienced Terraform user. Example regarding the AWS Batch compute environment and queue implemented using Terraform <a href="https://developer.hashicorp.com/terraform/language">hcl</a>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>resource "aws_batch_compute_environment" "compute_environment" {
  name = "${local.res_prefix}-${local.module_name}-compute-env"
  
  compute_resources {
    type               = var.compute_env_compute_type
    max_vcpus          = var.compute_env_max_vcpus
    security_group_ids = [aws_security_group.batch_sg.id]    
    subnets            = var.vpc_landing_private_subnets
  }

  service_role = aws_iam_role.batch_service_role.arn
  type         = var.compute_env_service_type
  depends_on   = [aws_iam_role.batch_service_role]

  tags = merge(local.default_tags, {
    Name = "${local.res_prefix}-${local.module_name}-compute-env"
  })
}


resource "aws_batch_job_queue" "job_queue" {
  name                 = "${local.res_prefix}-${local.module_name}-job-queue"
  state                = var.job_queue_state
  priority             = var.job_queue_priority
  
  compute_environment_order {
    order               = 1
    compute_environment = aws_batch_compute_environment.compute_environment.arn
  }
  
  tags = merge(local.default_tags, {
    Name = "${local.res_prefix}-${local.module_name}-job-queue"
  })
}
</code></pre></div></div>

<h2 id="custom-metrics">Custom Metrics</h2>

<p>In your application you can easily publish metrics to some custom namespace. An example using Python:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">def</span> <span class="nf">publish_cloudwatch_metric</span><span class="p">(</span>
    <span class="n">metric_name</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">namespace</span><span class="o">=</span><span class="n">DUMMY_BATCH_NS</span><span class="p">,</span> <span class="n">dimensions</span><span class="o">=</span><span class="bp">None</span>
<span class="p">):</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">metric_data</span> <span class="o">=</span> <span class="p">{</span>
            <span class="sh">'</span><span class="s">MetricName</span><span class="sh">'</span><span class="p">:</span> <span class="n">metric_name</span><span class="p">,</span>
            <span class="sh">'</span><span class="s">Value</span><span class="sh">'</span><span class="p">:</span> <span class="n">value</span><span class="p">,</span>
            <span class="sh">'</span><span class="s">Unit</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">Count</span><span class="sh">'</span>
        <span class="p">}</span>

        <span class="k">if</span> <span class="n">dimensions</span><span class="p">:</span>
            <span class="n">metric_data</span><span class="p">[</span><span class="sh">'</span><span class="s">Dimensions</span><span class="sh">'</span><span class="p">]</span> <span class="o">=</span> <span class="n">dimensions</span>

        <span class="n">cloudwatch_client</span><span class="p">.</span><span class="nf">put_metric_data</span><span class="p">(</span>
            <span class="n">Namespace</span><span class="o">=</span><span class="n">namespace</span><span class="p">,</span>
            <span class="n">MetricData</span><span class="o">=</span><span class="p">[</span><span class="n">metric_data</span><span class="p">]</span>
        <span class="p">)</span>
<span class="c1"># ...
</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>

<span class="c1"># ...
</span>    <span class="k">try</span><span class="p">:</span>
        <span class="nf">processing</span><span class="p">()</span>
        <span class="c1"># Process succeeded - publish success metric
</span>        <span class="n">dimensions</span> <span class="o">=</span> <span class="p">[</span>
            <span class="p">{</span>
                <span class="sh">'</span><span class="s">Name</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">SomeKey</span><span class="sh">'</span><span class="p">,</span>
                <span class="sh">'</span><span class="s">Value</span><span class="sh">'</span><span class="p">:</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">[</span><span class="sh">"</span><span class="s">SOME_KEY</span><span class="sh">"</span><span class="p">]</span>
            <span class="p">}</span>
        <span class="p">]</span>
        <span class="nf">publish_cloudwatch_metric</span><span class="p">(</span>
            <span class="sh">'</span><span class="s">ProcessingSucceeded</span><span class="sh">'</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">DUMMY_BATCH_NS</span><span class="p">,</span> <span class="n">dimensions</span>
        <span class="p">)</span>
<span class="c1"># ...
</span></code></pre></div></div>

<h2 id="conclusions">Conclusions</h2>

<p>After all these years AWS Batch is still a good option for simple batch processing in the AWS infrastructure. If you need more complex batch processing, consider using AWS Step Functions.</p>

<p><em>The writer is working at a major international IT corporation building cloud infrastructures and implementing applications on top of those infrastructures.</em></p>

<p>Kari Marttila</p>

<p><sub> Kari Marttila’s Home Page in LinkedIn: <a href="https://www.linkedin.com/in/karimarttila/">https://www.linkedin.com/in/karimarttila/</a> </sub></p>]]></content><author><name>Kari Marttila</name></author><category term="[&quot;aws&quot;]" /><category term="aws" /><category term="batch" /><category term="terraform" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Converting Clojure Backend to Python Using Copilot</title><link href="http://www.karimarttila.fi/python/2025/04/26/converting-clojure-to-python-using-copilot.html" rel="alternate" type="text/html" title="Converting Clojure Backend to Python Using Copilot" /><published>2025-04-26T00:00:00+00:00</published><updated>2025-04-26T00:00:00+00:00</updated><id>http://www.karimarttila.fi/python/2025/04/26/converting-clojure-to-python-using-copilot</id><content type="html" xml:base="http://www.karimarttila.fi/python/2025/04/26/converting-clojure-to-python-using-copilot.html"><![CDATA[<p><img src="/img/2025-04-26-converting-clojure-to-python-using-copilot_img_1.png" alt="Python and Clojure side to side" /></p>

<p><em>Python and Clojure side to side.</em></p>

<h2 id="introduction">Introduction</h2>

<p>We are building an in-house product in our unit. I was asked to create a quick <a href="https://en.wikipedia.org/wiki/Proof_of_concept">proof of concept</a> (POC) to see if it is feasible to create this kind of product. I asked if I am allowed to use what ever tools I want to build the POC as fast as possible. The product owner said  yes. I chose to build a Clojure fullstack app. The application runs on <a href="https://aws.amazon.com/">AWS platform</a> and uses several AWS services like <a href="https://aws.amazon.com/cognito/">Cognito</a>, <a href="https://aws.amazon.com/dynamodb/">DynamoDB</a>, and <a href="https://aws.amazon.com/bedrock/">Bedrock</a>. I used <a href="https://github.com/cognitect-labs/aws-api">Cognitect aws-api</a> to interact with those AWS services.</p>

<p>It was really fast to build the fullstack Clojure POC. But later on, the product owner said that we start to build the actual product, and for the product we need to use more mainstream languages. He chose <a href="https://www.python.org/">Python</a> as the language for the backend, and <a href="https://www.typescriptlang.org/">Typescript</a> for the frontend. I was asked to convert the Clojure POC backend to Python. Product owner suggested that I use some generative AI tool with the conversion. I thought that this was a good idea. I would see how feasible it is to use genAI tools in that kind of conversion. This blog post describes my experiences with that conversion work.</p>

<h2 id="strategy">Strategy</h2>

<p>I had heard bad experiences regarding converting code bases from one language to another in one shot. I was experimenting converting simple Clojure functions to Python, and I realized that mostly the conversion was done pretty well, but there were small errors here are there. If there are e.g. just one error per function, that will cumulate for a big code base quite quickly. And at the end of conversion you have a new totally broken code base with the target language.</p>

<p>Therefore, I decided not to try to convert the whole Clojure backend code base in one shot, but to convert every Clojure function one by one, and implement unit and integration tests for each function to validate the Python function works exactly the same way as its Clojure counterpart.</p>

<p>While doing this kind of conversion, I realized another insight. You need to have <em>domain knowledge</em> and understanding regarding the source system. This was naturally easy for me, since I had written all of the Clojure POC code base. But I realized that maybe those big conversion projects had failed for two reasons. In big conversion projects you may end up with a broken code base with the target language, and possibly the conversion specialists are not those specialists who created the source system in the first place - therefore lacking true domain understanding whether the generated target system really does the same thing as the source system.</p>

<h2 id="copilot">Copilot</h2>

<p>My corporation provides a Copilot Enterprise license for me. I have been using Copilot in programming for quite a long time already. You might want to read my other Copilot related blog posts as well:</p>

<ul>
  <li><a href="/programming/2024/11/20/using-copilot-in-programming.html">Using Copilot in Programming</a></li>
  <li><a href="/programming/2025/02/01/copilot-keybindings.html">Copilot Keybindings</a></li>
</ul>

<p>So, the first functions were a bit difficult for Copilot to convert from Clojure to Python. But then I realized that with good prompting and using the previous functions as an example (in the prompt, mentioning to use them as an example of a good conversion), Copilot became more and more fluent in the conversion work.</p>

<p>Example of this kind of prompting:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>In #file:sessionutil.py I have functions get_role_name and get_project_name. 
Now I need you to create a similar get_organization_name function in Python for this Clojure function: 
(defn get-organization-name "Gets the organization name for the given org-id." [org-id] (let [params...
</code></pre></div></div>

<p>(… I copy-pasted the Clojure function <code class="language-plaintext highlighter-rouge">get-organization-name</code> at the end of the prompt). Copilot created a perfect Python function as an output.</p>

<p>For similar functions I used the same prompting technique for test cases as well, and if there were similar test case function to be used as an example, Copilot quite often created good test cases for the new function as well (or at least good skeletons for the human programmer to fill in the details).</p>

<h2 id="example-conversion">Example Conversion</h2>

<p>Let’s show one example conversion. This is the source Clojure function to enrich the list of domain data with entity names:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nf">defn*</span><span class="w"> </span><span class="n">enrich-projects-and-roles</span><span class="w">
  </span><span class="s">"Enriches projects and roles with names."</span><span class="w">
  </span><span class="p">[</span><span class="n">session-config</span><span class="w"> </span><span class="n">projects-and-roles</span><span class="p">]</span><span class="w">
  </span><span class="p">(</span><span class="nb">map</span><span class="w"> </span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="p">[</span><span class="n">entity</span><span class="p">]</span><span class="w">
         </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[{</span><span class="no">:keys</span><span class="w"> </span><span class="p">[</span><span class="n">org-id</span><span class="w"> </span><span class="n">proj-id</span><span class="w"> </span><span class="n">role-id</span><span class="p">]}</span><span class="w"> </span><span class="n">entity</span><span class="w">
               </span><span class="n">org-name</span><span class="w"> </span><span class="p">(</span><span class="nf">get-organization-name</span><span class="w"> </span><span class="n">org-id</span><span class="w"> </span><span class="p">(</span><span class="no">:dynamodb-table-organization-name</span><span class="w"> </span><span class="n">session-config</span><span class="p">))</span><span class="w">
               </span><span class="n">proj-name</span><span class="w"> </span><span class="p">(</span><span class="nf">get-project-name</span><span class="w"> </span><span class="n">org-id</span><span class="w"> </span><span class="n">proj-id</span><span class="w"> </span><span class="p">(</span><span class="no">:dynamodb-table-project-name</span><span class="w"> </span><span class="n">session-config</span><span class="p">))</span><span class="w">
               </span><span class="n">role-name</span><span class="w"> </span><span class="p">(</span><span class="nf">get-role-name</span><span class="w"> </span><span class="n">role-id</span><span class="w"> </span><span class="p">(</span><span class="no">:dynamodb-table-role-name</span><span class="w"> </span><span class="n">session-config</span><span class="p">))]</span><span class="w">
           </span><span class="p">(</span><span class="nb">-&gt;</span><span class="w"> </span><span class="n">entity</span><span class="w">
               </span><span class="p">(</span><span class="nb">assoc</span><span class="w"> </span><span class="no">:org-name</span><span class="w"> </span><span class="n">org-name</span><span class="p">)</span><span class="w">
               </span><span class="p">(</span><span class="nb">assoc</span><span class="w"> </span><span class="no">:proj-name</span><span class="w"> </span><span class="n">proj-name</span><span class="p">)</span><span class="w">
               </span><span class="p">(</span><span class="nb">assoc</span><span class="w"> </span><span class="no">:role-name</span><span class="w"> </span><span class="n">role-name</span><span class="p">))))</span><span class="w">
       </span><span class="n">projects-and-roles</span><span class="p">))</span><span class="w">
</span></code></pre></div></div>

<p>… and the converted Python function:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">enrich_projects_and_roles</span><span class="p">(</span><span class="n">projects_and_roles</span><span class="p">:</span> <span class="nb">list</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">
    Enriches projects and roles with organization, project, and role names.

    Args:
        projects_and_roles (list): List of dictionaries, each representing a project and role.

    Returns:
        list: A list of enriched dictionaries with organization, project, and role names.
    </span><span class="sh">"""</span>
    <span class="n">enriched_entities</span> <span class="o">=</span> <span class="p">[]</span>

    <span class="k">for</span> <span class="n">entity</span> <span class="ow">in</span> <span class="n">projects_and_roles</span><span class="p">:</span>
        <span class="n">org_id</span> <span class="o">=</span> <span class="n">entity</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">org_id</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">proj_id</span> <span class="o">=</span> <span class="n">entity</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">proj_id</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">role_id</span> <span class="o">=</span> <span class="n">entity</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">role_id</span><span class="sh">"</span><span class="p">)</span>

        <span class="k">try</span><span class="p">:</span>
            <span class="c1"># Fetch names from DynamoDB tables
</span>            <span class="n">org_name</span> <span class="o">=</span> <span class="nf">get_organization_name</span><span class="p">(</span><span class="n">org_id</span><span class="p">)</span>
            <span class="n">proj_name</span> <span class="o">=</span> <span class="nf">get_project_name</span><span class="p">(</span><span class="n">org_id</span><span class="p">,</span> <span class="n">proj_id</span><span class="p">)</span>
            <span class="n">role_name</span> <span class="o">=</span> <span class="nf">get_role_name</span><span class="p">(</span><span class="n">role_id</span><span class="p">)</span>

            <span class="c1"># Enrich the entity with the fetched names
</span>            <span class="n">enriched_entity</span> <span class="o">=</span> <span class="p">{</span>
                <span class="o">**</span><span class="n">entity</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">org_name</span><span class="sh">"</span><span class="p">:</span> <span class="n">org_name</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">proj_name</span><span class="sh">"</span><span class="p">:</span> <span class="n">proj_name</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">role_name</span><span class="sh">"</span><span class="p">:</span> <span class="n">role_name</span><span class="p">,</span>
            <span class="p">}</span>
            <span class="n">enriched_entities</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">enriched_entity</span><span class="p">)</span>
        <span class="c1"># Let ValueErrors from the called functions to pass through.
</span>        <span class="c1"># Handle DynamoDB client errors
</span>        <span class="k">except</span> <span class="nb">RuntimeError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nc">RuntimeError</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">An error occurred while querying DynamoDB: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span> <span class="k">from</span> <span class="n">e</span>

    <span class="k">return</span> <span class="n">enriched_entities</span>
</code></pre></div></div>

<p>If you are a clojurian like me, you may have spotted <code class="language-plaintext highlighter-rouge">defn*</code>. That’s the <a href="https://github.com/AbhinavOmprakash/snitch">snitch</a> defmacro. I used snitch to instrument the function to check what is the value passed to parameter <code class="language-plaintext highlighter-rouge">projects-and-roles</code>, so that I can use that as data in the equivalent test case for the python code (I didn’t create tests for the Clojure code base since it was a quick POC). I also did some refactoring - moved fetching the DynamoDB table names from the configuration to the functions they are used.</p>

<h2 id="copilot-generation-instructions">Copilot Generation Instructions</h2>

<p>You can create file <code class="language-plaintext highlighter-rouge">.github/copilot-instructions.md</code> to provide detailed instructions Copilot should use when generating new code. E.g. I found out that Copilot generated too long lines in functions. Therefore, I needed to instruct Copilot to follow the project linting rules:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>In #file:sessionutil.py I need function get_project_role_info.
See other functions and #file:copilot-instructions.md how to write it.
Make it the same functionality as in this Clojure function:
(defn get-project-role-info "Queries the user Dynamo ...
</code></pre></div></div>

<p>And Copilot answers:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Here’s the Python implementation of the get_project_role_info function in 
#file:sessionutil.py, modeled after the provided Clojure function and adhering to the style 
of the existing functions and the linting instructions in #file:copilot-instructions.md:
def get_project_role_info(org_id: str, proj_id: str, email: str) -&gt; dict: ...
</code></pre></div></div>

<p>(Line breaks mine.) In the Copilot prompt, you don’t have to mention adhering to the <code class="language-plaintext highlighter-rouge">.github/copilot-instructions.md</code> - Copilot does this automatically.</p>

<h2 id="comparing-developer-experience-with-python-and-clojure">Comparing Developer Experience with Python and Clojure</h2>

<p>Clojure wins 6-0, no question about it. <code class="language-plaintext highlighter-rouge">Snitch</code> is just one example I could use in the Clojure side. Using <a href="https://clojure.org/guides/repl/introduction">Clojure REPL</a> and a good Clojure REPL editor integration like with <a href="https://calva.io/">Calva</a> you can quite quickly implement various functionalities using Clojure and test the functionality at the spot you are writing the code using the Clojure REPL editor integration. There is no such good REPL editor integration for Python, and there cannot be since Python is not a <a href="https://en.wikipedia.org/wiki/Homoiconicity">homoiconic language</a>. I once did some research regarding <a href="/python/2023/04/30/python-repl.html">Python REPL</a> and wrote that blog post regarding my findings. For python development my usual workflow is: 1. Implement the skeleton for production function, 2. Implement unit and integration tests for the function. 3. Use the test cases as a development bench for further developing the production function. I.e., write, run the test, see what happened - rather caveman style of programming if you compare to the Lisp development practices. In the Clojure side, you can add a <a href="https://practical.li/clojure/clojure-cli/projects/rich-comments/">Clojure rich comment</a>, e.g. snitch the function, and then examine the bindings in the function, put returned data to rich comment, tap it to portal for examination, etc. (for more about these techniques, see: <a href="/clojure/2025/04/20/clojure-power-tools-part-3.html">Clojure Power Tools Part 3</a>). Using Clojure you can interact with the live system with your REPL in the editor. This is really something I miss with other languages.</p>

<p>Another observation is the code base size. Using Clojure you end up with considerably smaller code base. Smaller code base makes it easier to grasp the domain knowledge of the code base.</p>

<p>If you interested comparing Clojure and Python, here are my observations as a short summary:</p>

<ul>
  <li><strong>REPL</strong>: Clojure has an excellent REPL editor integration. Python does have a REPL (like many interpreted languages do), but it is a poor counterpart compared to powerful REPLs in homoiconic Lisp languages.</li>
  <li><strong>Immutable data structures</strong>: Clojure: yes, Python: no.</li>
  <li><strong>Using functional programming paradigm</strong>: Clojure is a functional language, but you can do quite nice functional code using Python as well.</li>
  <li><strong>Standard library</strong>: Clojure provides a powerful <a href="https://clojure.org/reference/sequences">sequence abstraction</a> in its standard library.</li>
  <li><strong>Lazy evaluation</strong>: Clojure has <a href="https://clojure.org/reference/lazy">lazy sequences</a> which allows e.g. infinite lists. Python also has <a href="https://realpython.com/python-lazy-evaluation/">lazy evaluation</a>.</li>
  <li><strong>Macros</strong>: You need a homoiconic language for creating a powerful <a href="https://clojure.org/reference/macros">macro</a> functionality incorporated into the language. See an example of <a href="https://www.braveclojure.com/writing-macros/">writing macros</a>.</li>
  <li><strong>Concurrent programming and threads</strong>: See <a href="https://clojure.org/about/rationale">Rationale</a> which explains that Clojure is a hosted language running on <a href="https://docs.oracle.com/en/java/javase/24/vm/java-virtual-machine-technology-overview.html">JVM</a> which supports native threads. Clojure was implemented with concurrent programming as one of the main goals. Using immutable data structures concurrent programming removes one major issue with threads manipulating the same mutable data. Clojure also provides good primitives for sharing the same data with different threads, see more about this in <a href="https://clojure.org/about/concurrent_programming">Concurrent Programming</a>. Python has <a href="https://realpython.com/python-gil/">Global Interpreter Lock</a> (GIL) and uses a library to provide concurrent programming.</li>
  <li><strong>Structural code editing</strong>: Using <a href="https://calva.io/paredit/">paredit</a> editing Clojure code is a breeze.</li>
  <li><strong>Code base size</strong>: For Clojure, the code base tends to be considerably smaller. You can express more with less code.</li>
  <li><strong>Developer pool</strong>: Python wins hands down. E.g., this was the main reason my product owner didn’t want to implement the actual product using Clojure (<em>“How many Clojure developers there are in our corporation if you decide to leave?”</em>) This is a major issue with Clojure. One solution would be to build a Clojure team so that there are more than one developer in the team.</li>
</ul>

<h2 id="conclusions">Conclusions</h2>

<p>Generative AI tools are already so good that they can be used to convert code bases from one language to another. But you need to have good domain knowledge of the source system, and it is a good strategy to do the conversion function by function, not in one shot. But as these generative AI tools are getting better and better, I expect them to be able to do the conversion in one shot as well, and create test cases and run the test cases and fix the errors done in the first round of the conversion.</p>

<p><em>The writer is working at a major international IT corporation building cloud infrastructures and implementing applications on top of those infrastructures.</em></p>

<p>Kari Marttila</p>

<p><sub> Kari Marttila’s Home Page in LinkedIn: <a href="https://www.linkedin.com/in/karimarttila/">https://www.linkedin.com/in/karimarttila/</a> </sub></p>]]></content><author><name>Kari Marttila</name></author><category term="[&quot;python&quot;]" /><category term="programming" /><category term="python" /><category term="clojure" /><category term="languages" /><category term="repl" /><category term="productivity" /><category term="genai" /><category term="ai" /><category term="copilot" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Clojure Power Tools Part 3</title><link href="http://www.karimarttila.fi/clojure/2025/04/20/clojure-power-tools-part-3.html" rel="alternate" type="text/html" title="Clojure Power Tools Part 3" /><published>2025-04-20T00:00:00+00:00</published><updated>2025-04-20T00:00:00+00:00</updated><id>http://www.karimarttila.fi/clojure/2025/04/20/clojure-power-tools-part-3</id><content type="html" xml:base="http://www.karimarttila.fi/clojure/2025/04/20/clojure-power-tools-part-3.html"><![CDATA[<p><img src="/img/2025-04-20-clojure-power-tools-part-3_img_1.png" alt="Clojure REPL" /></p>

<p><em>Clojure REPL.</em></p>

<h1 id="table-of-contents--">Table of Contents  <!-- omit in toc --></h1>

<ul>
  <li><a href="#introduction">Introduction</a></li>
  <li><a href="#vscode-calva-and-repl-editor-integration">VSCode, Calva and REPL Editor Integration</a></li>
  <li><a href="#babashka">Babashka</a></li>
  <li><a href="#fullstack-libraries">Fullstack Libraries</a>
    <ul>
      <li><a href="#metosin-libraries-reitit-malli-and-jsonista">Metosin Libraries: Reitit, Malli and Jsonista</a></li>
      <li><a href="#aero-and-integrant">Aero and Integrant</a></li>
      <li><a href="#replicant-and-hiccup">Replicant and Hiccup</a></li>
    </ul>
  </li>
  <li><a href="#development-practices-and-tools">Development Practices and Tools</a>
    <ul>
      <li><a href="#repl">REPL</a></li>
      <li><a href="#personal-profile-deps">Personal Profile Deps</a></li>
      <li><a href="#inline-defs">Inline Defs</a></li>
      <li><a href="#hashp">Hashp</a></li>
      <li><a href="#portal">Portal</a></li>
      <li><a href="#gadget">Gadget</a></li>
      <li><a href="#calva-debugger">Calva Debugger</a></li>
      <li><a href="#snitch">Snitch</a></li>
    </ul>
  </li>
  <li><a href="#bonus-tool-copilot">Bonus Tool: Copilot</a></li>
  <li><a href="#conclusions">Conclusions</a></li>
</ul>

<h2 id="introduction">Introduction</h2>

<p>I have already covered Clojure Power Tools some 5 years ago in a couple of blog posts:</p>

<ul>
  <li><a href="/clojure/2020/10/26/clojure-power-tools-part-1.html">Clojure Power Tools Part 1</a></li>
  <li><a href="/clojure/2020/10/29/clojure-power-tools-part-2.html">Clojure Power Tools Part 2</a></li>
</ul>

<p>In this new blog post, I will briefly summarize the most important tools discussed in those two blog posts and then introduce some new power tools that I have found useful recently. I thought it might be a good idea to list all the most important Clojure power tools in one blog post so that I don’t forget them in the future. I add to this list also Clojure libraries that I use in my Clojure/script fullstack applications.</p>

<p>I use this Clojure fullstack application to introduce those power tools: <a href="https://github.com/karimarttila/clojure/tree/master/webstore-demo/replicant-webstore">replicant-webstore</a>.</p>

<h2 id="vscode-calva-and-repl-editor-integration">VSCode, Calva and REPL Editor Integration</h2>

<p>Your editor is of course one of your most important tools what ever programming language you use. My current choice is <a href="https://code.visualstudio.com/">Visual Studio Code</a>. It is rather light but also provides a rich set of extensions for various programming purposes. Nowadays, it also provides a good generative AI integration to help you with your programming tasks, I have written a couple of blog posts about <a href="/programming/2024/11/20/using-copilot-in-programming.html">Using Copilot in Programming</a> about and my <a href="/programming/2025/02/01/copilot-keybindings.html">Copilot Keybindings</a>.</p>

<p>If you are programming Clojure with VSCode editor, I defnitely recommend the excellent <a href="https://calva.io/">Calva</a> extension. It provides a great Clojure REPL integration to VSCode, <a href="https://calva.io/paredit/">paredit</a> structural editing, and much more. If you are interested trying Calva, I recommend reading the excellent Calva documentation and start using it. I have also written three blog posts regarding my Calva configurations:</p>

<ul>
  <li><a href="/clojure/2022/10/08/clojure-calva.html">Configuring VSCode/Calva for Clojure programming</a></li>
  <li><a href="/clojure/2022/10/16/clojure-calva-part2.html">Configuring VSCode/Calva for Clojure programming - Part 2</a></li>
  <li><a href="/clojure/2022/10/18/clojure-calva-part3.html">Configuring VSCode/Calva for Clojure programming - Part 3</a></li>
</ul>

<p>An important part of using Clojure is the keybindings (e.g. for evaluating forms, giving paredit commands, etc.). I have written a couple of blog posts regarding my keybindings:</p>

<ul>
  <li><a href="/clojure/2025/02/02/clojure-keybindings.html">Clojure Keybindings</a></li>
  <li><a href="/keyboard/2020/09/28/dygma-raise-reflections-part-1.html">Dygma Raise Keyboard Reflections Part 1</a></li>
</ul>

<p>And one hint. Keep your VSCode configurations (at least <code class="language-plaintext highlighter-rouge">keybindings.json</code> and <code class="language-plaintext highlighter-rouge">settings.json</code>) in version control (<a href="https://git-scm.com/">Git</a>).</p>

<h2 id="babashka">Babashka</h2>

<p><a href="https://github.com/babashka/babashka">Babashka</a> is a marvelous tool for writing scripts and automating tasks. I have written a couple of blog posts regarding Babashka:</p>

<ul>
  <li><a href="/clojure/2020/09/01/using-clojure-in-command-line-with-babashka.html">Using Clojure in Command Line with Babashka</a></li>
  <li><a href="/clojure/2022/12/04/using-babashka-to-get-electricity-prices.html">Using Babashka to Get Electricity Prices</a></li>
</ul>

<p>I learned from one Metosin example project how to use Babashka as a task runner for my projects. See my latest <a href="https://github.com/karimarttila/clojure/tree/master/webstore-demo/replicant-webstore">Clojure fullstack exercise</a> in which I used Babashka as a task runner, expecially file <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/bb.edn">bb.edn</a> and <a href="https://github.com/karimarttila/clojure/tree/master/webstore-demo/replicant-webstore/bb-scripts">bb-scripts</a> directory for how to start the backend and frontend REPLs.</p>

<h2 id="fullstack-libraries">Fullstack Libraries</h2>

<h3 id="metosin-libraries-reitit-malli-and-jsonista">Metosin Libraries: Reitit, Malli and Jsonista</h3>

<p>These are my favourite Metosin Libraries I always include to my Clojure fullstack projects. You can use these libraries both in the backend and the frontend.</p>

<p><a href="https://github.com/metosin/reitit">Reitit</a> provides excellent routing functionalities. See in that clojure fullstack application I mentioned previously:</p>

<ul>
  <li><a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/src/clj/backend/routes.clj">routes.clj</a>: Backend API routing.</li>
  <li><a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/src/cljs/frontend/routes.cljs">routes.cljs</a>: Frontend web app routing.</li>
</ul>

<p><a href="https://github.com/metosin/malli">Malli</a> provides excellent schema that you can use as a Clojure common (<code class="language-plaintext highlighter-rouge">cljc</code>) file that you can comprise both to your backend API and your frontend to validate that the backend returned data that conforms to the schema. See example in that Clojure fullstack application: <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/src/cljc/common/schema.cljc">schema.cljc</a>.</p>

<p><a href="https://github.com/metosin/jsonista">Jsonista</a> is a Clojure library for JSON encoding and decoding. Using <a href="https://github.com/metosin/muuntaja">Muuntaja</a> you can easily do edn/json encoding in your API.</p>

<h3 id="aero-and-integrant">Aero and Integrant</h3>

<p><a href="https://github.com/juxt/aero">Aero</a> is an excellent configuration library. See example in <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/resources/config.edn">config.edn</a> regarding the demonstration application configuration and how to read it in <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/src/clj/backend/main.clj">main.clj</a>.</p>

<p><a href="https://github.com/weavejester/integrant">Integrant</a> provides a nice way to define your application from components, define the relationships between the components in your configuration (see the <code class="language-plaintext highlighter-rouge">config.edn</code> file above), and reset/reload the state of your application using those components. See also <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/src/clj/backend/db.clj">db.clj</a> in which the <code class="language-plaintext highlighter-rouge">defmethod ig/init-key :db/tsv</code> function reads the tab separated file and initializes our little “demonstration database.”</p>

<h3 id="replicant-and-hiccup">Replicant and Hiccup</h3>

<p>In the frontend, I used for years <a href="https://github.com/reagent-project/reagent">Reagent</a> which is a <a href="https://react.dev/">React</a> wrapper for Clojurescript. There are <a href="https://www.metosin.fi/blog/reagent-towards-react-18">some technical challenges for Reagent to use the latest React versions</a>, and I was therefore looking for some new UI Clojurescript technology. I first considered using <a href="https://github.com/pitch-io/uix">UIx</a> which is also a React wrapper for Clojurescript. But then I discovered <a href="https://github.com/cjohansen/replicant">Replicant</a> which with <a href="https://github.com/weavejester/hiccup">Hiccup</a> is a very lightweight and Clojurish way of doing frontend. I have covered Replicant in a couple of my blog posts:</p>

<ul>
  <li><a href="/clojurescript/2025/02/28/clojurescript-with-replicant.html">Frontend Development with Clojurescript and Replicant</a></li>
  <li><a href="/clojurescript/2025/03/10/ui-design-with-clojurescript-replicant-and-portfolio.html">Designing UI components with Clojurescript, Replicant and Portfolio</a></li>
</ul>

<h2 id="development-practices-and-tools">Development Practices and Tools</h2>

<h3 id="repl">REPL</h3>

<p>If you are learning Clojure, <a href="https://clojure.org/guides/repl/introduction">Programming at the REPL</a> is something you definitely have to learn. You should check what kind of REPL support there is with the editor you are using, and start learning to use it. If you are using VSCode, you find more information above in chapter <a href="#vscode-calva-and-repl-editor-integration">VSCode, Calva and REPL Editor Integration</a>.</p>

<p>I have three monitors at my desk. The main monitor is where I keep my VSCode editor. In the side monitor I keep the REPL window. This way I can maximize the main monitor for the editing, but also see in my side the REPL output. If you are using VSCode, this is easy. You first start the REPL with Calva. If you have done the same kind of Calva configuration that I have explained in my previous blog posts, you should have your Calva Output in VSCode editor area in a tab. Give VSCode command <code class="language-plaintext highlighter-rouge">View: Move Editor into New Window</code>, this will move the active editor tab into a new VSCode Window. Now you can move the REPL output window into your second monitor.</p>

<p>I have a couple similar commands to evaluate Clojure forms in Calva. <code class="language-plaintext highlighter-rouge">Alt-L</code> evaluates the form and outputs the result in the editor as an ephemeral output which you can reset with <code class="language-plaintext highlighter-rouge">Esc</code> key. With <code class="language-plaintext highlighter-rouge">Alt+Shift+L</code> Calva writes the evaluation result below the evaluated form like this:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="p">(</span><span class="nb">keys</span><span class="w"> </span><span class="p">(</span><span class="nb">deref</span><span class="w"> </span><span class="p">(</span><span class="no">:db/tsv</span><span class="w"> </span><span class="p">(</span><span class="nf">user/env</span><span class="p">))))</span><span class="w">
  </span><span class="c1">;;=&gt; (:books :movies)</span><span class="w">
</span></code></pre></div></div>

<p>REPL is your power tool with Clojure and you should learn to use it efficiently.</p>

<h3 id="personal-profile-deps">Personal Profile Deps</h3>

<p>This is my current <code class="language-plaintext highlighter-rouge">~/.clojure/deps.edn</code> file:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">
</span><span class="p">{</span><span class="no">:aliases</span><span class="w"> </span><span class="p">{</span><span class="no">:kari</span><span class="w"> </span><span class="p">{</span><span class="no">:extra-paths</span><span class="w"> </span><span class="p">[</span><span class="s">"scratch"</span><span class="p">]</span><span class="w">
                  </span><span class="no">:extra-deps</span><span class="w"> </span><span class="p">{</span><span class="c1">; NOTE: hashp 0.2.1 sci print bug.</span><span class="w">
                               </span><span class="n">hashp/hashp</span><span class="w"> </span><span class="p">{</span><span class="no">:mvn/version</span><span class="w"> </span><span class="s">"0.2.2"</span><span class="p">}</span><span class="w">
                               </span><span class="n">org.clojars.abhinav/snitch</span><span class="w"> </span><span class="p">{</span><span class="no">:mvn/version</span><span class="w"> </span><span class="s">"0.1.16"</span><span class="p">}</span><span class="w">
                               </span><span class="n">com.gfredericks/debug-repl</span><span class="w"> </span><span class="p">{</span><span class="no">:mvn/version</span><span class="w"> </span><span class="s">"0.0.12"</span><span class="p">}</span><span class="w">
                               </span><span class="n">djblue/portal</span><span class="w"> </span><span class="p">{</span><span class="no">:mvn/version</span><span class="w"> </span><span class="s">"0.58.5"</span><span class="p">}}}</span><span class="w">

           </span><span class="no">:reveal</span><span class="w"> </span><span class="p">{</span><span class="no">:extra-deps</span><span class="w"> </span><span class="p">{</span><span class="n">vlaaad/reveal</span><span class="w"> </span><span class="p">{</span><span class="no">:mvn/version</span><span class="w"> </span><span class="s">"1.3.284"</span><span class="p">}}</span><span class="w">
                    </span><span class="no">:ns-default</span><span class="w"> </span><span class="n">vlaaad.reveal</span><span class="w">
                    </span><span class="no">:exec-fn</span><span class="w"> </span><span class="n">repl</span><span class="p">}</span><span class="w">

           </span><span class="no">:outdated</span><span class="w"> </span><span class="p">{</span><span class="c1">;; Note that it is `:deps`, not `:extra-deps`</span><span class="w">
                      </span><span class="no">:deps</span><span class="w"> </span><span class="p">{</span><span class="n">com.github.liquidz/antq</span><span class="w"> </span><span class="p">{</span><span class="no">:mvn/version</span><span class="w"> </span><span class="s">"2.11.1269"</span><span class="p">}}</span><span class="w">
                      </span><span class="no">:main-opts</span><span class="w"> </span><span class="p">[</span><span class="s">"-m"</span><span class="w"> </span><span class="s">"antq.core"</span><span class="p">]}}}</span><span class="w">
</span></code></pre></div></div>

<p>I use these tools quite often and therefore keep them in my personal profile <em>kari</em>.</p>

<p>I then add my <em>kari</em> profile to scripts I use to start REPL in development, like this:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">:backend-repl-command</span><span class="w"> </span><span class="p">[</span><span class="s">"clojure -M:dev:backend:frontend:shadow-cljs:calva-external-repl:test:kari -i bb-scripts/backendinit.clj -m nrepl.cmdline --middleware \"[cider.nrepl/cider-middleware,shadow.cljs.devtools.server.nrepl/middleware]\""</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<h3 id="inline-defs">Inline Defs</h3>

<p>Inline defs is an old Clojure trick to debug Clojure code. Let’s explain it with a small example:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defmethod</span><span class="w"> </span><span class="n">ig/init-key</span><span class="w"> </span><span class="no">:db/tsv</span><span class="w"> </span><span class="p">[</span><span class="n">_</span><span class="w"> </span><span class="p">{</span><span class="no">:keys</span><span class="w"> </span><span class="p">[</span><span class="nb">path</span><span class="w"> </span><span class="n">data</span><span class="p">]</span><span class="w"> </span><span class="no">:as</span><span class="w"> </span><span class="n">db-opts</span><span class="p">}]</span><span class="w">
  </span><span class="p">(</span><span class="nf">log/infof</span><span class="w"> </span><span class="s">"Reading tsv data, config is %s"</span><span class="w"> </span><span class="p">(</span><span class="nb">pr-str</span><span class="w"> </span><span class="n">db-opts</span><span class="p">))</span><span class="w">
  </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">books</span><span class="w"> </span><span class="p">(</span><span class="nf">read-datafile</span><span class="w"> </span><span class="p">(</span><span class="nb">str</span><span class="w"> </span><span class="nb">path</span><span class="w"> </span><span class="s">"/"</span><span class="w"> </span><span class="p">(</span><span class="no">:books</span><span class="w"> </span><span class="n">data</span><span class="p">))</span><span class="w"> </span><span class="n">book-line</span><span class="w"> </span><span class="n">book-str</span><span class="p">)</span><span class="w">
        </span><span class="n">_</span><span class="w"> </span><span class="p">(</span><span class="k">def</span><span class="w"> </span><span class="n">mybooks</span><span class="w"> </span><span class="n">books</span><span class="p">)</span><span class="w"> </span><span class="c1">;; THIS IS THE INLINE DEF</span><span class="w">
        </span><span class="n">movies</span><span class="w"> </span><span class="p">(</span><span class="nf">read-datafile</span><span class="w"> </span><span class="p">(</span><span class="nb">str</span><span class="w"> </span><span class="nb">path</span><span class="w"> </span><span class="s">"/"</span><span class="w"> </span><span class="p">(</span><span class="no">:movies</span><span class="w"> </span><span class="n">data</span><span class="p">))</span><span class="w"> </span><span class="n">movie-line</span><span class="w"> </span><span class="n">movie-str</span><span class="p">)]</span><span class="w">
    </span><span class="p">(</span><span class="nf">atom</span><span class="w"> </span><span class="p">{</span><span class="no">:books</span><span class="w"> </span><span class="n">books</span><span class="w">
           </span><span class="no">:movies</span><span class="w"> </span><span class="n">movies</span><span class="p">})))</span><span class="w">

</span><span class="p">(</span><span class="nb">comment</span><span class="w">
  </span><span class="c1">;; AND HERE WE EXAMINE WHAT HAPPENED.</span><span class="w">
  </span><span class="p">(</span><span class="nb">count</span><span class="w"> </span><span class="n">mybooks</span><span class="p">)</span><span class="w">
  </span><span class="c1">;;=&gt; 35</span><span class="w">
  </span><span class="p">(</span><span class="nb">first</span><span class="w"> </span><span class="n">mybooks</span><span class="p">)</span><span class="w">
  </span><span class="c1">;;=&gt; {:id 2001,</span><span class="w">
  </span><span class="c1">;;    :product-group 1,</span><span class="w">
  </span><span class="c1">;;    :title "Kalevala",</span><span class="w">
  </span><span class="c1">;;    :price 3.95,</span><span class="w">
  </span><span class="c1">;;    :author "Elias Lönnrot",</span><span class="w">
  </span><span class="c1">;;    :year 1835,</span><span class="w">
  </span><span class="c1">;;    :country "Finland",</span><span class="w">
  </span><span class="c1">;;    :language "Finnish"}</span><span class="w">
</span></code></pre></div></div>

<p>I hardly ever use the <a href="#calva-debugger">Calva debugger</a>, since Clojure provides much better tools to examine your live program state. Nowadays instead of inline defs, I use <a href="#snitch">Snitch</a>.</p>

<h3 id="hashp">Hashp</h3>

<p>I used to use <a href="https://github.com/weavejester/hashp">Hashp</a> quite often in my debugging sessions, but nowadays more <a href="#snitch">Snitch</a>. But instead of adding a <code class="language-plaintext highlighter-rouge">prn</code> line in some let and see the REPL output, <code class="language-plaintext highlighter-rouge">hashp</code> is a good alternative.</p>

<h3 id="portal">Portal</h3>

<p>You can use <a href="https://github.com/djblue/portal">portal</a> in development to tap to various data. I have added a couple of examples how to tap to the data in files.</p>

<p>In the Clojure side, in <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/src/clj/backend/routes.clj">routes.clj</a>:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="c1">;; Example how to tap to the data using djblue Portal:</span><span class="w">
  </span><span class="p">(</span><span class="nf">require</span><span class="w"> </span><span class="o">'</span><span class="p">[</span><span class="n">clj-http.client</span><span class="w"> </span><span class="no">:as</span><span class="w"> </span><span class="n">client</span><span class="p">])</span><span class="w">
  </span><span class="p">(</span><span class="nf">require</span><span class="w"> </span><span class="o">'</span><span class="p">[</span><span class="n">jsonista.core</span><span class="w"> </span><span class="no">:as</span><span class="w"> </span><span class="n">json</span><span class="p">])</span><span class="w">
  </span><span class="p">(</span><span class="k">defn</span><span class="w"> </span><span class="n">json-to-edn</span><span class="w"> </span><span class="p">[</span><span class="n">json-str</span><span class="p">]</span><span class="w">
    </span><span class="p">(</span><span class="nf">json/read-value</span><span class="w"> </span><span class="n">json-str</span><span class="w"> </span><span class="p">(</span><span class="nf">json/object-mapper</span><span class="w"> </span><span class="p">{</span><span class="no">:decode-key-fn</span><span class="w"> </span><span class="nb">keyword</span><span class="p">})))</span><span class="w"> 
  </span><span class="p">(</span><span class="nf">json-to-edn</span><span class="w"> </span><span class="s">"{\"name\": \"Book\", \"price\": 29.99}"</span><span class="p">)</span><span class="w"> 
  
  </span><span class="p">(</span><span class="no">:body</span><span class="w"> </span><span class="p">(</span><span class="nf">client/get</span><span class="w"> </span><span class="s">"http://localhost:8331/api/products/books"</span><span class="p">))</span><span class="w">
  </span><span class="c1">;; Tap to the data:</span><span class="w">
  </span><span class="c1">; https://github.com/djblue/portal</span><span class="w">
  </span><span class="p">(</span><span class="nf">require</span><span class="w"> </span><span class="o">'</span><span class="p">[</span><span class="n">portal.api</span><span class="w"> </span><span class="no">:as</span><span class="w"> </span><span class="n">p</span><span class="p">])</span><span class="w">
  </span><span class="c1">; This should open the Portal window.</span><span class="w">
  </span><span class="p">(</span><span class="k">def</span><span class="w"> </span><span class="n">p</span><span class="w"> </span><span class="p">(</span><span class="nf">p/open</span><span class="p">))</span><span class="w">
  </span><span class="p">(</span><span class="nf">add-tap</span><span class="w"> </span><span class="o">#</span><span class="ss">'p/submit</span><span class="p">)</span><span class="w"> 
  </span><span class="p">(</span><span class="nf">tap&gt;</span><span class="w"> </span><span class="no">:hello</span><span class="p">)</span><span class="w">
  </span><span class="p">(</span><span class="nf">tap&gt;</span><span class="w"> </span><span class="p">(</span><span class="nf">json-to-edn</span><span class="w"> </span><span class="p">(</span><span class="no">:body</span><span class="w"> </span><span class="p">(</span><span class="nf">client/get</span><span class="w"> </span><span class="s">"http://localhost:8331/api/products/books"</span><span class="p">))))</span><span class="w">
  </span><span class="c1">;; You should now see a vector of book maps in the portal window.</span><span class="w">
</span></code></pre></div></div>

<p>In the Clojurescript side, in <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/src/cljs/frontend/app.cljs">app.cljs</a>:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="c1">;; Example how to tap to the data using djblue Portal: </span><span class="w">
  </span><span class="p">(</span><span class="nf">require</span><span class="w"> </span><span class="o">'</span><span class="p">[</span><span class="n">portal.web</span><span class="w"> </span><span class="no">:as</span><span class="w"> </span><span class="n">p</span><span class="p">])</span><span class="w">
  </span><span class="c1">; NOTE: This asks a popup window, you have to accept it in the browser!!!</span><span class="w">
  </span><span class="p">(</span><span class="k">def</span><span class="w"> </span><span class="n">p</span><span class="w"> </span><span class="p">(</span><span class="nf">p/open</span><span class="p">))</span><span class="w">
  </span><span class="c1">; Now you should have a new pop-up browser window...</span><span class="w">
  </span><span class="p">(</span><span class="nf">add-tap</span><span class="w"> </span><span class="o">#</span><span class="ss">'p/submit</span><span class="p">)</span><span class="w">
  </span><span class="p">(</span><span class="nf">tap&gt;</span><span class="w"> </span><span class="no">:hello</span><span class="p">)</span><span class="w">
  </span><span class="p">(</span><span class="nf">tap&gt;</span><span class="w"> </span><span class="p">(</span><span class="nf">get-in</span><span class="w"> </span><span class="o">@</span><span class="n">!state</span><span class="w"> </span><span class="p">[</span><span class="no">:db/data</span><span class="w"> </span><span class="no">:books</span><span class="p">]))</span><span class="w">
  </span><span class="c1">;; You should now see a vector of book maps in the portal window.</span><span class="w">
</span></code></pre></div></div>

<h3 id="gadget">Gadget</h3>

<p><a href="https://github.com/cjohansen/gadget-inspector">Gadget</a> is nowadays my main debugging tool with <a href="https://github.com/cjohansen/replicant">Replicant</a>. Gadget provides a very good view to your frontend state while developing the frontend.</p>

<p><img src="/img/2025-04-20-clojure-power-tools-part-3_img_2.png" alt="Gadget" /></p>

<p><em>Gadget.</em></p>

<h3 id="calva-debugger">Calva Debugger</h3>

<p>Calva provides a nice <a href="https://calva.io/debugger/">debugger</a>. As I already explained before, I very seldom use it. But now, I just used it to provide the example below, and I realized that it is actually quite a nice tool, and I should use it more in the future.</p>

<p><img src="/img/2025-04-20-clojure-power-tools-part-3_img_3.png" alt="Calva debugger" /></p>

<p><em>Calva debugger.</em></p>

<p>So, you just add the <code class="language-plaintext highlighter-rouge">#dbg</code> reader tag to your code and once your code execution goes to that point the debugger triggers.</p>

<h3 id="snitch">Snitch</h3>

<p>Peter Strömberg, the creator of Calva, once again introduced an excellent new tool to me: Snitch. I watched Peter’s excellent demo how he uses Snitch, and I immediately realized that I switch ad hoc inline  defs to Snitch. I recommend watching <a href="https://youtu.be/jb1BcYpyOAs">Peter’s video on how to use Snitch</a>.</p>

<p><a href="https://github.com/AbhinavOmprakash/snitch">Snitch</a> is a tool that adds inline Defs to your function.</p>

<p>I mostly use <em>defn*</em> which injects inline defs for all the bindings in the function: parameters and let bindings. If I want to examine what happens in the function, my workflow is like this: 1. Change: <em>def</em> =&gt; <em>def*</em>. 2. Integrant reset. 3. Call the API (or what ever, which finally calls the function). 4. Examine bindings in the function by evaluating them in the function context.</p>

<p>Add this to <code class="language-plaintext highlighter-rouge">user.clj</code></p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">;; https://github.com/AbhinavOmprakash/snitch</span><span class="w">
</span><span class="p">(</span><span class="nf">require</span><span class="w"> </span><span class="o">'</span><span class="p">[</span><span class="n">snitch.core</span><span class="w"> </span><span class="no">:refer</span><span class="w"> </span><span class="p">[</span><span class="n">defn*</span><span class="w"> </span><span class="n">defmethod*</span><span class="w"> </span><span class="n">*fn</span><span class="w"> </span><span class="n">*let</span><span class="p">]])</span><span class="w">
</span></code></pre></div></div>

<h2 id="bonus-tool-copilot">Bonus Tool: Copilot</h2>

<p>My corporation provides <a href="https://docs.github.com/en/copilot/about-github-copilot/what-is-github-copilot">GitHub Copilot Enterprise License</a>. Copilot is a great tool to assist you in programming. I am still a bit of old school programmer in that sense that I hardly ever let Copilot to do editing in the actual text files, but I mostly have a conversation with Copilot in the <a href="https://code.visualstudio.com/docs/copilot/chat/getting-started-chat">VSCode integrated Copilot Chat view</a>.</p>

<p>I have explained my Copilot use in this blog post: <a href="/programming/2025/02/01/copilot-keybindings.html">Copilot Keybindings</a>.</p>

<h2 id="conclusions">Conclusions</h2>

<p>Clojure is an excellent programming language. It has a rich ecosystem and tools that you just don’t have in other programming languages, due to the fact that other programming languages not being <a href="https://en.wikipedia.org/wiki/Homoiconicity">homoiconic</a> languages just can’t have e.g. a real REPL.</p>

<p><em>The writer is working at a major international IT corporation building cloud infrastructures and implementing applications on top of those infrastructures.</em></p>

<p>Kari Marttila</p>

<p><sub> Kari Marttila’s Home Page in LinkedIn: <a href="https://www.linkedin.com/in/karimarttila/">https://www.linkedin.com/in/karimarttila/</a> </sub></p>]]></content><author><name>Kari Marttila</name></author><category term="[&quot;clojure&quot;]" /><category term="programming" /><category term="vscode" /><category term="calva" /><category term="languages" /><category term="clojure" /><category term="clojurescript" /><category term="full-stack" /><category term="web" /><category term="repl" /><category term="productivity" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Designing UI components with Clojurescript, Replicant and Portfolio</title><link href="http://www.karimarttila.fi/clojurescript/2025/03/10/ui-design-with-clojurescript-replicant-and-portfolio.html" rel="alternate" type="text/html" title="Designing UI components with Clojurescript, Replicant and Portfolio" /><published>2025-03-10T00:00:00+00:00</published><updated>2025-03-10T00:00:00+00:00</updated><id>http://www.karimarttila.fi/clojurescript/2025/03/10/ui-design-with-clojurescript-replicant-and-portfolio</id><content type="html" xml:base="http://www.karimarttila.fi/clojurescript/2025/03/10/ui-design-with-clojurescript-replicant-and-portfolio.html"><![CDATA[<p><img src="/img/2025-03-10-ui-design-with-clojurescript-replicant-and-portfolio_img_1.png" alt="Portfolio view showing project components" /></p>

<p><em>Portfolio showing components of the application view, and the actual application, side by side.</em></p>

<h2 id="introduction">Introduction</h2>

<p>In my previous blog post <a href="/clojurescript/2025/02/28/clojurescript-with-replicant.html">Frontend Development with Clojurescript and Replicant</a> I wrote about implementing a UI using the excellent <a href="https://github.com/cjohansen/replicant">Replicant</a> library. In this new blog post I tell my experiences on how to use another Christian Johansen’s great library: <a href="https://github.com/cjohansen/portfolio">Portfolio</a>.</p>

<p>The exercise I explain in this blog post is in my Clojure Github repo in directory <a href="https://github.com/karimarttila/clojure/tree/master/webstore-demo/replicant-webstore">replicant-webstore</a>.</p>

<h2 id="my-current-frontend-stack">My Current Frontend Stack</h2>

<p>But let’s first introduce my current favorite frontend tools:</p>

<ul>
  <li><a href="https://clojurescript.org/">Clojurescript</a>. A functional language with immutable data structures that transpiles to Javascript (like e.g. Typescript does).</li>
  <li><a href="https://tailwindcss.com/">Tailwind CSS</a>. An easy way to create various CSS stuff. Integrates well with hiccup and Clojurescript.</li>
  <li><a href="https://github.com/weavejester/hiccup">Hiccup</a>. Hiccup provides a very succint and functional way to express HTML that transpiles to HTML code.</li>
  <li><a href="https://github.com/cjohansen/replicant">Replicant</a>. An excellent rendering library with central event handling and store.</li>
  <li><a href="https://github.com/cjohansen/gadget-inspector">Gadget</a>. Gadget provides a great view to your centralized store - the state of your store is basically your UI.</li>
  <li><a href="https://github.com/cjohansen/portfolio">Portfolio</a>. Using Portfolio you can create a catalog of your UI component straight from your production view code.</li>
</ul>

<p>All these tools work amazingly well together. Using Clojure(script) is such a joy to work with. You can use your favorite programming language both in the backend and frontend sides, and share code (like schemas) between the backend and the frontend. And the Clojure (Lisp) super tool - <a href="https://clojure.org/guides/repl/introduction">the REPL</a> - is available both in the backend and in the frontend development workflow.</p>

<h2 id="what-is-portfolio">What is Portfolio?</h2>

<p>The <a href="https://github.com/cjohansen/portfolio">Portfolio</a> repo says:</p>

<blockquote>
  <p>Portfolio brings some of the best features of <a href="https://github.com/storybookjs/storybook">Storybook.js</a> to ClojureScript, and adds a few of its own. While Storybook.js was its starting point, Portfolio does not aspire to feature-parity with it, and instead caters to the REPL-oriented ClojureScript development process.</p>
</blockquote>

<p>The way I use Portfolio, is to setup a dummy replicant store and event handler for Portfolio, require the production view namespace in the portfolio namespace and tell Portfolio to show my UI components in the Portfolio view (see the picture at the beginning of this blog post).</p>

<p>I don’t explain Portfolio here any more than that. I encourage you to watch the <a href="https://www.youtube.com/watch?v=25JDQRFoQ_U">Portfolio: A “visual REPL” for UI Component development (by Christian Johansen) - a presentation at the London Clojurians</a>.</p>

<h2 id="show-me-the-code">Show Me the Code</h2>

<p>Let’s have a short example. I have various views in my frontend side. All these views are pure - they do not have any local state. The views only return hiccup based on the input arguments they receive. In this app the centralized application state will be supplied as the input argument. Whenever the state changes we tell Replicant to evaluate our views and render the resulting hiccup to the DOM.</p>

<p>An example of the <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/src/cljs/frontend/views.cljc">views.cljc</a> (note: this is a <code class="language-plaintext highlighter-rouge">cljc</code> file - (c for “common”) - it can be used to render the view in the frontend Clojurescript side, and it can e.g. be tested in the backend Clojure (JVM) side - since there are no Javascript dependencies, and no local state):</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defn-</span><span class="w"> </span><span class="n">show-error</span><span class="w"> </span><span class="p">[</span><span class="n">msg</span><span class="w"> </span><span class="n">button?</span><span class="w"> </span><span class="n">dissoc-key</span><span class="p">]</span><span class="w">
  </span><span class="p">[</span><span class="no">:div.inline-block.bg-red-50.border.border-red-500.rounded.px-4.py-3</span><span class="w"> </span><span class="p">{</span><span class="no">:role</span><span class="w"> </span><span class="s">"alert"</span><span class="w"> </span><span class="no">:style</span><span class="w"> </span><span class="p">{</span><span class="no">:max-width</span><span class="w"> </span><span class="s">"fit-content"</span><span class="p">}}</span><span class="w">
   </span><span class="p">[</span><span class="no">:div.flex.items-center</span><span class="w">
    </span><span class="p">[</span><span class="no">:p.font-bold.text-red-700</span><span class="w"> </span><span class="n">msg</span><span class="p">]</span><span class="w">
    </span><span class="p">(</span><span class="nb">when</span><span class="w"> </span><span class="n">button?</span><span class="w">
      </span><span class="p">[</span><span class="no">:button.text-xs.px-2.py-1.ml-4.rounded.bg-red-50.hover:bg-gray-300.cursor-pointer.border.border-gray-400</span><span class="w">
       </span><span class="p">{</span><span class="no">:on</span><span class="w"> </span><span class="p">{</span><span class="no">:click</span><span class="w"> </span><span class="p">[[</span><span class="no">:db/dissoc</span><span class="w"> </span><span class="n">dissoc-key</span><span class="p">]]}}</span><span class="w">
       </span><span class="s">"X"</span><span class="p">])]])</span><span class="w">

</span><span class="p">(</span><span class="k">defn-</span><span class="w"> </span><span class="n">show-info</span><span class="w"> </span><span class="p">[</span><span class="n">msg</span><span class="w"> </span><span class="n">button?</span><span class="w"> </span><span class="n">dissoc-key</span><span class="p">]</span><span class="w">
  </span><span class="p">[</span><span class="no">:div.inline-block.bg-blue-50.border.border-blue-500.rounded.px-4.py-3</span><span class="w"> </span><span class="p">{</span><span class="no">:role</span><span class="w"> </span><span class="s">"alert"</span><span class="w"> </span><span class="no">:style</span><span class="w"> </span><span class="p">{</span><span class="no">:max-width</span><span class="w"> </span><span class="s">"fit-content"</span><span class="p">}}</span><span class="w">
   </span><span class="p">[</span><span class="no">:div.flex.items-center</span><span class="w">
    </span><span class="p">[</span><span class="no">:p.font-bold.text-blue-700</span><span class="w"> </span><span class="n">msg</span><span class="p">]</span><span class="w">
    </span><span class="p">(</span><span class="nb">when</span><span class="w"> </span><span class="n">button?</span><span class="w">
      </span><span class="p">[</span><span class="no">:button.text-xs.px-2.py-1.ml-4.rounded.bg-blue-50.hover:bg-gray-300.cursor-pointer.border.border-gray-400</span><span class="w">
       </span><span class="p">{</span><span class="no">:on</span><span class="w"> </span><span class="p">{</span><span class="no">:click</span><span class="w"> </span><span class="p">[[</span><span class="no">:db/dissoc</span><span class="w"> </span><span class="n">dissoc-key</span><span class="p">]]}}</span><span class="w">
       </span><span class="s">"X"</span><span class="p">])]])</span><span class="w">
</span></code></pre></div></div>

<p>Next, the Portfolio code in <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/portfolio/frontend/scenes.cljs">scenes.clj</a>:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nf">ns</span><span class="w"> </span><span class="n">frontend.scenes</span><span class="w">
  </span><span class="p">(</span><span class="no">:require</span><span class="w"> </span><span class="p">[</span><span class="n">portfolio.replicant</span><span class="w"> </span><span class="no">:refer-macros</span><span class="w"> </span><span class="p">[</span><span class="n">defscene</span><span class="p">]]</span><span class="w">
            </span><span class="p">[</span><span class="n">portfolio.ui</span><span class="w"> </span><span class="no">:as</span><span class="w"> </span><span class="n">portfolio</span><span class="p">]</span><span class="w">
            </span><span class="p">[</span><span class="n">frontend.views</span><span class="w"> </span><span class="no">:as</span><span class="w"> </span><span class="n">f-views</span><span class="p">]</span><span class="w">
            </span><span class="p">[</span><span class="n">frontend.replicantutil</span><span class="w"> </span><span class="no">:as</span><span class="w"> </span><span class="n">f-rutil</span><span class="p">]))</span><span class="w">

</span><span class="p">(</span><span class="nf">defscene</span><span class="w"> </span><span class="n">show-info</span><span class="w">
  </span><span class="p">(</span><span class="nf">f-views/show-info</span><span class="w"> </span><span class="s">"Hello info from Portfolio!"</span><span class="w"> </span><span class="n">true</span><span class="w"> </span><span class="no">:db/dummy</span><span class="p">))</span><span class="w">

</span><span class="p">(</span><span class="nf">defscene</span><span class="w"> </span><span class="n">show-error</span><span class="w">
  </span><span class="p">(</span><span class="nf">f-views/show-error</span><span class="w"> </span><span class="s">"Hello error from Portfolio!"</span><span class="w"> </span><span class="n">true</span><span class="w"> </span><span class="no">:db/dummy</span><span class="p">))</span><span class="w">

</span><span class="p">(</span><span class="k">defn</span><span class="w"> </span><span class="n">main</span><span class="w"> </span><span class="p">[]</span><span class="w">
  </span><span class="p">(</span><span class="nf">portfolio/start!</span><span class="w">
   </span><span class="p">{</span><span class="no">:config</span><span class="w">
    </span><span class="p">{</span><span class="no">:css-paths</span><span class="w"> </span><span class="p">[</span><span class="s">"/css/main.css"</span><span class="p">]</span><span class="w">
     </span><span class="no">:viewport/defaults</span><span class="w">
     </span><span class="p">{</span><span class="no">:background/background-color</span><span class="w"> </span><span class="s">"#fdeddd"</span><span class="p">}}}))</span><span class="w">
</span></code></pre></div></div>

<p>It’s as simple as that. And since using Replicant, you design your UI pure without local state, <em>it is very convenient to develop the UI components in isolation using Portfolio</em>.</p>

<p><strong>Some auxiliary configuration.</strong></p>

<p>Since my UI components use Replicant and expect an event handler (see: <code class="language-plaintext highlighter-rouge">{:on {:click [[:db/dissoc dissoc-key]]}}</code>), you have to setup a dummy store and event handler. Not difficult. You can just copy-paste your production store and event handler and strip almost everything away. This is how I did it in <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/portfolio/frontend/replicantutil.cljs">replicantutil.cljs</a>.</p>

<p>And then, of course, you need to setup the build and the routing for your Portfolio, <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/shadow-cljs.edn">shadow-cljs.edn</a>:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">          </span><span class="no">:portfolio</span><span class="w">
          </span><span class="p">{</span><span class="no">:target</span><span class="w"> </span><span class="no">:browser</span><span class="w">
           </span><span class="no">:modules</span><span class="w"> </span><span class="p">{</span><span class="no">:main</span><span class="w"> </span><span class="p">{</span><span class="no">:init-fn</span><span class="w"> </span><span class="n">frontend.scenes/main</span><span class="p">}}</span><span class="w">
           </span><span class="no">:dev</span><span class="w"> </span><span class="p">{</span><span class="no">:output-dir</span><span class="w"> </span><span class="s">"target/dev/public/portfolio"</span><span class="p">}}</span><span class="w">
</span></code></pre></div></div>

<p>… and routing <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/src/clj/backend/routes.clj">routes.clj</a>:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defn</span><span class="w"> </span><span class="n">portfolio-js-file</span><span class="w"> </span><span class="p">[]</span><span class="w">
  </span><span class="p">(</span><span class="nb">-&gt;</span><span class="w"> </span><span class="p">(</span><span class="nf">io/file</span><span class="w"> </span><span class="s">"target/dev/public/portfolio/manifest.edn"</span><span class="p">)</span><span class="w">
      </span><span class="nb">slurp</span><span class="w">
      </span><span class="n">edn/read-string</span><span class="w">
      </span><span class="nb">first</span><span class="w">
      </span><span class="no">:output-name</span><span class="p">))</span><span class="w">

</span><span class="c1">;; Open portfolio in http://localhost:9333/portfolio/index</span><span class="w">
</span><span class="p">(</span><span class="k">defn</span><span class="w"> </span><span class="n">portfolio</span><span class="w"> </span><span class="p">[]</span><span class="w">
  </span><span class="p">(</span><span class="nf">hiccup/html</span><span class="w"> </span><span class="p">{</span><span class="no">:mode</span><span class="w"> </span><span class="no">:html</span><span class="p">}</span><span class="w">
               </span><span class="p">(</span><span class="nf">hiccup/raw</span><span class="w"> </span><span class="s">"&lt;!DOCTYPE html&gt;\n"</span><span class="p">)</span><span class="w">
               </span><span class="p">[</span><span class="no">:html</span><span class="w">
                </span><span class="p">{</span><span class="no">:lang</span><span class="w"> </span><span class="s">"en"</span><span class="p">}</span><span class="w">
                </span><span class="p">[</span><span class="no">:head</span><span class="w">
                 </span><span class="p">[</span><span class="no">:title</span><span class="w"> </span><span class="s">"Portfolio Design Area"</span><span class="p">]</span><span class="w">
                 </span><span class="p">[</span><span class="no">:meta</span><span class="w"> </span><span class="p">{</span><span class="no">:charset</span><span class="w"> </span><span class="s">"utf-8"</span><span class="p">}]</span><span class="w">
                 </span><span class="p">[</span><span class="no">:link</span><span class="w"> </span><span class="p">{</span><span class="no">:rel</span><span class="w"> </span><span class="s">"icon"</span><span class="w"> </span><span class="no">:href</span><span class="w"> </span><span class="s">"/assets/favicon.ico"</span><span class="w"> </span><span class="no">:type</span><span class="w"> </span><span class="s">"image/x-icon"</span><span class="p">}]]</span><span class="w">
                </span><span class="p">[</span><span class="no">:body</span><span class="w">
                 </span><span class="p">[</span><span class="no">:div#app</span><span class="p">]</span><span class="w">
                 </span><span class="p">[</span><span class="no">:script</span><span class="w"> </span><span class="p">{</span><span class="no">:type</span><span class="w"> </span><span class="s">"text/javascript"</span><span class="w"> </span><span class="no">:src</span><span class="w"> </span><span class="p">(</span><span class="nb">str</span><span class="w"> </span><span class="s">"/portfolio/"</span><span class="w"> </span><span class="p">(</span><span class="nf">portfolio-js-file</span><span class="p">))}]]]))</span><span class="w">

</span><span class="p">(</span><span class="k">defn</span><span class="w"> </span><span class="n">app</span><span class="w"> </span><span class="p">[</span><span class="n">env</span><span class="p">]</span><span class="w">
  </span><span class="p">(</span><span class="nf">ring/ring-handler</span><span class="w">
   </span><span class="p">(</span><span class="nf">ring/router</span><span class="w">
    </span><span class="p">[</span><span class="s">""</span><span class="w">
     </span><span class="p">[</span><span class="s">"/portfolio"</span><span class="w">
      </span><span class="p">[</span><span class="s">"/index"</span><span class="w">
       </span><span class="p">[</span><span class="s">""</span><span class="w">
        </span><span class="p">{</span><span class="no">:get</span><span class="w"> </span><span class="p">{</span><span class="no">:handler</span><span class="w"> </span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="p">[</span><span class="n">_req</span><span class="p">]</span><span class="w"> </span><span class="p">(</span><span class="nf">resp/ok</span><span class="w"> </span><span class="p">(</span><span class="nb">str</span><span class="w"> </span><span class="p">(</span><span class="nf">portfolio</span><span class="p">))))}}]]]</span><span class="w">
     </span><span class="p">[</span><span class="s">"/api"</span><span class="w">
       </span><span class="c1">;; ...</span><span class="w">
</span></code></pre></div></div>

<p>So, you can develop your UI components in isolation using Portfolio. Another great way of using Portfolio, is to create a portfolio, a catalog of your UI components, and the Portfolio view can be a playground for your UI designers and testers to examine e.g. the CSS styles, and internationalization of your components. Watch the <a href="https://www.youtube.com/watch?v=25JDQRFoQ_U">Portfolio: A “visual REPL” for UI Component development (by Christian Johansen)</a> presentation - it provides great examples on how Christian Johansen has been using Portfolio himself.</p>

<h2 id="conclusions">Conclusions</h2>

<p>Portfolio and Gadget are two extraodinary tools to be used with Replicant. I encourage you to try Replicant, and Portfolio and Gadget with Replicant. It might change the way you think about building UI - at least it changed my way of thinking on how to build UIs - in a simpler way.</p>

<p><em>The writer is working at a major international IT corporation building cloud infrastructures and implementing applications on top of those infrastructures.</em></p>

<p>Kari Marttila</p>

<p><sub> Kari Marttila’s Home Page in LinkedIn: <a href="https://www.linkedin.com/in/karimarttila/">https://www.linkedin.com/in/karimarttila/</a> </sub></p>]]></content><author><name>Kari Marttila</name></author><category term="[&quot;clojurescript&quot;]" /><category term="programming" /><category term="vscode" /><category term="calva" /><category term="languages" /><category term="clojure" /><category term="clojurescript" /><category term="full-stack" /><category term="web" /><category term="repl" /><category term="replicant" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Frontend Development with Clojurescript and Replicant</title><link href="http://www.karimarttila.fi/clojurescript/2025/02/28/clojurescript-with-replicant.html" rel="alternate" type="text/html" title="Frontend Development with Clojurescript and Replicant" /><published>2025-02-28T00:00:00+00:00</published><updated>2025-02-28T00:00:00+00:00</updated><id>http://www.karimarttila.fi/clojurescript/2025/02/28/clojurescript-with-replicant</id><content type="html" xml:base="http://www.karimarttila.fi/clojurescript/2025/02/28/clojurescript-with-replicant.html"><![CDATA[<p><img src="/img/2025-02-28-clojurescript-with-replicant_img_1.png" alt="Web app implemented using Replicant" /></p>

<p><em>Web app exercise implemented using Replicant, and showing the Gadget.</em></p>

<h2 id="introduction">Introduction</h2>

<p>In my previous blog post <a href="/clojure/2020/10/15/clojure-re-frame-exercise.html">Clojure Re-Frame Exercise</a> I wrote about implementing a UI using the <a href="https://reagent-project.github.io/">Reagent</a> Clojurescript React wrapper and the <a href="https://day8.github.io/re-frame/re-frame/">re-frame</a> framework for managing state changes in your application. In this new blog post I tell my experiences using the <a href="https://github.com/cjohansen/replicant">Replicant</a> library.</p>

<p>The exercise I explain in this blog post is in my Clojure Github repo in directory <a href="https://github.com/karimarttila/clojure/tree/master/webstore-demo/replicant-webstore">replicant-webstore</a>.</p>

<h2 id="what-is-replicant">What is Replicant?</h2>

<p>The <a href="https://github.com/cjohansen/replicant">Replicant</a> repo says:</p>

<blockquote>
  <p>Replicant is a data-driven rendering library for Clojure(Script). It renders hiccup to strings or DOM nodes. Over and over. Efficiently, without a single dependency.</p>
</blockquote>

<p>Replicant is a data-driven rendering library for Clojurescript (and Clojure) that transforms <a href="https://github.com/weavejester/hiccup">Hiccup</a> syntax into DOM nodes. It focuses solely on rendering, making it a lightweight and efficient choice for building user interfaces. So, Replicant is not a React wrapper, but just a rendering library. It is very light and easy to use. You probably learn Replicant in a couple of days while creating some demo app with it, like I did.</p>

<p>Good pointers for learning Replicant are:</p>

<ul>
  <li><a href="https://github.com/cjohansen/replicant">Replicant Github repo</a></li>
  <li><a href="https://replicant.fun/learn/">Replicant User Guides</a></li>
  <li><a href="https://replicant.fun/in-the-wild/">Various Replicant Examples</a></li>
  <li><a href="https://cljdoc.org/d/no.cjohansen/replicant/2025.02.02/doc/readme">Reference API Docs, v. 2025-02.02</a> (I didn’t need this document while implementing my Replicant exercise)</li>
</ul>

<p>If someone asked me to list pros and cons using Replicant, I would provide this list:</p>

<p><strong>Pros:</strong></p>

<ul>
  <li>Very light, no dependencies.</li>
  <li>Extremely simple to learn and use.</li>
  <li>The event handling and top-down rendering model is easy to start with but also scales for bigger projects.</li>
  <li>Using hiccup makes UI code very lean.</li>
  <li>Everything is just data.</li>
  <li>The state paradigm is great. You register your event handler to Replicant. You update your store via your event handler. Whenever the app state changes, you ask Replicant to re-render the changed parts of your UI.</li>
  <li>Reagent + re-frame feels more complex and clumsy after using Replicant.</li>
  <li>You don’t have to worry about various React related cryptic error messages while implementing your UI (which were for me as an old backend programmer always a bit difficult).</li>
  <li>Replicant is just data. No local state. No network connections in components. Therefore, you are forced to design your frontend application architecture so, that it works around passing data events to your event handler, changing the application store, and then asking Replicant to re-render all the hiccup again, and trust Replicant does this efficiently.</li>
  <li>Since your UI = the current state of your application store, it is easy to grasp what is happening, e.g. using the <code class="language-plaintext highlighter-rouge">Gadget</code> to see the data in your application store (more about Gadget later).</li>
</ul>

<p><strong>Cons:</strong></p>

<ul>
  <li>It’s not React. So, if you need some more complex component like a filterable and sortable html table with navigation buttons (like <a href="https://tanstack.com/table/latest">Tanstack Table</a>), you either need to implement it yourself, or find the solution somewhere. I do not find this as a big issue, since while I was using Reagent, I actually very seldom needed to import any off-the-shelf React components. So, if you are not using React components, why to carry the burden of React with you when implementing simple UIs?</li>
</ul>

<h2 id="example-projects">Example Projects</h2>

<p>I used mostly these example projects in my exercise:</p>

<ul>
  <li><a href="https://github.com/metosin/example-project">metosin-example-project</a>. This example project shows how to use <a href="https://github.com/babashka/babashka">babashka</a> as a command runner (I have previously used mostly <a href="https://github.com/casey/just">just</a>). I also used a lot of configurations of this example as a basis for my exercise. This example project provides also a good example on how to use <a href="https://github.com/metosin/reitit">reitit</a>, middleware, <a href="https://github.com/metosin/malli">malli</a>, etc. in the backend side.</li>
  <li><a href="https://github.com/anteoas/replicant-todomvc">replicant-todomvc</a>: Interesting repo to study the event handling in replicant.</li>
  <li><a href="https://github.com/anteoas/replicant-mini-app">replicant-mini-app</a>: I took from this solution the basic setup on how to do the replicant event processing.</li>
  <li><a href="https://github.com/cjohansen/replicant">replicant-networking</a>: How to do networking with replicant.</li>
</ul>

<h2 id="using-replicant">Using Replicant</h2>

<h3 id="rendering">Rendering</h3>

<p>In this exercise app I create the UI using hiccup to reflect the application state. In this app the rendering happens when something changes in application store (by the event handler). That’s basically it. See an example of the rendering in the next chapter. Other applications might do rendering otherwise - Replicant do not dictate how you do your rendering, it just provides the rendering functionality.</p>

<p>An example how you can render your web pages regarding the application state (<a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/src/cljs/frontend/views.cljc">views.cljc</a>):</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defn-</span><span class="w"> </span><span class="n">page-content</span><span class="w"> </span><span class="p">[</span><span class="n">state</span><span class="p">]</span><span class="w">
  </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">page</span><span class="w"> </span><span class="p">(</span><span class="no">:page/navigated</span><span class="w"> </span><span class="n">state</span><span class="p">)]</span><span class="w">
    </span><span class="p">(</span><span class="nf">case</span><span class="w"> </span><span class="p">(</span><span class="no">:page</span><span class="w"> </span><span class="n">page</span><span class="p">)</span><span class="w">
      </span><span class="no">:home</span><span class="w">
      </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">status</span><span class="w"> </span><span class="p">(</span><span class="no">:db/product-created</span><span class="w"> </span><span class="n">state</span><span class="p">)</span><span class="w">
            </span><span class="n">error</span><span class="w"> </span><span class="p">(</span><span class="no">:error</span><span class="w"> </span><span class="n">status</span><span class="p">)]</span><span class="w">
        </span><span class="p">(</span><span class="k">if</span><span class="w"> </span><span class="n">error</span><span class="w">
          </span><span class="p">[</span><span class="no">:div</span><span class="w">
           </span><span class="p">(</span><span class="nf">show-error</span><span class="w"> </span><span class="s">"Failed to create product!"</span><span class="w"> </span><span class="n">true</span><span class="w"> </span><span class="no">:db/product-created</span><span class="p">)]</span><span class="w">
          </span><span class="p">(</span><span class="nb">when</span><span class="w"> </span><span class="n">status</span><span class="w">
            </span><span class="p">[</span><span class="no">:div</span><span class="w">
             </span><span class="p">(</span><span class="nf">show-info</span><span class="w"> </span><span class="s">"New product created!"</span><span class="w"> </span><span class="n">true</span><span class="w"> </span><span class="no">:db/product-created</span><span class="p">)])))</span><span class="w">
      </span><span class="no">:products</span><span class="w">
      </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">table</span><span class="w"> </span><span class="p">(</span><span class="nf">products-table</span><span class="w"> </span><span class="p">(</span><span class="nf">get-in</span><span class="w"> </span><span class="n">state</span><span class="w"> </span><span class="p">[</span><span class="no">:db/data</span><span class="w"> </span><span class="p">(</span><span class="no">:pg</span><span class="w"> </span><span class="n">page</span><span class="p">)])</span><span class="w"> </span><span class="p">(</span><span class="no">:pg</span><span class="w"> </span><span class="n">page</span><span class="p">))]</span><span class="w">
        </span><span class="n">table</span><span class="p">)</span><span class="w">
      </span><span class="no">:product</span><span class="w">
      </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">id</span><span class="w"> </span><span class="p">(</span><span class="no">:id</span><span class="w"> </span><span class="n">page</span><span class="p">)</span><span class="w">
            </span><span class="n">product</span><span class="w"> </span><span class="p">(</span><span class="nf">find-item-by-id</span><span class="w"> </span><span class="p">(</span><span class="nf">get-in</span><span class="w"> </span><span class="n">state</span><span class="w"> </span><span class="p">[</span><span class="no">:db/data</span><span class="w"> </span><span class="p">(</span><span class="no">:pg</span><span class="w"> </span><span class="n">page</span><span class="p">)])</span><span class="w"> </span><span class="n">id</span><span class="p">)</span><span class="w">
            </span><span class="n">table</span><span class="w"> </span><span class="p">(</span><span class="nf">case</span><span class="w"> </span><span class="p">(</span><span class="no">:pg</span><span class="w"> </span><span class="n">page</span><span class="p">)</span><span class="w">
                    </span><span class="no">:books</span><span class="w"> </span><span class="p">(</span><span class="nf">book-details</span><span class="w"> </span><span class="n">product</span><span class="p">)</span><span class="w">
                    </span><span class="no">:movies</span><span class="w"> </span><span class="p">(</span><span class="nf">movie-details</span><span class="w"> </span><span class="n">product</span><span class="p">)</span><span class="w">
                    </span><span class="p">[</span><span class="no">:div</span><span class="p">])]</span><span class="w">
        </span><span class="n">table</span><span class="p">)</span><span class="w">
      </span><span class="no">:new</span><span class="w">
      </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">table</span><span class="w"> </span><span class="p">(</span><span class="nf">case</span><span class="w"> </span><span class="p">(</span><span class="no">:pg</span><span class="w"> </span><span class="n">page</span><span class="p">)</span><span class="w">
                    </span><span class="no">:books</span><span class="w"> </span><span class="p">(</span><span class="nf">new-product</span><span class="w"> </span><span class="n">state</span><span class="w"> </span><span class="no">:books</span><span class="w"> </span><span class="p">[</span><span class="s">"Title"</span><span class="w"> </span><span class="s">"Author"</span><span class="w"> </span><span class="s">"Year"</span><span class="w"> </span><span class="s">"Country"</span><span class="w"> </span><span class="s">"Language"</span><span class="w"> </span><span class="s">"Price"</span><span class="p">])</span><span class="w">
                    </span><span class="no">:movies</span><span class="w"> </span><span class="p">(</span><span class="nf">new-product</span><span class="w"> </span><span class="n">state</span><span class="w"> </span><span class="no">:movies</span><span class="w"> </span><span class="p">[</span><span class="s">"Title"</span><span class="w"> </span><span class="s">"Director"</span><span class="w"> </span><span class="s">"Year"</span><span class="w"> </span><span class="s">"Country"</span><span class="w"> </span><span class="s">"Genre"</span><span class="w"> </span><span class="s">"Price"</span><span class="p">])</span><span class="w">
                    </span><span class="p">[</span><span class="no">:div</span><span class="p">])]</span><span class="w">
        </span><span class="n">table</span><span class="p">)</span><span class="w">
      </span><span class="p">[</span><span class="no">:div</span><span class="p">])))</span><span class="w">
</span></code></pre></div></div>

<p>I.e., if the state says we are in the <code class="language-plaintext highlighter-rouge">:products</code> page, render it (and show the content in the picture at the beginning of this blog article, note also the Gadget showing that the state says that the <code class="language-plaintext highlighter-rouge">:page/navigated</code> is <code class="language-plaintext highlighter-rouge">:products</code> and specifically product groups is <code class="language-plaintext highlighter-rouge">:books</code>).</p>

<p>Replicant is just a rendering library. Read more in the Replicant documentation about the <a href="https://replicant.fun/top-down/">Top-down rendering model</a>.</p>

<h3 id="application-store-and-state">Application Store and State</h3>

<p>With Replicant, you can use a data model in which your UI rendering reflects the state of your application store (see example in the previous chapter “Rendering”). When something changes in the application store, you can ask in your event handler Replicant to re-render your UI related to those parts that have changed in UI.</p>

<p>I guess there are several ways to setup the application store. I used the simple way used in the <a href="https://github.com/anteoas/replicant-mini-app">replicant-mini-app</a> - the application store is just a Clojure atom <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/src/cljs/frontend/app.cljs">app.cljs</a>:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nf">defonce</span><span class="w"> </span><span class="o">^</span><span class="no">:private</span><span class="w"> </span><span class="n">!state</span><span class="w"> </span><span class="p">(</span><span class="nf">atom</span><span class="w"> </span><span class="n">...</span><span class="w">
</span></code></pre></div></div>

<p>… and therefore the state of the application is the state of the atom in a point of time.</p>

<p>And at the end of the event handler, when we have processed the events, we finally render the application</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defn-</span><span class="w"> </span><span class="n">event-handler</span><span class="w"> </span><span class="p">[{</span><span class="no">:replicant/keys</span><span class="w"> </span><span class="p">[</span><span class="o">^</span><span class="n">js</span><span class="w"> </span><span class="n">js-event</span><span class="p">]</span><span class="w"> </span><span class="no">:as</span><span class="w"> </span><span class="n">replicant-data</span><span class="p">}</span><span class="w"> </span><span class="n">actions</span><span class="p">]</span><span class="w">
  </span><span class="p">(</span><span class="nb">doseq</span><span class="w"> </span><span class="p">[</span><span class="n">action</span><span class="w"> </span><span class="n">actions</span><span class="p">]</span><span class="w">
    </span><span class="c1">; ...</span><span class="w">
  </span><span class="c1">; ...  </span><span class="w">
  </span><span class="p">(</span><span class="nf">render!</span><span class="w"> </span><span class="o">@</span><span class="n">!state</span><span class="p">))</span><span class="w">

</span><span class="p">(</span><span class="k">defn-</span><span class="w"> </span><span class="n">render!</span><span class="w"> </span><span class="p">[</span><span class="n">state</span><span class="p">]</span><span class="w">
  </span><span class="p">(</span><span class="nf">r/render</span><span class="w">
   </span><span class="p">(</span><span class="nf">js/document.getElementById</span><span class="w"> </span><span class="s">"app"</span><span class="p">)</span><span class="w">
   </span><span class="p">(</span><span class="nf">view</span><span class="w"> </span><span class="n">state</span><span class="p">)))</span><span class="w">   </span><span class="c1">; =&gt; Here you can build your UI view related to the state.</span><span class="w">

</span></code></pre></div></div>

<p>You can register your store to the Gadget to see a nice view of your app state in the Gadget Developer Tool:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defn</span><span class="w"> </span><span class="o">^</span><span class="no">:export</span><span class="w"> </span><span class="n">init!</span><span class="w"> </span><span class="p">[]</span><span class="w">
  </span><span class="p">(</span><span class="nf">inspector/inspect</span><span class="w"> </span><span class="s">"App state"</span><span class="w"> </span><span class="n">!state</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>But more about the Gadget in the Development Tooling chapter.</p>

<h3 id="event-handler">Event Handler</h3>

<p>The event handler is the place where all updates happen <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/src/cljs/frontend/app.cljs">app.cljs</a>:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defn-</span><span class="w"> </span><span class="n">event-handler</span><span class="w"> </span><span class="p">[{</span><span class="no">:replicant/keys</span><span class="w"> </span><span class="p">[</span><span class="o">^</span><span class="n">js</span><span class="w"> </span><span class="n">js-event</span><span class="p">]</span><span class="w"> </span><span class="no">:as</span><span class="w"> </span><span class="n">replicant-data</span><span class="p">}</span><span class="w"> </span><span class="n">actions</span><span class="p">]</span><span class="w">
  </span><span class="p">(</span><span class="nb">doseq</span><span class="w"> </span><span class="p">[</span><span class="n">action</span><span class="w"> </span><span class="n">actions</span><span class="p">]</span><span class="w">
    </span><span class="c1">; ...</span><span class="w">
      </span><span class="p">(</span><span class="nf">case</span><span class="w"> </span><span class="n">action-name</span><span class="w">
        </span><span class="no">:dom/prevent-default</span><span class="w"> </span><span class="p">(</span><span class="nf">.preventDefault</span><span class="w"> </span><span class="n">js-event</span><span class="p">)</span><span class="w">
        </span><span class="no">:db/assoc</span><span class="w"> </span><span class="p">(</span><span class="nb">apply</span><span class="w"> </span><span class="n">swap!</span><span class="w"> </span><span class="n">!state</span><span class="w"> </span><span class="nb">assoc</span><span class="w"> </span><span class="n">args</span><span class="p">)</span><span class="w">
        </span><span class="no">:db/assoc-in</span><span class="w"> </span><span class="p">(</span><span class="nb">apply</span><span class="w"> </span><span class="n">swap!</span><span class="w"> </span><span class="n">!state</span><span class="w"> </span><span class="n">assoc-in</span><span class="w"> </span><span class="n">args</span><span class="p">)</span><span class="w">
        </span><span class="no">:db/dissoc</span><span class="w"> </span><span class="p">(</span><span class="nb">apply</span><span class="w"> </span><span class="n">swap!</span><span class="w"> </span><span class="n">!state</span><span class="w"> </span><span class="nb">dissoc</span><span class="w"> </span><span class="n">args</span><span class="p">)</span><span class="w">
        </span><span class="no">:dom/set-input-text</span><span class="w"> </span><span class="p">(</span><span class="nf">set!</span><span class="w"> </span><span class="p">(</span><span class="nf">.-value</span><span class="w"> </span><span class="p">(</span><span class="nb">first</span><span class="w"> </span><span class="n">args</span><span class="p">))</span><span class="w"> </span><span class="p">(</span><span class="nb">second</span><span class="w"> </span><span class="n">args</span><span class="p">))</span><span class="w">
        </span><span class="no">:dom/focus-element</span><span class="w"> </span><span class="p">(</span><span class="nf">.focus</span><span class="w"> </span><span class="p">(</span><span class="nb">first</span><span class="w"> </span><span class="n">args</span><span class="p">))</span><span class="w">
        </span><span class="no">:backend/fetch</span><span class="w"> </span><span class="p">(</span><span class="nf">f-http/fetch</span><span class="w"> </span><span class="p">(</span><span class="nf">get-dispatcher</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nb">second</span><span class="w"> </span><span class="n">enriched-action</span><span class="p">))</span><span class="w">
        </span><span class="no">:backend/post</span><span class="w"> </span><span class="p">(</span><span class="nf">f-http/post</span><span class="w"> </span><span class="p">(</span><span class="nf">get-dispatcher</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nb">second</span><span class="w"> </span><span class="n">enriched-action</span><span class="p">))</span><span class="w">
        </span><span class="no">:route/home</span><span class="w"> </span><span class="p">(</span><span class="nf">navigated-home-page</span><span class="p">)</span><span class="w">
        </span><span class="no">:route/products</span><span class="w"> </span><span class="p">(</span><span class="nf">navigated-products-page</span><span class="w"> </span><span class="p">(</span><span class="nb">assoc</span><span class="w"> </span><span class="p">(</span><span class="nb">second</span><span class="w"> </span><span class="n">enriched-action</span><span class="p">)</span><span class="w"> </span><span class="no">:state</span><span class="w"> </span><span class="o">@</span><span class="n">!state</span><span class="p">))</span><span class="w">
        </span><span class="no">:route/product</span><span class="w"> </span><span class="p">(</span><span class="nf">navigated-product-page</span><span class="w"> </span><span class="p">(</span><span class="nb">assoc</span><span class="w"> </span><span class="p">(</span><span class="nb">second</span><span class="w"> </span><span class="n">enriched-action</span><span class="p">)</span><span class="w"> </span><span class="no">:state</span><span class="w"> </span><span class="o">@</span><span class="n">!state</span><span class="p">))</span><span class="w">
        </span><span class="no">:route/new</span><span class="w"> </span><span class="p">(</span><span class="nf">navigated-new-product-page</span><span class="w"> </span><span class="p">(</span><span class="nb">assoc</span><span class="w"> </span><span class="p">(</span><span class="nb">second</span><span class="w"> </span><span class="n">enriched-action</span><span class="p">)</span><span class="w"> </span><span class="no">:state</span><span class="w"> </span><span class="o">@</span><span class="n">!state</span><span class="p">))</span><span class="w">
        </span><span class="no">:action/new</span><span class="w"> </span><span class="p">(</span><span class="nf">action-new-product</span><span class="w"> </span><span class="p">(</span><span class="nb">assoc</span><span class="w"> </span><span class="p">(</span><span class="nb">second</span><span class="w"> </span><span class="n">enriched-action</span><span class="p">)</span><span class="w"> </span><span class="no">:state</span><span class="w"> </span><span class="o">@</span><span class="n">!state</span><span class="p">))</span><span class="w">
        </span><span class="no">:action/validate</span><span class="w"> </span><span class="p">(</span><span class="nf">action-validate-new-product</span><span class="w"> </span><span class="p">(</span><span class="nb">assoc</span><span class="w"> </span><span class="p">(</span><span class="nb">second</span><span class="w"> </span><span class="n">enriched-action</span><span class="p">)</span><span class="w"> </span><span class="no">:state</span><span class="w"> </span><span class="o">@</span><span class="n">!state</span><span class="p">))</span><span class="w">
        </span><span class="p">(</span><span class="nb">when</span><span class="w"> </span><span class="n">goog.DEBUG</span><span class="w"> </span><span class="p">(</span><span class="nf">f-util/clog</span><span class="w"> </span><span class="s">"Unknown action"</span><span class="w"> </span><span class="n">action</span><span class="p">)))))</span><span class="w">
  </span><span class="p">(</span><span class="nf">render!</span><span class="w"> </span><span class="o">@</span><span class="n">!state</span><span class="p">))</span><span class="w">
</span></code></pre></div></div>

<p>And then you just pass events (pure data) to your event handling whenever something happens that changes your UI. Example, you get new data, send the data to the event handler <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/src/cljs/frontend/http.cljs">http.cljs</a>:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defn</span><span class="w"> </span><span class="n">fetch</span><span class="w"> </span><span class="p">[</span><span class="n">dispatcher</span><span class="w"> </span><span class="n">action</span><span class="p">]</span><span class="w">
  </span><span class="p">(</span><span class="nf">f-util/clog</span><span class="w"> </span><span class="s">"fetch, action: "</span><span class="w"> </span><span class="n">action</span><span class="p">)</span><span class="w">
  </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">pg</span><span class="w"> </span><span class="p">(</span><span class="no">:pg</span><span class="w"> </span><span class="n">action</span><span class="p">)</span><span class="w">
        </span><span class="n">url</span><span class="w"> </span><span class="p">(</span><span class="nb">str</span><span class="w"> </span><span class="s">"/api"</span><span class="w"> </span><span class="p">(</span><span class="nf">get-in</span><span class="w"> </span><span class="n">action</span><span class="w"> </span><span class="p">[</span><span class="no">:query</span><span class="w"> </span><span class="no">:api</span><span class="p">]))]</span><span class="w">
    </span><span class="p">(</span><span class="nf">go</span><span class="w"> </span><span class="p">(</span><span class="nf">try</span><span class="w">
          </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">response</span><span class="w"> </span><span class="p">(</span><span class="nf">&lt;!</span><span class="w"> </span><span class="p">(</span><span class="nf">http/get</span><span class="w"> </span><span class="n">url</span><span class="p">))]</span><span class="w">
            </span><span class="p">(</span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nb">=</span><span class="w"> </span><span class="mi">200</span><span class="w"> </span><span class="p">(</span><span class="no">:status</span><span class="w"> </span><span class="n">response</span><span class="p">))</span><span class="w">
              </span><span class="p">(</span><span class="nf">dispatcher</span><span class="w"> </span><span class="n">nil</span><span class="w"> </span><span class="p">[[</span><span class="no">:db/assoc-in</span><span class="w"> </span><span class="p">[</span><span class="no">:db/data</span><span class="w"> </span><span class="n">pg</span><span class="p">]</span><span class="w"> </span><span class="p">(</span><span class="no">:body</span><span class="w"> </span><span class="n">response</span><span class="p">)]])</span><span class="w">
              </span><span class="p">(</span><span class="nf">dispatcher</span><span class="w"> </span><span class="n">nil</span><span class="w"> </span><span class="p">[[</span><span class="no">:db/assoc-in</span><span class="w"> </span><span class="p">[</span><span class="no">:db/data</span><span class="w"> </span><span class="n">pg</span><span class="p">]</span><span class="w"> </span><span class="p">{</span><span class="no">:error</span><span class="w"> </span><span class="p">(</span><span class="no">:status</span><span class="w"> </span><span class="n">response</span><span class="p">)</span><span class="w">
                                                            </span><span class="no">:pg</span><span class="w"> </span><span class="n">pg</span><span class="p">}]])))</span><span class="w">
          </span><span class="p">(</span><span class="nf">catch</span><span class="w"> </span><span class="n">js/Error</span><span class="w"> </span><span class="n">e</span><span class="w">
            </span><span class="p">(</span><span class="nf">dispatcher</span><span class="w"> </span><span class="n">nil</span><span class="w"> </span><span class="p">[[</span><span class="no">:db/assoc</span><span class="w"> </span><span class="no">:db/data</span><span class="w"> </span><span class="p">{</span><span class="no">:error</span><span class="w"> </span><span class="p">(</span><span class="nf">.-message</span><span class="w"> </span><span class="n">e</span><span class="p">)</span><span class="w">
                                                  </span><span class="no">:pg</span><span class="w"> </span><span class="n">pg</span><span class="p">}]</span><span class="w">
                             </span><span class="p">[</span><span class="no">:route/home</span><span class="p">]]))))))</span><span class="w">
</span></code></pre></div></div>

<p>The centralized application store and state, and centralized event handling makes the UI development simple and straightforward. You can pass your event handler (<code class="language-plaintext highlighter-rouge">dispatcher</code> as the parameter of the <code class="language-plaintext highlighter-rouge">fetch</code> function above) around your application and say your functions that if anything happens here that needs to update the UI, send an event to the event handler.</p>

<h3 id="routing">Routing</h3>

<p>Routing can also just send data to the event handler, see <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/src/cljs/frontend/routes.cljs">routes.cljs</a>, using Metosin <a href="https://github.com/metosin/reitit/blob/master/doc/frontend/browser.md">reitit.frontend.easy</a> as the routing library:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">def</span><span class="w"> </span><span class="n">routes</span><span class="w"> </span><span class="p">[[</span><span class="s">"/"</span><span class="w"> </span><span class="p">{</span><span class="no">:name</span><span class="w"> </span><span class="no">:route/home</span><span class="p">}]</span><span class="w">
             </span><span class="p">[</span><span class="s">"/products/:pg"</span><span class="w"> </span><span class="p">{</span><span class="no">:name</span><span class="w"> </span><span class="no">:route/products</span><span class="w">
                               </span><span class="no">:path</span><span class="w"> </span><span class="p">[</span><span class="no">:pg</span><span class="w"> </span><span class="nb">string?</span><span class="p">]}]</span><span class="w">
             </span><span class="p">[</span><span class="s">"/product/:pg/:id"</span><span class="w"> </span><span class="p">{</span><span class="no">:name</span><span class="w"> </span><span class="no">:route/product</span><span class="w">
                                  </span><span class="no">:path</span><span class="w"> </span><span class="p">[</span><span class="no">:pg</span><span class="w"> </span><span class="nb">string?</span><span class="w">
                                         </span><span class="no">:id</span><span class="w"> </span><span class="nb">string?</span><span class="p">]}]</span><span class="w">
             </span><span class="p">[</span><span class="s">"/new/:pg"</span><span class="w"> </span><span class="p">{</span><span class="no">:name</span><span class="w"> </span><span class="no">:route/new</span><span class="w">
                          </span><span class="no">:path</span><span class="w"> </span><span class="p">[</span><span class="no">:pg</span><span class="w"> </span><span class="nb">string?</span><span class="p">]}]])</span><span class="w">

</span><span class="p">(</span><span class="k">defn-</span><span class="w"> </span><span class="n">get-route-actions</span><span class="w"> </span><span class="p">[{</span><span class="no">:keys</span><span class="w"> </span><span class="p">[</span><span class="n">data</span><span class="w"> </span><span class="n">path-params</span><span class="p">]</span><span class="w"> </span><span class="no">:as</span><span class="w"> </span><span class="n">all</span><span class="p">}]</span><span class="w">
  </span><span class="p">(</span><span class="nf">case</span><span class="w"> </span><span class="p">(</span><span class="no">:name</span><span class="w"> </span><span class="n">data</span><span class="p">)</span><span class="w">
    </span><span class="no">:route/home</span><span class="w"> </span><span class="p">[[</span><span class="no">:route/home</span><span class="p">]]</span><span class="w">
    </span><span class="no">:route/products</span><span class="w"> </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">pg</span><span class="w"> </span><span class="p">(</span><span class="nb">keyword</span><span class="w"> </span><span class="p">(</span><span class="no">:pg</span><span class="w"> </span><span class="n">path-params</span><span class="p">))]</span><span class="w">
                      </span><span class="p">[[</span><span class="no">:route/products</span><span class="w"> </span><span class="p">{</span><span class="no">:pg</span><span class="w"> </span><span class="n">pg</span><span class="p">}]])</span><span class="w">
    </span><span class="no">:route/product</span><span class="w"> </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">id</span><span class="w"> </span><span class="p">(</span><span class="nb">int</span><span class="w"> </span><span class="p">(</span><span class="no">:id</span><span class="w"> </span><span class="n">path-params</span><span class="p">))</span><span class="w">
                         </span><span class="n">pg</span><span class="w"> </span><span class="p">(</span><span class="nb">keyword</span><span class="w"> </span><span class="p">(</span><span class="no">:pg</span><span class="w"> </span><span class="n">path-params</span><span class="p">))]</span><span class="w">
                     </span><span class="p">[[</span><span class="no">:route/product</span><span class="w"> </span><span class="p">{</span><span class="no">:id</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="no">:pg</span><span class="w"> </span><span class="n">pg</span><span class="p">}]])</span><span class="w">
    </span><span class="no">:route/new</span><span class="w"> </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">pg</span><span class="w"> </span><span class="p">(</span><span class="nb">keyword</span><span class="w"> </span><span class="p">(</span><span class="no">:pg</span><span class="w"> </span><span class="n">path-params</span><span class="p">))]</span><span class="w">
                 </span><span class="p">[[</span><span class="no">:route/new</span><span class="w"> </span><span class="p">{</span><span class="no">:pg</span><span class="w"> </span><span class="n">pg</span><span class="p">}]])))</span><span class="w">

</span><span class="p">(</span><span class="k">defn</span><span class="w"> </span><span class="n">start!</span><span class="w"> </span><span class="p">[</span><span class="n">routes</span><span class="w"> </span><span class="n">dispatch!</span><span class="p">]</span><span class="w">
  </span><span class="p">(</span><span class="nf">rfe/start!</span><span class="w"> </span><span class="p">(</span><span class="nf">rf/router</span><span class="w"> </span><span class="n">routes</span><span class="p">)</span><span class="w">
              </span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="n">do-routing</span><span class="w"> </span><span class="p">[</span><span class="n">m</span><span class="p">]</span><span class="w">
                </span><span class="p">(</span><span class="nf">dispatch!</span><span class="w"> </span><span class="n">nil</span><span class="w"> </span><span class="p">(</span><span class="nf">get-route-actions</span><span class="w"> </span><span class="n">m</span><span class="p">)))</span><span class="w">
              </span><span class="p">{</span><span class="no">:use-fragment</span><span class="w"> </span><span class="n">true</span><span class="p">}))</span><span class="w">
</span></code></pre></div></div>

<p>So, regarding the route we send either <code class="language-plaintext highlighter-rouge">[[:route/home]]</code> or <code class="language-plaintext highlighter-rouge">[[:route/products {:pg pg}]]</code>, etc. to the event handler.</p>

<h3 id="form-handling">Form Handling</h3>

<p>I took the idea of form handling from the excellent <a href="https://github.com/anteoas/replicant-mini-app">replicant-mini-app</a> example. The idea is that in the form we store the new product (a map comprising its input fields) into the store, see: <a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/src/cljs/frontend/views.cljc">views.cljc</a>:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defn</span><span class="w"> </span><span class="n">new-product</span><span class="w"> </span><span class="p">[</span><span class="n">state</span><span class="w"> </span><span class="n">pg</span><span class="w"> </span><span class="n">headers</span><span class="p">]</span><span class="w">
  </span><span class="p">[</span><span class="no">:div</span><span class="w">
   </span><span class="p">[</span><span class="no">:form</span><span class="w"> </span><span class="p">{</span><span class="no">:on</span><span class="w"> </span><span class="p">{</span><span class="no">:submit</span><span class="w"> </span><span class="p">[[</span><span class="no">:dom/prevent-default</span><span class="p">]</span><span class="w">
                         </span><span class="p">[</span><span class="no">:action/validate</span><span class="w"> </span><span class="p">{</span><span class="no">:pg</span><span class="w"> </span><span class="n">pg</span><span class="p">}]]}}</span><span class="w">
    </span><span class="p">[</span><span class="no">:table.table-auto.w-full</span><span class="w">
     </span><span class="p">[</span><span class="no">:tbody</span><span class="w">
      </span><span class="p">(</span><span class="k">for</span><span class="w"> </span><span class="p">[</span><span class="n">header</span><span class="w"> </span><span class="n">headers</span><span class="p">]</span><span class="w">
        </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">input-tag</span><span class="w"> </span><span class="p">(</span><span class="nb">keyword</span><span class="w"> </span><span class="p">(</span><span class="nb">str</span><span class="w"> </span><span class="s">"input#"</span><span class="w"> </span><span class="p">(</span><span class="nf">cstring/lower-case</span><span class="w"> </span><span class="n">header</span><span class="p">)))</span><span class="w">
              </span><span class="n">db-key-on</span><span class="w"> </span><span class="p">(</span><span class="nb">keyword</span><span class="w"> </span><span class="p">(</span><span class="nb">str</span><span class="w"> </span><span class="p">(</span><span class="nf">cstring/lower-case</span><span class="w"> </span><span class="n">header</span><span class="p">)))]</span><span class="w">
          </span><span class="p">[</span><span class="no">:tr</span><span class="w">
           </span><span class="p">[</span><span class="no">:td.border.px-4.py-2</span><span class="w"> </span><span class="n">header</span><span class="p">]</span><span class="w">
           </span><span class="p">[</span><span class="no">:td.border.px-4.py-2</span><span class="w"> </span><span class="p">[</span><span class="n">input-tag</span><span class="w"> </span><span class="p">{</span><span class="no">:on</span><span class="w"> </span><span class="p">{</span><span class="no">:input</span><span class="w"> </span><span class="p">[[</span><span class="no">:db/assoc-in</span><span class="w"> </span><span class="p">[</span><span class="no">:db/new-product</span><span class="w"> </span><span class="n">db-key-on</span><span class="p">]</span><span class="w"> </span><span class="no">:event/target.value</span><span class="p">]]}}]]]))]]</span><span class="w">
    </span><span class="p">[</span><span class="no">:div.flex.justify-center.mt-4</span><span class="w">
     </span><span class="p">[</span><span class="no">:button.rounded-lg.border-2.border-gray-300.px-4.py-1.m-2.hover:bg-gray-200.cursor-pointer</span><span class="w">
      </span><span class="p">{</span><span class="no">:type</span><span class="w"> </span><span class="no">:submit</span><span class="p">}</span><span class="w">
      </span><span class="s">"Submit"</span><span class="p">]]</span><span class="w">
    </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">error</span><span class="w"> </span><span class="p">(</span><span class="no">:db/product-validation-error</span><span class="w"> </span><span class="n">state</span><span class="p">)</span><span class="w">
          </span><span class="p">[</span><span class="n">k</span><span class="w"> </span><span class="n">v</span><span class="p">]</span><span class="w"> </span><span class="p">(</span><span class="nb">first</span><span class="w"> </span><span class="p">(</span><span class="no">:error</span><span class="w"> </span><span class="n">error</span><span class="p">))]</span><span class="w">
      </span><span class="p">(</span><span class="nb">when</span><span class="w"> </span><span class="n">error</span><span class="w">
        </span><span class="p">(</span><span class="nf">show-error</span><span class="w"> </span><span class="p">(</span><span class="nb">str</span><span class="w"> </span><span class="p">(</span><span class="nb">name</span><span class="w"> </span><span class="n">k</span><span class="p">)</span><span class="w"> </span><span class="s">": "</span><span class="w"> </span><span class="p">(</span><span class="nb">first</span><span class="w"> </span><span class="n">v</span><span class="p">))</span><span class="w"> </span><span class="n">false</span><span class="w"> </span><span class="n">nil</span><span class="p">)))]])</span><span class="w">
</span></code></pre></div></div>

<p>So, <code class="language-plaintext highlighter-rouge">[input-tag {:on {:input [[:db/assoc-in [:db/new-product db-key-on] :event/target.value]]}}]</code> =&gt; we store into the store the field values as the user types them. And finally, when all the fields have been filled, the user submits it: <code class="language-plaintext highlighter-rouge">[:form {:on {:submit [[:dom/prevent-default][:action/validate {:pg :books}]]</code> =&gt; we send <code class="language-plaintext highlighter-rouge">:action/validate</code> event. Everything is nicely event-driven.</p>

<p><a href="https://github.com/karimarttila/clojure/blob/master/webstore-demo/replicant-webstore/src/cljs/frontend/app.cljs">app.cljs</a></p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defn</span><span class="w"> </span><span class="n">action-validate-new-product</span><span class="w"> </span><span class="p">[{</span><span class="no">:keys</span><span class="w"> </span><span class="p">[</span><span class="n">pg</span><span class="w"> </span><span class="n">state</span><span class="p">]}]</span><span class="w">
  </span><span class="p">(</span><span class="nb">when</span><span class="w"> </span><span class="n">goog.DEBUG</span><span class="w"> </span><span class="p">(</span><span class="nf">f-util/clog</span><span class="w"> </span><span class="s">"action-validate-new-product, pg: "</span><span class="w"> </span><span class="n">pg</span><span class="p">))</span><span class="w">
  </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">pg-c</span><span class="w"> </span><span class="p">(</span><span class="nf">f-util/get-pg-config-by-id</span><span class="w"> </span><span class="n">pg</span><span class="w"> </span><span class="p">(</span><span class="no">:db/pg-config</span><span class="w"> </span><span class="n">state</span><span class="p">))</span><span class="w">
        </span><span class="n">pg-id</span><span class="w"> </span><span class="p">(</span><span class="no">:pg-id</span><span class="w"> </span><span class="n">pg-c</span><span class="p">)</span><span class="w"> </span><span class="c1">; This is the number that backend uses for product group.</span><span class="w">
        </span><span class="n">product</span><span class="w"> </span><span class="p">(</span><span class="nf">get-product-from-store</span><span class="w"> </span><span class="n">state</span><span class="w"> </span><span class="n">pg-id</span><span class="p">)</span><span class="w">
        </span><span class="n">dispatcher</span><span class="w"> </span><span class="p">(</span><span class="nf">get-dispatcher</span><span class="p">)</span><span class="w">
        </span><span class="n">validation-ok</span><span class="w">
        </span><span class="p">(</span><span class="nf">case</span><span class="w"> </span><span class="n">pg</span><span class="w">
          </span><span class="no">:books</span><span class="w"> </span><span class="p">(</span><span class="nf">m/validate</span><span class="w"> </span><span class="n">f-schema/book-without-id</span><span class="w"> </span><span class="n">product</span><span class="p">)</span><span class="w">
          </span><span class="no">:movies</span><span class="w"> </span><span class="p">(</span><span class="nf">m/validate</span><span class="w"> </span><span class="n">f-schema/movie-without-id</span><span class="w"> </span><span class="n">product</span><span class="p">))]</span><span class="w">
    </span><span class="p">(</span><span class="k">if</span><span class="w"> </span><span class="n">validation-ok</span><span class="w">
      </span><span class="p">(</span><span class="nf">dispatcher</span><span class="w"> </span><span class="n">nil</span><span class="w"> </span><span class="p">[[</span><span class="no">:db/dissoc</span><span class="w"> </span><span class="no">:db/product-validation-error</span><span class="p">]</span><span class="w"> </span><span class="p">[</span><span class="no">:action/new</span><span class="w"> </span><span class="p">{</span><span class="no">:pg</span><span class="w"> </span><span class="n">pg</span><span class="p">}]])</span><span class="w">
      </span><span class="p">(</span><span class="k">let</span><span class="w"> </span><span class="p">[</span><span class="n">error</span><span class="w"> </span><span class="p">(</span><span class="nf">case</span><span class="w"> </span><span class="n">pg</span><span class="w">
                    </span><span class="no">:books</span><span class="w"> </span><span class="p">(</span><span class="nf">me/humanize</span><span class="w"> </span><span class="p">(</span><span class="nf">m/explain</span><span class="w"> </span><span class="n">f-schema/book-without-id</span><span class="w"> </span><span class="n">product</span><span class="p">))</span><span class="w">
                    </span><span class="no">:movies</span><span class="w"> </span><span class="p">(</span><span class="nf">me/humanize</span><span class="w"> </span><span class="p">(</span><span class="nf">m/explain</span><span class="w"> </span><span class="n">f-schema/movie-without-id</span><span class="w"> </span><span class="n">product</span><span class="p">)))]</span><span class="w">
        </span><span class="p">(</span><span class="nf">dispatcher</span><span class="w"> </span><span class="n">nil</span><span class="w"> </span><span class="p">[[</span><span class="no">:db/assoc</span><span class="w"> </span><span class="no">:db/product-validation-error</span><span class="w"> </span><span class="p">{</span><span class="no">:error</span><span class="w"> </span><span class="n">error</span><span class="w">
                                                                  </span><span class="no">:pg</span><span class="w"> </span><span class="n">pg</span><span class="p">}]])))))</span><span class="w">
</span></code></pre></div></div>

<p>Here we read the new product from the store: <code class="language-plaintext highlighter-rouge">product (:db/new-product state)</code>, start processing it (validation), and if validation is ok, we finally post it to backend (new event: <code class="language-plaintext highlighter-rouge">:action/new</code>).</p>

<p>This example also showed how simple the form validation is - just another step (event).</p>

<h3 id="testing">Testing</h3>

<p>I do not have any tests in my application. But there is a great repo that provides a good example on how to test your pure data UI implementation: <a href="https://github.com/anteoas/replicant-todomvc">replicant-todomvc</a>.</p>

<p>Peter Strömberg (the author of the <a href="https://calva.io/">Calva tool</a>) told me that:</p>

<blockquote>
  <p>It is straight forward to make your application so that you have a pure function as your action handler, and have the side effects (updating the store, calling API:s and endpoints, whatever) in a separate function. It scales better, I think. And also, this way you can run your action handler from tests. (Like the todomvc example demos.) If you are familiar with re-frame, you may recognize this way of separating side effects out from pure data transformations.</p>
</blockquote>

<h2 id="development-tools">Development Tools</h2>

<p>I discovered new ways to use various Clojure and Clojurescript development tools during this exercise. I decided to write a separate blog post regarding those discoveries. But let’s introduce one development tool here that I used with Replicant: the <a href="https://github.com/cjohansen/gadget-inspector">Gadget</a>.</p>

<p>You can see the Gadget in the Chrome Developer tools in the top picture of this blog post. The Gadget repo gives instructions on how to install Gadget to browser. Then telling Gadget to follow the changes of your app store is as easy as this:</p>

<div class="language-clojure highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">defn</span><span class="w"> </span><span class="o">^</span><span class="no">:export</span><span class="w"> </span><span class="n">init!</span><span class="w"> </span><span class="p">[]</span><span class="w">
  </span><span class="p">(</span><span class="nf">inspector/inspect</span><span class="w"> </span><span class="s">"App state"</span><span class="w"> </span><span class="n">!state</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>Since your application store is your UI, having the app store visually next to your app UI in your browser is a great way to boost your productivity and see what happens during event handling (when application store changes, and the changes should trigger changes in your UI).</p>

<h2 id="how-mature-is-replicant">How Mature is Replicant?</h2>

<p>I asked in the Clojurians slack / <code class="language-plaintext highlighter-rouge">#replicant</code> channel, Christian Johansen, the author of Replicant, how mature Replicant is, and is it safe to use Replicant in production. This is his answer:</p>

<blockquote>
  <p>You asked earlier how mature Replicant is - and I forgot to answer. I wrote the bulk of it the fall of 2023, and have been using it in production for a year. Over the past year I have only made minor changes to it, stabilizing, fixing some issues etc. I now consider it pretty much done. So in terms of functionality and stability I’d say it is very mature and safe to use. In terms of community (you asked for examples, etc) it was not publicly announced until January this year. So it’s still young in that sense. I’m hoping more people pick it up, and that those of you who do - and like it - help spread the word.</p>
</blockquote>

<h2 id="start-learning-and-getting-help">Start Learning and Getting Help</h2>

<p>I encourage you to start learning Replicant. Read briefly the documentation to get some mental model how things work. Or you already know the main players of the Replicant game after reading this blog post: the app store/state and the event handler. Then study some simple Replicant example, e.g. the <a href="https://github.com/anteoas/replicant-mini-app">replicant-mini-app</a>. Then start implementing your own app using Replicant.</p>

<p>If you encounter any issues how to do something <em>in a Replicant way</em>, consult other developers and the Replicant author Christian Johansen in the <a href="https://clojurians.slack.com/">Clojurisans Slack</a>, in its <code class="language-plaintext highlighter-rouge">#replicant</code> channel. Christian is active in the Clojurians slack and willing to help Replicant learners. Other clojurians like Peter Strömberg (the author of the great <a href="https://calva.io/">Calva tool</a>, and enthusiastic Replicant user) also often provide help there.</p>

<h2 id="conclusions">Conclusions</h2>

<p>Replicant was really a joy to use since it’s so lightweight and easy to learn. It made state management and rendering straightforward without the complexity of other frameworks. Sure, it doesn’t have all the fancy pre-built components (like in the React ecosystem), but for my UI needs, it was perfect. If you’re looking for a simple and efficient way to build UIs in Clojurescript, give Replicant a try!</p>

<p><em>The writer is working at a major international IT corporation building cloud infrastructures and implementing applications on top of those infrastructures.</em></p>

<p>Kari Marttila</p>

<p><sub> Kari Marttila’s Home Page in LinkedIn: <a href="https://www.linkedin.com/in/karimarttila/">https://www.linkedin.com/in/karimarttila/</a> </sub></p>]]></content><author><name>Kari Marttila</name></author><category term="[&quot;clojurescript&quot;]" /><category term="programming" /><category term="vscode" /><category term="calva" /><category term="languages" /><category term="clojure" /><category term="clojurescript" /><category term="full-stack" /><category term="web" /><category term="babashka" /><category term="repl" /><category term="replicant" /><summary type="html"><![CDATA[]]></summary></entry></feed>