Skip to content

Commit 9f2bf31

Browse files
committed
Bug: Fix issue with reaction usage for SSR
1 parent cc819dc commit 9f2bf31

File tree

6 files changed

+143
-91
lines changed

6 files changed

+143
-91
lines changed

RELEASE-NOTES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ This is a pre-release version and APIs will change quickly. Before `1.0` release
66

77
Please note after `1.0` Semver will be followed using normal protocols.
88

9+
# Version 0.13.1 - 07.14.2025
10+
11+
* **Bug** - Fixed a bug with SSR in reactive directives like conditional/data. If a reaction was long-lived (for example an interval is set up in onCreated) the reaction would not properly get gced and could rerun on the server causing an ssr error like (TypeError: this._$Ct._$AI is not a function at ReactiveDataDirective.setValue)
12+
913
# Version 0.13.0 - 07.14.2025
1014

1115
## CSS Tokens

packages/renderer/src/lit/directives/reactive-async.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,23 @@ export class ReactiveAsyncDirective extends AsyncDirective {
2121
this.reaction = null;
2222
}
2323

24+
// Create a new reaction that watches for reactive changes on client
25+
if(isClient) {
26+
this.watchChanges();
27+
}
28+
29+
// Return initial render
30+
return this.renderCurrentState(asyncCondition);
31+
}
32+
33+
watchChanges() {
34+
2435
// pass through context for debugging
2536
let context = {
2637
message: `async block: {#async ${asyncCondition.expression}}`,
2738
async: asyncCondition,
2839
};
2940

30-
// Create a new reaction
3141
this.reaction = Reaction.create((computation) => {
3242
if (!this.isConnected) {
3343
computation.stop();
@@ -46,9 +56,6 @@ export class ReactiveAsyncDirective extends AsyncDirective {
4656
this.setValue(rendered);
4757
}
4858
}, { context });
49-
50-
// Return initial render
51-
return this.renderCurrentState(asyncCondition);
5259
}
5360

5461
handleExpressionResult(result, asyncCondition) {

packages/renderer/src/lit/directives/reactive-conditional.js

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Reaction } from '@semantic-ui/reactivity';
2-
import { each } from '@semantic-ui/utils';
2+
import { each, isClient } from '@semantic-ui/utils';
33
import { nothing, noChange } from 'lit';
44
import { AsyncDirective } from 'lit/async-directive.js';
55
import { directive } from 'lit/directive.js';
@@ -21,53 +21,71 @@ export class ReactiveConditionalDirective extends AsyncDirective {
2121
message: `if/else statement: {#if ${conditional.expression}}`,
2222
conditional: conditional,
2323
};
24-
this.reaction = Reaction.create((comp) => {
25-
if (!this.isConnected) {
26-
comp.stop();
27-
return;
28-
}
29-
let matchIndex = -1;
30-
if (conditional.condition()) {
31-
html = conditional.content();
32-
matchIndex = 1000; // special index for if condition
33-
}
34-
else if (conditional.branches?.length) {
35-
// evaluate each elseif/else branch
36-
each(conditional.branches, (branch, index) => {
37-
if(matchIndex === -1) {
38-
if (branch.type == 'elseif' && branch.condition()) {
39-
matchIndex = index;
40-
html = branch.content();
41-
}
42-
else if (branch.type == 'else') {
43-
matchIndex = index;
44-
html = branch.content();
45-
}
46-
}
47-
});
48-
}
49-
else {
50-
html = nothing;
51-
delete this.matchIndex;
52-
}
53-
if (!html) {
54-
html = nothing;
55-
delete this.matchIndex;
56-
}
57-
if (!comp.firstRun && this.matchIndex !== matchIndex) {
58-
this.matchIndex = matchIndex;
59-
this.setValue(html);
60-
}
61-
return html;
62-
}, { context });
63-
/* Commented out until can resolve mobile menu
24+
25+
// Create a new reaction that watches for reactive changes on client
26+
if(isClient) {
27+
this.reaction = Reaction.create((comp) => {
28+
if (!this.isConnected) {
29+
comp.stop();
30+
return;
31+
}
32+
33+
const result = this.getBranch(conditional);
34+
matchIndex = result.matchIndex
35+
html = result.html;
36+
37+
if (!comp.firstRun && this.matchIndex !== matchIndex) {
38+
this.matchIndex = matchIndex;
39+
this.setValue(html);
40+
}
41+
return html;
42+
}, { context });
43+
}
44+
else {
45+
const result = this.getBranch(conditional);
46+
matchIndex = result.matchIndex
47+
html = result.html;
48+
}
49+
50+
/* Experimental (not used currently *
6451
if(this.matchIndex == matchIndex) {
6552
return noChange;
66-
}
67-
*/
53+
} */
6854
return html;
6955
}
7056

