Skip to contents

A block is represented by several (nested) shiny modules and the top level module is created using the block_server() generic. S3 dispatch is offered as a way to add flexibility, but in most cases the default method for the block class should suffice at top level. Further entry points for customization are offered by the generics expr_server() and block_eval(), which are responsible for initializing the block "expression" module (i.e. the block server function passed in new_block()) and block evaluation (evaluating the interpolated expression in the context of input data), respectively.

Usage

block_eval(x, expr, env, ...)

eval_env(data)

block_eval_trigger(x, session = get_session())

block_server(id, x, data = list(), ...)

# S3 method for class 'block'
block_server(
  id,
  x,
  data = list(),
  block_id = id,
  edit_block = NULL,
  ctrl_block = NULL,
  board = reactiveValues(),
  update = reactiveVal(),
  inputs_ready = reactive(TRUE),
  needed = reactive(TRUE),
  visibility = NULL,
  ...
)

expr_server(x, data, ...)

block_render_trigger(x, session = get_session())

Arguments

x

Object for which to generate a shiny::moduleServer()

expr

Quoted expression to evaluate in the context of data

env

Environment in which to evaluate expr

...

Generic consistency

data

Input data (list of reactives)

session

Shiny session object

id

Namespace ID

block_id

Block ID

edit_block, ctrl_block

Block plugins

board

Reactive values object containing board information

update

Reactive value object to initiate board updates

inputs_ready

Reactive flag signaling whether the block's required inputs are all connected to ready upstream blocks (supplied by board_server(); defaults to always-ready when a block server is run standalone)

needed

Reactive flag signaling whether the block is currently in the eval set (supplied by board_server(); defaults to always-needed when a block server is run standalone)

visibility

Front-end channel bundle – a gate reactiveVal holding the owner label of the front-end that made the board lazy, plus visible and frozen, each an environment of per-block reactiveVals, supplied by board_server() to hold rendering until a block is painted and to freeze block inputs; NULL (the standalone default) renders the block as soon as it is ready

Value

Both block_server() and expr_server() return shiny server module (i.e. a call to shiny::moduleServer()), while block_eval() evaluates an interpolated (w.r.t. block "user" inputs) block expression in the context of block data inputs.

Details

The module returned from block_server(), at least in the default implementation, provides much of the essential but block-type agnostic functionality, including data input validation (if available), instantiation of the block expression server (handling the block-specific functionality, i.e. block user inputs and expression), and instantiation of the edit_block module (if passed from the parent scope).

Each block carries an eval status – one of unevaluated, stale, waiting, unset, failed or ready – which, together with its orthogonal front-end visibility, determines its behavior. The status is what the block's last check found, a check being a run of the block or the finding that it cannot run. A block that is needed – held eager, or feeding a block that is, as described below – is checked afresh whenever its status or result is read. One that is not, a parked block, keeps its inputs unfulfilled (shiny::req() out) and evaluates nothing: it reports what its last check found, and its result is the one that check left, for as long as nothing the check read has changed. Needed or parked, a block that is current reads the same. Four statuses are what a check can find, separating the two input kinds (data inputs from links, user inputs from state) and a genuine failure:

  • waiting – a required data input is missing: unconnected, below the required number of variadic ...args inputs (one by default), or fed by an upstream block that is not itself ready (see allow_empty_state).

  • unset – data inputs are ready, but a required user input (state value) has not been provided (unless permitted by allow_empty_state).

  • failed – all inputs are present, but the block cannot produce a result: the data validator (validate_data_inputs()) or the block expression raised. The offending condition is surfaced through the block conditions.

  • ready – evaluation succeeded and a result (possibly a legitimate NULL) is available for downstream blocks to consume.

A parked block reads one of the other two when it has no current check to report on:

  • stale – something the last check read has changed since: the block's expression or eval trigger, which blocks feed its data inputs, or what one of those holds. An upstream that is itself stale or unevaluated counts as changed, so a change reaches the whole downstream cone. The block is not re-evaluated; the status only reports that what its last check found is out of date, so a front-end can flag it (e.g. a muted node badge) without forcing a recompute. An expression built from the input data cannot be rebuilt while those are withheld, so for such a block the state it is built from is compared instead.

  • unevaluated – the block has never been checked, which includes a board block that is not built yet.

A consumer that needs a parked block current asks for it with a board_update() evaluate request. Once none of the blocks it asked for reads stale or unevaluated, what they report is current.

A block reaches ready only once its upstreams have, so an unconnected or pending block holds its whole downstream chain waiting without any of them evaluating against missing data. Output rendering follows the status: the block output is shown only while ready and cleared otherwise, so a block leaving ready never displays a stale result. While not ready the block surfaces a condition explaining why – a status-phase note for waiting and unset, or the raised error for failed. The note is recorded by the check that finds the block unable to run, so a block checked off screen carries it too, and the check that runs the block clears it. Conditions raised during validation and evaluation are caught and returned to be surfaced to the app user.

