Explore Re-frame, a state management library for Reagent applications, and learn about its unidirectional data flow architecture, events, subscriptions, and effects. Discover how to handle user interactions, update application state, and trigger side effects like AJAX requests.
In this section, we will delve into Re-frame, a powerful state management library designed for Reagent applications in ClojureScript. As experienced Java developers, you may be familiar with state management patterns in JavaScript frameworks like Redux. Re-frame offers a similar unidirectional data flow architecture but leverages the functional programming paradigms of ClojureScript. We’ll explore the core concepts of Re-frame, including events, subscriptions, and effects, and provide practical examples to illustrate how to manage state effectively in your applications.
Re-frame is built on the principles of unidirectional data flow, which simplifies the management of application state by enforcing a single source of truth. This architecture consists of several key components:
Let’s visualize this architecture with a diagram:
Diagram Description: This flowchart illustrates the unidirectional data flow in Re-frame. User interactions trigger events, which are processed by handlers to update the state. Subscriptions derive data from the state to update the UI, while effects handle interactions with external systems.
Before we dive into code examples, let’s set up a basic Re-frame project. We’ll use Leiningen, a popular build tool for Clojure, to bootstrap our project.
Create a new project:
lein new re-frame my-app
Navigate to the project directory:
cd my-app
Start the development server:
lein figwheel
This will start a live-reloading development server, allowing you to see changes in real-time as you develop your application.
Events in Re-frame are similar to actions in Redux. They represent changes in the application state and are dispatched in response to user interactions or other triggers.
Let’s define a simple event to increment a counter:
(ns my-app.events
(:require [re-frame.core :as re-frame]))
(re-frame/reg-event-db
:increment-counter
(fn [db _]
(update db :counter inc)))
Code Explanation:
reg-event-db
is used to register an event handler.:increment-counter
is the event identifier.db
) and an event vector (_
), and returns the updated state.To dispatch the :increment-counter
event, we use the dispatch
function:
(re-frame/dispatch [:increment-counter])
This can be triggered by a button click in the UI:
[:button {:on-click #(re-frame/dispatch [:increment-counter])} "Increment"]
Subscriptions are used to derive data from the application state and provide it to the UI. They are similar to selectors in Redux.
Let’s define a subscription to access the counter value:
(ns my-app.subs
(:require [re-frame.core :as re-frame]))
(re-frame/reg-sub
:counter
(fn [db _]
(:counter db)))
Code Explanation:
reg-sub
registers a subscription.:counter
is the subscription identifier.db
) and returns the counter value.To use the :counter
subscription in a Reagent component, we use the subscribe
function:
(ns my-app.views
(:require [re-frame.core :as re-frame]
[reagent.core :as reagent]))
(defn counter-view []
(let [counter (re-frame/subscribe [:counter])]
(fn []
[:div
[:p "Counter: " @counter]
[:button {:on-click #(re-frame/dispatch [:increment-counter])} "Increment"]])))
Code Explanation:
subscribe
returns a reactive atom that updates when the subscription value changes.@counter
dereferences the atom to get the current counter value.Effects in Re-frame handle side effects, such as AJAX requests or local storage updates. They are defined using reg-fx
.
Let’s define an effect to log messages to the console:
(re-frame/reg-fx
:log
(fn [message]
(js/console.log message)))
Code Explanation:
reg-fx
registers an effect handler.:log
is the effect identifier.To trigger the :log
effect, we use reg-event-fx
to define an event that returns an effect map:
(re-frame/reg-event-fx
:log-message
(fn [_ [_ message]]
{:log message}))
Code Explanation:
reg-event-fx
registers an event handler that returns an effect map.:log
effect and the message to log.Re-frame makes it easy to handle user interactions by dispatching events in response to UI actions. Let’s create a simple form that updates the application state based on user input.
(defn input-form []
(let [input-value (reagent/atom "")]
(fn []
[:div
[:input {:type "text"
:value @input-value
:on-change #(reset! input-value (-> % .-target .-value))}]
[:button {:on-click #(re-frame/dispatch [:submit-input @input-value])} "Submit"]])))
Code Explanation:
reagent/atom
creates a local state for the input value.reset!
updates the atom with the new input value.dispatch
sends the :submit-input
event with the input value.(re-frame/reg-event-db
:submit-input
(fn [db [_ input-value]]
(assoc db :submitted-value input-value)))
Code Explanation:
Re-frame allows you to trigger side effects, such as AJAX requests, using effects. Let’s create an effect to fetch data from an API.
(re-frame/reg-fx
:fetch-data
(fn [url]
(js/fetch url
(clj->js {:method "GET"})
(.then (fn [response] (-> response .json (.then #(re-frame/dispatch [:data-received %]))))))))
Code Explanation:
:data-received
event.(re-frame/reg-event-db
:data-received
(fn [db [_ data]]
(assoc db :api-data data)))
Code Explanation:
Now that we’ve covered the basics of Re-frame, try modifying the examples to deepen your understanding:
:log
effect to log user interactions to the console.By mastering Re-frame, you’ll be well-equipped to manage state in complex ClojureScript applications, leveraging the power of functional programming to build robust and maintainable software.