---
title: "Getting Started with irid"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Getting Started with irid}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

irid is a new way to build dynamic Shiny apps. Instead of `renderUI`,
irid binds reactivity directly to individual DOM attributes — one reactive
changes, one attribute updates. You keep using `reactiveVal` and `reactive` as
usual, and build UIs from plain R functions that compose naturally.

The core idea is one simple rule: **pass a function instead of a value to make
any tag attribute reactive.**

## Installation

Install from GitHub:

```r
# install.packages("pak")
pak::pak("khusmann/irid")
```

## Your first irid app

```r
library(irid)

Counter <- function() {
  count <- reactiveVal(0)

  tags$div(
    tags$p("Count: ", count),
    tags$button(
      "Increment",
      disabled = \() count() >= 10,
      onClick = \(ev) count(count() + 1)
    )
  )
}

iridApp(Counter)
```

***[Try it live.](https://irid.kylehusmann.com/apps/counter/index.html?_shinylive-mode=editor-terminal-viewer){target="_blank"}***

A irid app is a single function that returns a tag tree. Inside the function
you create `reactiveVal`s for state and wire them to the DOM with functions.
The function runs **once** — irid processes the tag tree, extracts the
reactive functions, and sets up observers that surgically update individual DOM
nodes when values change. No re-rendering entire subtrees, no DOM destruction.

Three things to notice: `count` appears as a child of `tags$p()` and its value
is displayed inline; the button's `disabled` attribute is a function, so it
re-evaluates whenever `count` changes; and `onClick` is wired directly on the
tag — no observers, no input/output IDs, no `updateActionButton()` or
`observeEvent()`.

`iridApp()` returns a `shinyApp` object, so it works with `runApp()`,
`shinytest2`, and deployment tools.

## Components compose

Because a component is just a function that returns a tag tree, you build
larger UIs by calling smaller ones. Pass `reactiveVal`s as arguments to share
state between them:

```r
Counter <- function(label, count) {
  card(
    card_header(label),
    card_body(
      tags$h2(
        class = "text-center",
        \() paste("Count:", count())
      ),
      tags$input(
        type = "range", min = 0, max = 100,
        value = reactiveProxy(get = count, set = \(v) count(as.numeric(v)))
      ),
      tags$button(
        class = "btn btn-outline-secondary btn-sm",
        disabled = \() count() == 0,
        onClick = \() count(0),
        "Reset"
      )
    )
  )
}

App <- function() {
  count_a <- reactiveVal(0)
  count_b <- reactiveVal(0)
  total <- reactive(count_a() + count_b())

  page_fluid(
    tags$h3(class = "text-center", \() paste("Total:", total())),
    layout_columns(
      Counter("A", count_a),
      Counter("B", count_b)
    )
  )
}

iridApp(App)
```

***[Try it live.](https://irid.kylehusmann.com/apps/composing/index.html?_shinylive-mode=editor-terminal-viewer){target="_blank"}***

`count_a` and `count_b` are created once in `App` and passed down to each
`Counter`. There are no string IDs to keep in sync. `total` is a derived
reactive that reads both — any component in the tree can read or write shared
state by holding a reference to the same `reactiveVal`.

## Reactive attributes

Any tag attribute can be static or reactive. Pass a function to make it
reactive:

```r
# Static
tags$div(class = "panel")

# Reactive
tags$div(class = \() if (is_active()) "panel active" else "panel")
```

Since `reactiveVal` and `reactive` are both functions, they work directly as
attribute values:

```r
name <- reactiveVal("hello")
upper_name <- reactive(toupper(name()))

tags$span(upper_name)   # reactive is a function
tags$span(name)         # reactiveVal is a function
tags$span(\() name())   # anonymous function works too
```

## Reactive children

Tag children can also be reactive functions, but they must return **text
only** — not tag trees. Use control flow primitives (below) for structural
changes:

```r
tags$span(\() paste("Count:", count()))   # text — works
```

## Event callbacks

Event callbacks receive `(event)` or `(event, id)`. The `event` is a list of
primitive-valued properties from the browser event, plus element properties
like `value`, `valueAsNumber`, and `checked`:

```r
onClick = \(event) handle_click(event)              # event object
onClick = \(event, id) handle_click(id)             # event + element id
onClick = \() count(count() + 1)                    # neither
```

Event timing, backpressure, and `event.preventDefault()` ride the **slot**
they configure. Wrap a handler (or a bound reactive) in `wire()` to
attach a timing shape — `wire_immediate()`, `wire_debounce(ms)`, or
`wire_throttle(ms)` — and DOM listener options via `wire_dom_opts()`:

```r
tags$input(value = wire(field, wire_debounce(500)))
tags$button("Save", onClick = wire(\() save(), wire_throttle(1000)))
tags$form(onSubmit = wire(\(e) handle(e),
                               dom_opts = wire_dom_opts(prevent_default = TRUE)))
```

Each event is configured exactly once, on its own slot — there are no
element-level lists to keep in sync:

```r
tags$input(
  value = wire(field, wire_debounce(500)),
  onKeyDown = \(e) if (e$key == "Enter") submit()
)

tags$form(
  onSubmit = wire(\() submit(),
                       dom_opts = wire_dom_opts(prevent_default = TRUE)),
  onClick = \(e) handle_click(e)
)
```

A bare handler (`onClick = \() …`) is sugar for `wire(\() …)` with the
default config. When a wire carries no `timing`, irid applies a per-event
default keyed on the DOM event name: `input` events default to
`wire_debounce(200)` (typing floods the wire with intermediate values),
every other event to `wire_immediate()`. `coalesce` (gate on server idle)
is universal, so it lives on the carrier — `wire(field,
wire_debounce(500), coalesce = FALSE)` — and defaults to `TRUE` for
rate-limited shapes, `FALSE` for `wire_immediate()`.

## Controlled inputs

Bind a state-binding prop (`value`, `checked`) to a callable to get two-way
binding for free. The callable can be a `reactiveVal`, a store leaf, a
`reactiveProxy`, or any function:

```r
name <- reactiveVal("")

tags$input(type = "text", value = name)
```

The read fills the input; DOM events on the element write back through the
same callable. Multiple inputs can share the same `reactiveVal` — type in
one, the others update. No `updateTextInput`. No `freezeReactiveValue`. No
`onInput` write handler.

If you need to transform reads or gate writes, wrap the callable in a
`reactiveProxy`:

```r
# Coerce the string from a range slider into a number on write
reactiveProxy(get = count, set = \(v) count(as.numeric(v)))

# Bidirectional transform — display Fahrenheit, store Celsius
reactiveProxy(
  get = \() celsius() * 9/5 + 32,
  set = \(f) celsius((as.numeric(f) - 32) * 5/9)
)

# Read-only view — writes silently dropped, input snaps back to current value
reactiveProxy(get = name)
```

A 0-arg function (e.g. `\() toupper(name())`) behaves the same as
`reactiveProxy(get = name)` (read-only with snap-back).

A DOM event is **bound *or* handled, never both.** Combining an auto-bound
state-binding prop with an explicit `on*` handler for the *same* event
(e.g. `value = rv, onInput = \(e) ...`) is an error — the binding already
claims that event. This is per-event: `value = rv` with `onKeyDown` is
fine, since they're different events. To run a synchronous side-effect or
validation on write, put it in the proxy's `set`
(`value = reactiveProxy(get, set)`); to react asynchronously, observe the
bound reactive.

*See the [Temperature Converter](https://irid.kylehusmann.com/apps/temperature/index.html?_shinylive-mode=editor-terminal-viewer){target="_blank"} example.*

## Control flow

Because the component function runs once, you can't use plain `if`/`else` for
conditional rendering. irid provides control flow primitives instead.

### When

`When` is the binary specialization. The bodies are 0-arg functions that
return a tag tree — they are called fresh on each activation, since the
previous branch's closures are torn down with its reactives:

```r
When(logged_in,
  \() Dashboard(),
  otherwise = \() LoginPanel()
)
```

### Match / Case / Default

`Match` dispatches on a leading callable. Records are projected as a
mini-store for the active case body; scalars are passed as the bare
callable. `Case`'s first arg is one of: a function `\(v) cond` of the
bound value, a function `\() cond` ignoring it (cross-cutting), or a
literal (equality match via `identical`). `Case`'s second arg and
`Default`'s arg are 0- or 1-arg functions returning a tag tree:

```r
Match(tab,
  Case("home",     \() HomePage()),
  Case("settings", \() SettingsPage()),
  Default(\() NotFoundPage())
)
```

### Each

Dynamic lists. The callback receives a per-item callable — a mini-store
for record items, a scalar accessor for atomic items — and an optional
1-indexed position accessor:

```r
tags$ul(
  Each(todos, by = \(t) t$id, \(todo) {
    tags$li(
      tags$input(type = "checkbox", checked = todo$done),
      tags$span(\() todo$text())
    )
  })
)
```

`by = NULL` (the default) reconciles positionally — slot *i* is slot
*i*, the list grows or shrinks at the end, and same-length value
changes update slots in place without DOM recreation. `by = fn` keys
items by `fn(item)` — kept items are reused across reorders, adds, and
removes; mini-store leaves are diffed so only changed fields fire.

*See the [Todo List](https://irid.kylehusmann.com/apps/todo/index.html?_shinylive-mode=editor-terminal-viewer){target="_blank"} example.*

## Shiny outputs

Plots, tables, and other binary outputs use Shiny's existing render
infrastructure via `Output`, or convenience wrappers:

```r
PlotOutput(\() ggplot(mtcars, aes(wt, mpg)) + geom_point())
TableOutput(\() head(mtcars))
DTOutput(\() mtcars)
```

For any render/output pair, pass both functions explicitly:

```r
Output(renderPlot, plotOutput, \() ggplot(mtcars, aes(wt, mpg)) + geom_point())
```

## Incremental adoption

You don't have to go all-in. Drop irid into an existing Shiny app with
`iridOutput` / `renderIrid`:

```r
ui <- fluidPage(
  sliderInput("n", "N", 1, 100, 50),
  iridOutput("filters"),
  plotOutput("plot")
)

server <- function(input, output, session) {
  threshold <- reactiveVal(0.5)

  output$filters <- renderIrid(
    tags$div(
      tags$input(
        type = "range", min = 0, max = 1, step = 0.1,
        value = reactiveProxy(get = threshold, set = \(v) threshold(as.numeric(v)))
      ),
      tags$span(\() paste("Threshold:", threshold()))
    )
  )

  output$plot <- renderPlot({
    mtcars |> head(input$n) |>
      dplyr::filter(mpg > threshold() * 30) |>
      ggplot2::ggplot(ggplot2::aes(wt, mpg)) + ggplot2::geom_point()
  })
}

shinyApp(ui, server)
```

The migration path:

1. Start with a normal Shiny app
2. Drop in one `iridOutput`/`renderIrid` for a painful `renderUI`
3. Gradually convert more components
4. Eventually switch to `iridApp` when the whole app is irid
5. Old `sliderInput` etc. still work at every stage

*See the [Shiny Modules](https://irid.kylehusmann.com/apps/shiny-interop/index.html?_shinylive-mode=editor-terminal-viewer){target="_blank"} example.*