Block-level user inputs (provided by the expression module) are separated from output, the behavior of which can be customized via the block_output() generic. The block_ui() generic can then be used to control rendering of outputs.

A board is eager by default: every block is needed, so every block evaluates. A front-end (such as blockr.dock) makes it lazy by returning eager() from the callback it registers with board_server(), naming its owner label and the blocks it needs evaluated from the start. From then on only the blocks some owner holds eager are needed, together with what feeds them. A board whose callbacks return no such value stays eager, and setting the gate_visibility blockr_option() (default TRUE) to FALSE keeps every board eager. Core reads the declaration as it runs the callbacks and seeds the opening eager set there and then, before the first flush decides what to construct – which no board update could do, since a payload only applies at the end of the flush it is written in.

Which blocks the front-end needs evaluated from then on is not a channel of its own: it travels as an eager component under that same owner label, leaving the front-end one owner among several rather than a special case core can distinguish from a code export or an extension (see the Evaluation requests section of board_server()).

Evaluation follows the needed set, the blocks held eager together with their upstream closure over board_links() (recomputed only when eager sets or links change). A block's input data reactives stay unfulfilled (they shiny::req() out) unless the block is needed, so a block that is neither held eager nor feeding one pulls no input and stays fully quiescent: its result reactive hands back what the last check left, and any observer its expression server registers on the incoming data short-circuits and does nothing. A needed but off-screen block (one feeding a block held eager) evaluates but does not render.

Rendering follows visible, the per-block channel through which the front-end reports what it has painted – the effect, where holding a block eager is the cause. The render observer is suspended while a block carries no visible slot and resumed once the front-end reports it painted, starting suspended so nothing renders before the first report.

Block-server construction is prioritized the same way: the needed set is instantiated first so that first paint waits only for the blocks held eager and their upstreams, and the remaining block servers are built progressively in the background. That background pass holds until the front-end reports every block it holds eager as visible, so it never competes with first paint. Until a block is built it is absent from the board$blocks handed to plugins and callbacks, which simply see it appear once constructed. The background cadence is set by the background_construction_delay blockr_option() (milliseconds between successive blocks, default 50); a value of 0 disables the staggering and builds every block up front.

Core's own board UI drives those channels through a callback, on the same footing as a front-end rather than built into the board server. Stacks render as a bslib::accordion() which opens one stack and collapses the rest (see stack_ui()), so on a stacked board part of what is on screen is hidden from the first render and any stack can be collapsed afterwards. The gate_stacks() callback reads that accordion back, holding the blocks of every open stack plus every unstacked block eager and parking the rest, so collapsing a stack stops its blocks evaluating and expanding one starts them again. It is board_server()'s default callbacks value. Which stacks render open is core's own decision (see stack_ui()), so on a stacked board the callback returns that set as its opening eager set: an eager board evaluates every block, and a collapsed stack's blocks would otherwise evaluate once before the accordion reports. The accordion's report then refines the set rather than establishing it. A board with no stacks binds no such input and has nothing to park, so this callback leaves it eager; a board driven by another front-end never runs it, since it passes its own callbacks. Setting the gate_visibility option to FALSE keeps this board eager too.

The same bundle carries a third channel, frozen, through which a front-end reports the blocks whose inputs it has hidden (for example a locked board that shows outputs but not controls). While frozen a block is read-only: its expression, state readiness and the state it exposes for serialization are held at the values last seen while editable, and the input trigger is dropped, so a forged client input (which still fires the block's own observer) reaches neither the expression, the block's status, a re-evaluation, nor a save. Externally controllable inputs (see external_ctrl_vars()) are held too – a high-priority observer reverts any write while frozen – so not even the programmatic control channel can drive a frozen block. Upstream data still flows through, and unfreezing resumes normal input handling.

Evaluation trigger

block_eval_trigger() lets a block declare reactive state its evaluation depends on that is not visible in the block expression or its data inputs – the plot_block method returns the thematic and dark_mode board options, so a theme change re-renders the plot. Its value joins the block's unchanged-inputs check: the block re-evaluates when its interpolated expression, its input data, or the value returned here changes. Reading a reactive inside the method registers the dependency that wakes the block, but it is the returned value changing – not the method being re-run – that forces re-evaluation: returning a value equal to the previous one skips it, even if the reactives it read invalidated. To force re-evaluation on an event with no natural value, return a value that changes on it, such as an incrementing counter. The default method returns NULL, declaring no such dependency.