preactjs/enzyme-adapter-preact-pure

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.