qiniu / formstate-x

Manage state of form with ease.
https://qiniu.github.io/formstate-x
MIT License
33 stars 10 forks source link

Opt-in debouncing #61

Closed nighca closed 2 years ago

nighca commented 2 years ago

With useDeferredValue (and startTransition) in React 18, the debounce between (FieldState's) value & _value may be unnecessary.

Current behavior:

https://github.com/qiniu/formstate-x/blob/bd4ec5e16194949a224fc7b183e5ea59224351ea/src/fieldState.ts#L30-L34

nighca commented 2 years ago

Some other concerns:

1. Confusion

In our practices in Qiniu, it is a common mistake to misuse value & _value - although the differences are documented here.

2. bindInput

bindInput is important for avoid misuse of value & _value, but it is not natural for beginners.

3. Unnecessary

Debounce is unnecessary for most inputs (which will not change frequently).

nighca commented 2 years ago

Proposal

The proposal includes two parts:

  1. Remove debouncing from FieldState
  2. Provide class DebouncedState & DebouncedFieldState for certain situations

Certain situations includes:

Implementation:

class DebouncedState wraps another state, and reflect its change, while with a certain delay.

class DebouncedFieldState is a simple shortcut for DebouncedState + FieldState, which acts like a FieldState while with a certain delay (which is almost the same as legacy FieldState).

class DebouncedState<V> implements IState<V> {

  constructor(public $: IState<V>, delay = 200) {
    // ...
  }

  // ...
}

class DebouncedFieldState<V> extends DebouncedState implements IState<V> {
  constructor(public initialValue: V, delay = 200) {
    super(new FieldState(initialValue), delay)
  }
}

Usage for DebouncedFieldState

State initialization:

const nameField = new DebouncedFieldState('foo').validators(...)

State binding:

const nameFieldForBinding = nameField.$

// HTML `input`
<input value={nameFieldForBinding.value} onChange={e => nameFieldForBinding.onChange(e.target.value)} />

// react-icecream `TextInput`
<TextInput value={nameFieldForBinding.value} onChange={nameFieldForBinding.onChange} />

// react-icecream `form-x/TextInput`
<TextInput state={nameFieldForBinding} />

State operation:

const formState = new FormState({
  name: nameField
})

const value = formState.value
value.name // 'foo'
formState.set({ name: 'bar' }) // or: nameField.set('bar')
value.name // bar

Usage for DebouncedState

With DebouncedState, you can add debounce to any kind of state in addition to FieldState:

const fullNameState = new FormState({
  first: new FieldState('foo'),
  last: new FieldState('bar')
})

const debouncedFullNameState = new DebouncedState(fullNameState)

// For details of ProxyState, see https://github.com/qiniu/formstate-x/issues/49
const fullNameTextState = new ProxyState(
  fullNameState,
  ({ first, last }) => `${first} ${last}`,
  fullName => {
    const [first, last] = fullName.split(' ')
    return { first, last }
  }
)

const debouncedFullNameTextState = new DebouncedState(fullNameTextState)

When to use

Based on certain situations explained above.

Use DebouncedFieldState instead of FieldState when the field state is intended to be bound to a text input (like HTML input type="text", HTML textarea, react-icecream TextInput, ...)

Use DebouncedState when you want to add certain delay between the value change & value consumption — value consumption may be expensive, while other tools like React useDeferredValue & startTransition are not proper for the situation.