Skip to content

Commit 250c15f

Browse files
authoredApr 29, 2022
Implement hook state settling (#219)
* Implement hook state settling (fixes #218) * Create chilly-ligers-agree.md
1 parent c1eb8c4 commit 250c15f

File tree

3 files changed

+71
-7
lines changed

3 files changed

+71
-7
lines changed
 

‎.changeset/chilly-ligers-agree.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"preact-render-to-string": minor
3+
---
4+
5+
Implement hook state settling. Setting hook state during the execution of a function component (eg: in `useMemo`) will now re-render the component and use the final result. Previously, these updates were dropped.

‎src/index.js

+23-6
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|sou
1919

2020
const UNSAFE_NAME = /[\s\n\\/='"\0<>]/;
2121

22-
const noop = () => {};
22+
function markAsDirty() {
23+
this.__d = true;
24+
}
2325

2426
/** Render Preact JSX + Components to an HTML string.
2527
* @name render
@@ -124,8 +126,9 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
124126
context,
125127
props: vnode.props,
126128
// silently drop state updates
127-
setState: noop,
128-
forceUpdate: noop,
129+
setState: markAsDirty,
130+
forceUpdate: markAsDirty,
131+
__d: true,
129132
// hooks
130133
__h: []
131134
});
@@ -134,7 +137,7 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
134137
if (options.__b) options.__b(vnode);
135138

136139
// options._render
137-
if (options.__r) options.__r(vnode);
140+
let renderHook = options.__r;
138141

139142
if (
140143
!nodeName.prototype ||
@@ -151,8 +154,20 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
151154
: cxType.__
152155
: context;
153156

154-
// stateless functional components
155-
rendered = nodeName.call(vnode.__c, props, cctx);
157+
// If a hook invokes setState() to invalidate the component during rendering,
158+
// re-render it up to 25 times to allow "settling" of memoized states.
159+
// Note:
160+
// This will need to be updated for Preact 11 to use internal.flags rather than component._dirty:
161+
// https://github.com/preactjs/preact/blob/d4ca6fdb19bc715e49fd144e69f7296b2f4daa40/src/diff/component.js#L35-L44
162+
let count = 0;
163+
while (c.__d && count++ < 25) {
164+
c.__d = false;
165+
166+
if (renderHook) renderHook(vnode);
167+
168+
// stateless functional components
169+
rendered = nodeName.call(vnode.__c, props, cctx);
170+
}
156171
} else {
157172
// class-based components
158173
let cxType = nodeName.contextType;
@@ -195,6 +210,8 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
195210
: c.state;
196211
}
197212

213+
if (renderHook) renderHook(vnode);
214+
198215
rendered = c.render(c.props, c.state, c.context);
199216
}
200217

‎test/render.test.js

+43-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { render, shallowRender } from '../src';
22
import { h, Component, createContext, Fragment, options } from 'preact';
3-
import { useState, useContext, useEffect, useLayoutEffect } from 'preact/hooks';
3+
import {
4+
useState,
5+
useContext,
6+
useEffect,
7+
useLayoutEffect,
8+
useMemo
9+
} from 'preact/hooks';
410
import { expect } from 'chai';
511
import { spy, stub, match } from 'sinon';
612

@@ -1052,12 +1058,48 @@ describe('render', () => {
10521058
});
10531059

10541060
it('should work with useState', () => {
1061+
let renders = 0;
1062+
10551063
function Foo() {
1064+
renders++;
10561065
let [v] = useState(0);
10571066
return <div>{v}</div>;
10581067
}
10591068

10601069
expect(render(<Foo />)).to.equal('<div>0</div>');
1070+
expect(renders).to.equal(1);
1071+
});
1072+
1073+
it('should re-render when useState setter is called during rendering', () => {
1074+
let renders = 0;
1075+
1076+
function Foo() {
1077+
renders++;
1078+
let [v, setV] = useState(0);
1079+
useMemo(() => {
1080+
setV(1);
1081+
}, []);
1082+
return <div>{v}</div>;
1083+
}
1084+
1085+
expect(render(<Foo />)).to.equal('<div>1</div>');
1086+
expect(renders).to.equal(2);
1087+
});
1088+
1089+
it('should re-render up to 25 times to allow useState settling', () => {
1090+
let renders = 0;
1091+
1092+
function Foo() {
1093+
renders++;
1094+
let [v, setV] = useState(0);
1095+
if (v < 30) {
1096+
setV(v + 1);
1097+
}
1098+
return <div>{v}</div>;
1099+
}
1100+
1101+
expect(render(<Foo />)).to.equal('<div>24</div>');
1102+
expect(renders).to.equal(25);
10611103
});
10621104

10631105
it('should not trigger useEffect callbacks', () => {

0 commit comments

Comments
 (0)
Please sign in to comment.