Repository: preactjs/preact-render-to-string (js)

Issue:
setState discrepancy between preact-render-to-string and react-dom/server
We've noticed a discrepancy in how preact / react server renders a component that looks like this:


```
// Note: this app is set up to SSR only, no CSR
export default function App() {
  const [x, setX] = React.useState(0);
  var foo = undefined;

  React.useMemo(() => {
    foo = "foo";
    setX(1);
  }, []);

  return (
    <>
      <pre>x: {x}</pre>
      <pre>foo: {foo}</pre>
    </>
  );
}
```

Code sandboxes for preact and react to compare the output:

* [preact code example](https://codesandbox.io/s/preact-usememo-setstate-example-mrclje?file=/src/App.js)
* [react code example](https://codesandbox.io/s/react-usememo-setstate-example-v9tzg4?file=/src/App.js)

In preact, it appears as though the `setState` is not called. In react however, we are getting one initial call on `setState` here, and the output is `x: 0` vs. `x: 1`. 

This isn't very idiomatic react or preact code so I think we can migrate examples that have this issue, but I was curious about whether this is an intended discrepancy, or if this is a bug. 

Maybe this is a similar issue to what was raised in https://github.com/preactjs/preact-render-to-string/issues/108 , but with a functional component instead of a class component.

Interested to hear what you think anyway!

Pull request:
Implement hook state settling
This implements hooks state "settling", where a function component that invokes setState/forceUpdate during rendering is re-invoked up to 25 times or until it stops invalidating during invocation.

The implementation here is for Preact 10, and will need to be updated to use Internal.flags in Preact 11 (however RTS is moved to core in 11).

Fixes #218.

Patch:
diff --git a/.changeset/chilly-ligers-agree.md b/.changeset/chilly-ligers-agree.md
new file mode 100644
index 0000000..b7047b3
--- /dev/null
+++ b/.changeset/chilly-ligers-agree.md
@@ -0,0 +1,5 @@
+---
+"preact-render-to-string": minor
+---
+
+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.
diff --git a/src/index.js b/src/index.js
index 2c6f682..6c2b24a 100644
--- a/src/index.js
+++ b/src/index.js
@@ -19,7 +19,9 @@ const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|sou
 
 const UNSAFE_NAME = /[\s\n\\/='"\0<>]/;
 
-const noop = () => {};
+function markAsDirty() {
+	this.__d = true;
+}
 
 /** Render Preact JSX + Components to an HTML string.
  *	@name render
@@ -124,8 +126,9 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
 				context,
 				props: vnode.props,
 				// silently drop state updates
-				setState: noop,
-				forceUpdate: noop,
+				setState: markAsDirty,
+				forceUpdate: markAsDirty,
+				__d: true,
 				// hooks
 				__h: []
 			});
@@ -134,7 +137,7 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
 			if (options.__b) options.__b(vnode);
 
 			// options._render
-			if (options.__r) options.__r(vnode);
+			let renderHook = options.__r;
 
 			if (
 				!nodeName.prototype ||
@@ -151,8 +154,20 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
 							: cxType.__
 						: context;
 
-				// stateless functional components
-				rendered = nodeName.call(vnode.__c, props, cctx);
+				// If a hook invokes setState() to invalidate the component during rendering,
+				// re-render it up to 25 times to allow "settling" of memoized states.
+				// Note:
+				//   This will need to be updated for Preact 11 to use internal.flags rather than component._dirty:
+				//   https://github.com/preactjs/preact/blob/d4ca6fdb19bc715e49fd144e69f7296b2f4daa40/src/diff/component.js#L35-L44
+				let count = 0;
+				while (c.__d && count++ < 25) {
+					c.__d = false;
+
+					if (renderHook) renderHook(vnode);
+
+					// stateless functional components
+					rendered = nodeName.call(vnode.__c, props, cctx);
+				}
 			} else {
 				// class-based components
 				let cxType = nodeName.contextType;
@@ -195,6 +210,8 @@ function _renderToString(vnode, context, opts, inner, isSvgMode, selectValue) {
 							: c.state;
 				}
 
+				if (renderHook) renderHook(vnode);
+
 				rendered = c.render(c.props, c.state, c.context);
 			}
 


Test patch:
diff --git a/test/render.test.js b/test/render.test.js
index 7f64c42..4ef1ce4 100644
--- a/test/render.test.js
+++ b/test/render.test.js
@@ -1,6 +1,12 @@
 import { render, shallowRender } from '../src';
 import { h, Component, createContext, Fragment, options } from 'preact';
-import { useState, useContext, useEffect, useLayoutEffect } from 'preact/hooks';
+import {
+	useState,
+	useContext,
+	useEffect,
+	useLayoutEffect,
+	useMemo
+} from 'preact/hooks';
 import { expect } from 'chai';
 import { spy, stub, match } from 'sinon';
 
@@ -1052,12 +1058,48 @@ describe('render', () => {
 		});
 
 		it('should work with useState', () => {
+			let renders = 0;
+
 			function Foo() {
+				renders++;
 				let [v] = useState(0);
 				return <div>{v}</div>;
 			}
 
 			expect(render(<Foo />)).to.equal('<div>0</div>');
+			expect(renders).to.equal(1);
+		});
+
+		it('should re-render when useState setter is called during rendering', () => {
+			let renders = 0;
+
+			function Foo() {
+				renders++;
+				let [v, setV] = useState(0);
+				useMemo(() => {
+					setV(1);
+				}, []);
+				return <div>{v}</div>;
+			}
+
+			expect(render(<Foo />)).to.equal('<div>1</div>');
+			expect(renders).to.equal(2);
+		});
+
+		it('should re-render up to 25 times to allow useState settling', () => {
+			let renders = 0;
+
+			function Foo() {
+				renders++;
+				let [v, setV] = useState(0);
+				if (v < 30) {
+					setV(v + 1);
+				}
+				return <div>{v}</div>;
+			}
+
+			expect(render(<Foo />)).to.equal('<div>24</div>');
+			expect(renders).to.equal(25);
 		});
 
 		it('should not trigger useEffect callbacks', () => {