Synchronous `setState` can cause incorrect output
robertknight opened this issue · 0 comments
The adapter monkey-patches a component's setState
method so that it triggers a synchronous update, as opposed to Preact's default behaviour which is to mark the component as "dirty" and schedule a future re-render. This can cause incorrect output if a state setter returned by useState
is called in the middle of a render, a pattern that the React docs specifically recommend for certain use cases.
Steps to reproduce:
Consider this test case of an input field which renders an initial value
and allows the user to edit the value, but resets it whenever the value
prop changes. The component uses a React hooks idiom for updating state in response to prop changes.
let n = 0;
function InputWidget({ value }) {
const rid = n++;
const [prevValue, setPrevValue] = useState(value);
const [pendingValue, setPendingValue] = useState(value);
if (value !== prevValue) {
setPrevValue(value);
setPendingValue(value);
}
console.log(`render ${rid} with value ${value}, prevValue ${prevValue}, pendingValue ${pendingValue}`);
return (
<input
value={pendingValue}
onInput={e => setPendingValue(e.target.value)}
/>
);
}
const wrapper = mount(<InputWidget value="foo"/>);
// Check initial render.
assert.equal(wrapper.find('input').prop('value'), 'foo');
// Simulate user typing and check that their edit is displayed.
wrapper.find('input').getDOMNode().value = 'pending-value';
wrapper.find('input').simulate('input');
assert.equal(wrapper.find('input').prop('value'), 'pending-value');
// Change prop and check that user-entered value was overwritten.
wrapper.setProps({ value: 'bar' });
// Assert fails with `expected 'pending-value' to equal 'bar'`
assert.equal(wrapper.find('input').prop('value'), 'bar');
Expected result:. Test passes
Actual result:
Test fails on the last assertion. The console log output is:
LOG: 'render 0 with value foo, prevValue foo, pendingValue foo'
LOG: 'render 1 with value foo, prevValue foo, pendingValue pending-value'
LOG: 'render 3 with value bar, prevValue bar, pendingValue pending-value'
LOG: 'render 4 with value bar, prevValue bar, pendingValue bar'
LOG: 'render 2 with value bar, prevValue foo, pendingValue pending-value'
The synchronous updates caused by the set{PrevValue, PendingValue}
calls in the middle of a render result in incorrect final output, because renders 3 & 4 start after 2 but complete before it.
Workaround
Commenting out the setState
monkey-patching in the adapter fixes the problem.