Skip Initial Run Example

examples/Intermediate/SkipInitialRun/SkipInitialRun.ino
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/**
 * SPDX-FileCopyrightText: 2026 Maximiliano Ramirez <maximiliano.ramirezbravo@gmail.com>
 *
 * SPDX-License-Identifier: MIT
 */

/**
 * ReactiveESP32 Example Overview:
 * - This example demonstrates the use of the `skip_initial_run` option in Computed and Effect
 *   nodes, including the `watch()` method to manually register Effect dependencies.
 *
 * - By default, Computed and Effect nodes execute once at construction to compute their initial
 *   value and register their source dependencies via automatic tracking. With
 *   `skip_initial_run = true`, this initial execution is deferred.
 *
 * - For Computed nodes, the deferred computation is transparent: the first call to get()
 *   triggers the initial computation and dependency tracking automatically.
 *
 * - For Effect nodes, deferral creates a problem: Effects are never "read" by anyone, so
 *   without a first execution there is no way to automatically register dependencies. The Effect
 *   would never fire, even when its intended sources change.
 *   The solution is watch(): call it right after construction to manually register the source
 *   nodes the Effect should listen to. watch() does NOT execute the effect function, it only
 *   sets up the dependency links so the Effect fires on the next change.
 *
 * - The Serial interface is used to interact with the program:
 *   - 'g': Get the current Celsius and Fahrenheit values (triggers Computed on first call).
 *   - 's': Set temperature to a new value (increments by 5 °C each time).
 *   - 'w': Call watch() on the Effect to register 'celsius' as a dependency.
 *   - 'i': Print initialization status of Computed and Effect.
 *
 * - Pressing '0' restarts the ESP32.
 */

#include <ReactiveESP32.h>
using namespace RxESP32;

/* ---------------------------------------------------------------------------------------------- */
// Source signal: temperature in Celsius
Signal<int> celsius(20, {.name = "celsius"});

// Computed: converts Celsius to Fahrenheit.
// With skip_initial_run = true, the conversion is NOT computed at construction.
// The value starts as the default T() (0.0f). The first get() triggers computation.
Computed<float> fahrenheit(
  []() {
    int c   = celsius.get(); // Automatically registers 'celsius' as a source
    float f = c * 9.0f / 5.0f + 32.0f;
    Serial.printf("\t[Computed] %d °C -> %.1f °F\n", c, f);
    return f;
  },
  {.name = "fahrenheit", .skip_initial_run = true});

// Effect: logs when temperature changes.
// With skip_initial_run = true, the effect does NOT run at construction, so dependency tracking
// never happens automatically. Without watch(), this Effect would never fire.
// Call watch(celsius) after construction to manually register the dependency.
Effect<> logger(
  []() {
    int c = celsius.get();
    Serial.printf("\t[Effect] Temperature changed to %d °C\n", c);
    return nullptr;
  },
  {.name = "logger", .skip_initial_run = true});

// Read Serial input and process commands
void serialRead();
/* ---------------------------------------------------------------------------------------------- */

void setup() {
  Serial.begin(115200);
  delay(1000);

  Serial.println("======================================");
  Serial.println("ReactiveESP32 - SkipInitialRun Example");
  Serial.println("======================================");
  Serial.println("Commands:");
  Serial.println("  'g' - Get current temperature values");
  Serial.println("  's' - Set temperature (increments by 5 each call)");
  Serial.println("  'w' - Register Effect dependency via watch()");
  Serial.println("  'i' - Show initialization status");
  Serial.println("  '0' - Restart ESP32\n");
  Serial.println("Tip: try 's' before and after 'w' to see the difference.\n");

  // Start the ReactiveESP32 dispatcher
  if (!Dispatcher::start()) {
    Serial.println("Failed to start ReactiveESP32 Dispatcher!");
    while (true) {
      delay(1000);
    }
  }
}

void loop() { serialRead(); }

void serialRead() {
  if (!Serial.available()) return;

  char c = Serial.read();

  if (c == '\r') return;
  if (c == '\n') c = ' ';
  Serial.printf("> %c\n", c);

  switch (c) {
    case '0':
    {
      // Restart the ESP32
      ESP.restart();
    } break;

    case 'g':
    {
      // Calling get() on a Computed with skip_initial_run triggers the first computation
      // transparently. Subsequent calls return the cached value until a dependency changes.
      bool was_initialized = !fahrenheit.isDirty();

      if (fahrenheit.isDirty()) {
        Serial.println("'fahrenheit' not yet computed, get() will trigger first computation...");
      } else {
        Serial.println("'fahrenheit' is clean, returning cached value.");
      }

      int c_val   = celsius.get();
      float f_val = fahrenheit.get(); // Computes & registers deps on first call

      Serial.printf("celsius=%d - fahrenheit=%.2f\n", c_val, f_val);

      if (!was_initialized) {
        Serial.println("'fahrenheit' is now initialized and tracking 'celsius'.");
      }
    } break;

    case 's':
    {
      static int new_celsius = 25;

      Serial.printf("Setting celsius to %d °C\n", new_celsius);
      Serial.println("  -> Computed will recompute next get() (or when dispatcher runs).");
      Serial.println("  -> Effect will fire ONLY if watch() was called.");

      celsius.set(new_celsius);
      new_celsius += 5;
    } break;

    case 'w':
    {
      // watch() registers 'celsius' as a dependency of 'logger' WITHOUT running the effect.
      // From this point on, any change to 'celsius' will trigger 'logger' automatically.
      Status status = logger.watch(celsius);

      if (status == Status::Ok) {
        Serial.println("watch(celsius) succeeded.");
        Serial.println("  -> 'logger' will now fire whenever 'celsius' changes.");
      } else if (status == Status::WatchNotAllowed) {
        Serial.println("watch() not allowed: Effect is already initialized, lazy, or not using "
                       "skip_initial_run.");
      } else {
        Serial.printf("watch() failed: %s\n", Utils::statusToStr(status));
      }
    } break;

    case 'i':
    {
      // Show the initialization and dirty state of both nodes
      Serial.println("--- Node status ---");
      Serial.printf("  fahrenheit (Computed): dirty=%s\n",
        fahrenheit.isDirty() ? "yes (not yet computed)" : "no (clean)");
      Serial.printf("  logger     (Effect):   dirty=%s\n",
        logger.isDirty() ? "yes (pending execution)" : "no (clean / not yet watching)");
    } break;
  }
}

See Also

  • Core - Core API Reference

  • Filters - Filters API Reference

  • Helpers - Helpers API Reference