ef.js

Declarative DOM helper experiment

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  77. 77
  78. 78
  79. 79
  80. 80
  81. 81
  82. 82
  83. 83
  84. 84
  85. 85
  86. 86
  87. 87
  88. 88
  89. 89
  90. 90
  91. 91
  92. 92
  93. 93
  94. 94
  95. 95
  96. 96
  97. 97
  98. 98
  99. 99
  100. 100
  101. 101
  102. 102
  103. 103
  104. 104
  105. 105
  106. 106
  107. 107
  108. 108
  109. 109
  110. 110
  111. 111
  112. 112
  113. 113
  114. 114
  115. 115
  116. 116
  117. 117
  118. 118
# Async sample

This page demonstrates an example approach declarative UI to cooperate with async operation naturally.
The example uses a help function named `series`, which leverages Async Generator Function to allow users write multi-step UI smoothly.

Sample application loads specified user info asynchronously then display that as `<dl>`.

## Explicit resource association is required

Signals and Deriveds/Effects are associated using function call stack by design.
This design choice has one big drawback: everything must be a synchronous normal function call.

For example, this code does not work as intended due to the changes to `$name` will not re-run `e1`.

```ts
const $name = signal("Alice");

// 1. global stack = []
const e1 = effect(() => {
  // 2. global stack = [e1]

  const tid = setTimeout(() => {
    // 4. global stack = []
    // next loop = outside of `e1`

    // When this line is executed, `e1` is popped from global stack.
    // Therefore, `$name.get()` would be orphan and changes to `$name` does not
    // re-run `e1`.
    console.log($name.get());
  }, 500);

  return () => {
    clearTimeout(tid);
  };
});

// 3. global stack = []
```

There are two way to tackle this problem.

### Retrieve value early

If you call `.get()` eagerly (when the call stack is not emptied yet), there is nothing to care about.

```ts
// ...
const e1 = effect(() => {
  // global stack = [e1]
  // Hence `$name` is associated to `e1`
  const name = $name.get();

  const tid = setTimeout(() => {
    console.log(name);
  }, 500);

  return () => {
    clearTimeout(tid);
  };
});
```

### Manually specify dependant effect

`effect` function passes itself as a first argument to the callback.
When `Signal.prototype.get()` and `Derived.prototype.get()` saw the first parameter, they treat the first parameter as a dependant instead of the global stack.
By using those API, you can explicitly specify which value re-runs which effect on value changes.

```ts
const e1 = effect((ctx) => {
  const tid = setTimeout(() => {
    // global stack is empty, but `.get(ctx)` refers the first argument
    // instead of global stack so `$name` is associated to `e1`.
    console.log($name.get(ctx));
  }, 500);

  return () => {
    clearTimeout(tid);
  };
});
```

While this approach is more flexible as it allows values to associate themselves to an effect lazily, this could be more error-prone because it's easy to forget put the context to the first parameter.
I recommend you to carefully evaluate all three approaches before integrating into your app.

The helper function in this example page uses this approach, mainly for demonstrating lazy association.

## Alternative

Use `asyncDerived`. It's the most straightforward way to represent async procedure.

```ts
import { asyncDerived } from "@pocka/ef";

const $myApiResult = asyncDerived(async () => {
  const resp = await fetch("/my/api");

  if (resp.status !== 200) {
    throw new Error(`Unexpected status code (HTTP ${resp.status})`);
  }

  return resp.json();
});

const myUI = derived(() => {
  const myApiResult = $myApiResult.get();

  if (!myApiResult.isSettled) {
    return el("p", [], ["Fetching"]);
  }

  if (myApiResult.isRejected) {
    return el("p", [], ["ERROR: " + String(myApiResult.error)]);
  }

  return el("div", [], [doSomethingWithApiData(myApiResult.data)]);
});
```