Repository: h2oai/wave
File: ui/src/combobox.tsx (TypeScript)

Code before:
  const
    [options, setOptions] = useOptions(m.choices),
    [selected, setSelected] = React.useState<S | null>(m.value ?? null),
    skipUseEffectForValue = React.useRef(false),
    onChange = (_e: React.FormEvent<IComboBox>, option?: IComboBoxOption, _index?: U, value?: S) => {
      if (!option && value) setOptions((prevOptions = []) => [...prevOptions, { key: value, text: value }])
      
      const v = option?.text || value || ''
      setSelected(v)
      
      wave.args[m.name] = v
      if (m.trigger) wave.push()

      // Hacky: Ensure that next dynamically change to value from Wave app will trigger the useEffect
      m.value = v
      // The line above will trigger the useEffect for change in m.value and we need to skip it
      skipUseEffectForValue.current = true
    }
  
  React.useEffect(() => {
    wave.args[m.name] = (selected && options.map(o => String(o.key)).includes(selected || '')) ? selected : null
  }, [m.name, m.value, options, selected])

  // Select value when "value" is set from Wave App dynamically
  React.useEffect(() => {
    if (!skipUseEffectForValue.current) setSelected(m.value!)
    skipUseEffectForValue.current = false
  }, [m.value])

  return (
    <Fluent.ComboBox
      data-test={m.name}
      label={m.label}
      placeholder={m.placeholder}
      options={options}
      required={m.required}
      selectedKey={selected}
      allowFreeform
      disabled={m.disabled}
      autoComplete="on"
      errorMessage={m.error}
      onChange={onChange}
    />
  )
}

const ComboboxMultiSelect = ({ model: m }: { model: Omit<Combobox, 'value'> }) => {
  const
    [options, setOptions] = useOptions(m.choices),
    [selected, setSelected] = React.useState<S[]>(m.values ?? []),
    selectOpt = (option: IComboBoxOption) => {

Review comment (suggestion):
```suggestion
    if (!skipUseEffectForValue.current) setSelected(m.value || null)
```

Code after:
  const
    [options, setOptions] = useOptions(m.choices),
    [selected, setSelected] = React.useState<S | null>(m.value ?? null),
    onChange = (_e: React.FormEvent<IComboBox>, option?: IComboBoxOption, _index?: U, value?: S) => {
      const v = (option && String(option.key)) || value || ''
      setSelected(v)

      // Hacky: Ensure that next "model.value" set from a Wave App will be different and trigger the second "useEffect"
      m.value = v

      wave.args[m.name] = v
      if (m.trigger) wave.push()
    }

  React.useEffect(() => { wave.args[m.name] = selected }, [m.name, selected])

  // Whenever a new "value" is set in a Wave App, set it as the current value and add it to options list if it's not included yet
  React.useEffect(() => {
    setSelected(m.value || null)
    if (m.value && !(m.choices || []).includes(m.value)) {
      setOptions((prevOptions = []) => [...prevOptions, { key: m.value!, text: m.value! }])
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [m.value])

  return (
    <Fluent.ComboBox
      data-test={m.name}
      label={m.label}
      placeholder={m.placeholder}
      options={options}
      required={m.required}
      selectedKey={selected}
      allowFreeform
      disabled={m.disabled}
      autoComplete="on"
      errorMessage={m.error}
      onChange={onChange}
    />
  )
}

const ComboboxMultiSelect = ({ model: m }: { model: Omit<Combobox, 'value'> }) => {
  const
    [options, setOptions] = useOptions(m.choices),
    [selected, setSelected] = React.useState<S[]>(m.values ?? []),
    selectOpt = (option: IComboBoxOption) => {
      setSelected(keys => {
        const result = option.selected ? [...keys, String(option.key)] : keys.filter(key => key !== option.key)
        wave.args[m.name] = result
        return result