57+
getBranch(conditional) {
58+
let matchIndex = -1;
59+
let html;
60+
if (conditional.condition()) {
61+
html = conditional.content();
62+
matchIndex = 1000; // special index for if condition
63+
}
64+
else if (conditional.branches?.length) {
65+
// evaluate each elseif/else branch
66+
each(conditional.branches, (branch, index) => {
67+
if(matchIndex === -1) {
68+
if (branch.type == 'elseif' && branch.condition()) {
69+
matchIndex = index;
70+
html = branch.content();
71+
}
72+
else if (branch.type == 'else') {
73+
matchIndex = index;
74+
html = branch.content();
75+
}
76+
}
77+
});
78+
}
79+
else {
80+
html = nothing;
81+
delete this.matchIndex;
82+
}
83+
if(!html) {
84+
html = nothing;
85+
}
86+
return { matchIndex, html };
87+
}
88+
7189
disconnected() {
7290
if (this.reaction) {
7391
this.reaction.stop();

packages/renderer/src/lit/directives/reactive-data.js

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { AsyncDirective } from 'lit/async-directive.js';
33
import { directive } from 'lit/directive.js';
44

55
import { Reaction } from '@semantic-ui/reactivity';
6-
import { inArray, isArray, isObject } from '@semantic-ui/utils';
6+
import { inArray, isServer, isArray, isObject, isClient } from '@semantic-ui/utils';
77
import { ifDefined } from 'lit/directives/if-defined.js';
88
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
99

@@ -26,7 +26,6 @@ export class ReactiveDataDirective extends AsyncDirective {
2626

2727
// Create a new reaction to rerun the computation function if reactive data updates
2828
// that dont trigger rerender occur
29-
3029
if (this.reaction) {
3130
// if reaction already set up just return value for rerender
3231
return this.getReactiveValue();
@@ -35,30 +34,41 @@ export class ReactiveDataDirective extends AsyncDirective {
3534
// Create a new reaction to rerun the computation function if reactive data updates
3635
// that dont trigger rerender occur
3736
let value;
38-
39-
const context = {
40-
message: `expression: {${expression.expression}}`,
41-
expression: expression.expression,
42-
};
43-
44-
this.reaction = Reaction.create((computation) => {
45-
if (!this.isConnected) {
46-
computation.stop();
47-
return;
48-
}
37+
if(isClient) {
38+
value = this.watchChanges();
39+
}
40+
else {
4941
value = this.getReactiveValue();
50-
if (this.settings.unsafeHTML) {
51-
value = unsafeHTML(value);
52-
}
53-
if (!computation.firstRun) {
54-
this.setValue(value);
55-
}
56-
}, { context });
57-
42+
}
5843
return value;
5944
}
6045
}
6146

47+
watchChanges() {
48+
const context = {
49+
message: `expression: {${this.expression.expression}}`,
50+
expression: this.expression.expression,
51+
};
52+
let value;
53+
this.reaction = Reaction.create((computation) => {
54+
if (!this.isConnected) {
55+
computation.stop();
56+
return;
57+
}
58+
value = this.getReactiveValue();
59+
if (this.settings.unsafeHTML) {
60+
value = unsafeHTML(value);
61+
}
62+
if (!computation.firstRun) {
63+
this.setValue(value);
64+
}
65+
}, { context });
66+
67+
// this returns the value for perf
68+
// otherwise we calculate twice on first run
69+
return value;
70+
}
71+
6272
getReactiveValue() {
6373
let reactiveValue = this.expression.value();
6474

packages/renderer/src/lit/directives/reactive-each.js

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { repeat } from 'lit/directives/repeat.js';
66
import { Reaction } from '@semantic-ui/reactivity';
77
import { isEmpty } from '@semantic-ui/utils';
88

9-
import { arrayFromObject, isArray, isPlainObject, isString } from '@semantic-ui/utils';
9+
import { arrayFromObject, isArray, isPlainObject, isString, isClient } from '@semantic-ui/utils';
1010

1111
export class ReactiveEachDirective extends AsyncDirective {
1212
constructor(partInfo) {
@@ -36,19 +36,23 @@ export class ReactiveEachDirective extends AsyncDirective {
3636
}
3737

3838
// Create a new reaction
39-
this.reaction = Reaction.create((computation) => {
40-
if (!this.isConnected) {
41-
computation.stop();
42-
return;
43-
}
44-
this.items = this.getItems(this.eachCondition);
45-
if (!computation.firstRun) {
46-
const rendered = this.renderItems();
47-
this.setValue(rendered);
48-
}
49-
}, { context });
50-
51-
return this.renderItems();
39+
let html = this.renderItems();
40+
41+
if(isClient) {
42+
this.reaction = Reaction.create((computation) => {
43+
if (!this.isConnected) {
44+
computation.stop();
45+
return;
46+
}
47+
this.items = this.getItems(this.eachCondition);
48+
if (!computation.firstRun) {
49+
html = this.renderItems();
50+
this.setValue(html);
51+
}
52+
}, { context });
53+
}
54+
55+
return html;
5256
}
5357

5458
renderItems() {

packages/renderer/src/lit/directives/render-template.js

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Reaction } from '@semantic-ui/reactivity';
22
import { Template } from '@semantic-ui/templating';
3-
import { isEqual, isString, mapObject } from '@semantic-ui/utils';
3+
import { isEqual, isClient, isString, mapObject } from '@semantic-ui/utils';
44
import { noChange, nothing } from 'lit';
55
import { AsyncDirective } from 'lit/async-directive.js';
66
import { directive } from 'lit/directive.js';
@@ -20,6 +20,22 @@ export class RenderTemplateDirective extends AsyncDirective {
2020
this.subTemplates = subTemplates;
2121
this.data = data;
2222
this.ast = null;
23+
24+
// Create a new reaction that watches for reactive changes on client
25+
if(isClient) {
26+
this.watchChanges();
27+
}
28+
29+
this.maybeCreateTemplate();
30+
31+
// this is an empty template
32+
if (!this.template || this.template?.ast.length == 0) {
33+
return nothing;
34+
}
35+
return this.renderTemplate();
36+
}
37+
38+
watchChanges() {
2339
this.reaction = Reaction.create((reaction) => {
2440
this.maybeCreateTemplate(); // reactive reference to template
2541
const dataContext = this.unpackData(this.data); // reactive reference to data
@@ -29,6 +45,7 @@ export class RenderTemplateDirective extends AsyncDirective {
2945
reaction.stop();
3046
return;
3147
}
48+
3249
// first run handled by main path
3350
if (reaction.firstRun) {
3451
return;
@@ -51,14 +68,6 @@ export class RenderTemplateDirective extends AsyncDirective {
5168
const html = this.renderTemplate(dataContext);
5269
this.setValue(html);
5370
});
54-
55-
this.maybeCreateTemplate();
56-
57-
// this is an empty template
58-
if (!this.template || this.template?.ast.length == 0) {
59-
return nothing;
60-
}
61-
return this.renderTemplate();
6271
}
6372

6473
renderTemplate(dataContext) {

0 commit comments

Comments
 (0)