Downshift: Force inputValue update

Created on 24 Oct 2019  ·  17Comments  ·  Source: downshift-js/downshift

Is there a reason why inputValue cannot be set directly? There are several scenarios that I have come across in which either setting inputValue to a value from an event handler would be useful OR forcing a stateReducer update with an arbitrary stateChange Type (e.g. "__autocomplete_forceUpdate__") would be handy. As is, I sometimes 'hack' the state reducer to fire using openMenu or closeMenu when the menu is already actually in one of those two current states to trigger a state update when I want to update inputValue manually. For example, there is a scenario in which I want to get inputValue set to a new value that comes from an updated state value in my component after downshift has already been initialized on first render. So what I do is while the menu is closed, onFocus, I fire closeMenu(), and with some additional conditional logic to exclude unwanted stateUpdates, I am able to cycle in this new value to the inputValue using the stateReducer because closeMenu triggers the stateReducer. But I must leave comments behind explaining why I am using closeMenu() when the state of isOpen is false.

Seems like a dedicated forceStateUpdate would be reasonable.

question

Most helpful comment

This isn't using the stateReducer but instead the onStateChange function, and this is exactly what it was designed for?

See https://codesandbox.io/s/github/kentcdodds/downshift-examples/tree/master/?module=%2Fsrc%2Fordered-examples%2F03-typeahead.js&moduleview=1

All 17 comments

I don't understand. Can't you control the inputValue and do whatever you'd like?

No. Not if inputValue is handled inside of Downshift rather than passed in as a prop. I can't share code, unfortunately, but I can describe it.

If I use an onBlur event and set inputValue = {myVar} where myVar is a state const set using react hooks (setMyVar('somevalue')), nothing happens to the value of inputValue.

I can force a stateReducer update by using an onBlur event and run a function such as menuOpen() or menuClose() because these trigger the stateReducer. Inside the stateReducer, I use some logic and say something like if state.inputValue !== myVar then return { ...changes, inputValue: myVar }.

This effectively updates inputValue, but like I said, I have to use something that triggers the stateReducer which is arbitrary and a little hacky

^ I should clarify that this is not specific to onBlur, that was just the event I chose for the example. Setting inputValue = {myVar} anywhere inside of Downshift does nothing on my implementations

I just can't wrap my head around the problem. Generally it shouldn't be required to set state from event handlers. We have control props, we have onChange handlers, state change handlers, state reducer ... Indeed a codesandbox may help.

@silviuavram - thanks for the reply. The problem is not that I would like to trigger a state change from an event handler. Rather triggering a state reducer from an event has become the solution to the problem.

The problem here is that, in this case, my downshift instance manages inputValue state. However, I would like to override the state of inputValue from outside of the downshift instance in a few cases. But there is no way to assign a value to inputValue except for on initial render or from within the component using the state reducer.

A possible solution would be to create a manual “update state” prop where if the prop passed into downshift changes, then the state reducer is fired and the incoming prop value can be cycled into the state reducer.

Again, I apologize, but for this I cannot share code.

@MatthewPardini I think what you're looking for is this

You could do

function Component (props) {
  const [inputValue, setInputValue] = useState('');

  return (
    <Downshift
      inputValue={inputValue}
      onStateChange={(changes, stateAndHelpers) => {
        if (changes.hasOwnProperty('inputValue')) {
          setInputValue(changes.inputValue);
        }
      }}
    >
      {...}
    </Downshift>
  )
}

then outside of downshift you can do whatever else you want with inputValue and it will be reflected in the downshift state - just being careful that downshift doesn't override the inputValue state in ways that you don't want it to, but that can be done with conditionals in the onStateChange function.

All due respect, that is an anti-pattern. The stateReducer should never have side effects and instead only return state. In addition, updating inputValue (the component state) does not trigger the state reducer, so
if (changes.hasOwnProperty('inputValue')){ ... }
is never evaluated.

This isn't using the stateReducer but instead the onStateChange function, and this is exactly what it was designed for?

See https://codesandbox.io/s/github/kentcdodds/downshift-examples/tree/master/?module=%2Fsrc%2Fordered-examples%2F03-typeahead.js&moduleview=1

Ah, thank you. I missed that. Does this work for an inputValue that is controlled by stateReducer? Mine is and it needs to be, except when I want to cycle in a new value from the outside

Basically, I want

onStateChange={(changes, stateAndHelpers) => { if (changes.hasOwnProperty('inputValue')) { **TRIGGER STATE REDUCER** } }}

And I would want to pass into the state reducer as a change of type "autocomplete_CUSTOMUPDATE" changes.inputValue

The onStateChange function will be run on every internal state change and will be passed the _reduced_ state in the stateAndHelpers param. So you can still manipulate the inputValue in the stateReducer if you need to, but it happens before that function is run.

I need the state updates to flow from external to internal, not internal to external. onStateChange is designed to update component state (external) when internal state is changed. I want an external state change (from the component) to be passed into downshift (internal). This is possible with initialInputValue. But I want to do it AFTER the input value has been initialized.

My recommended way of doing so would be to use the stateReducer with a new change type that can be triggered from events, much the way blur triggers a state change now.

wait sorry... It's the end of my day and I'm getting sloppy. Sorry... blur does not trigger a state change event. But menuOpen() does. My current work around has a menuOpen() that fires onBlur so that the stateReducer fires and I can pass the new value into the state reducer this way.

The example from above has a state flow that is more like internal -> external -> internal where since you are controlling the inputValue prop then downshift will respect it, but you might not want to have to handle all of the state changes that inputValue goes through by yourself so you delegate some of that work to downshift with the onStateChange function. There is nothing stopping you from doing whatever you like with the external inputValue that you define.

Taking my example from before you could have a button that changes the inputValue but does so from outside of the Downshift component where you don't have access to all of the inner workings.

function Component (props) {
  const [inputValue, setInputValue] = useState('');

  return (
    <>
      <buton onClick={() => {
        setInputValue('Some random value');
      }}>Click me to imperatively change inputValue</buton>
      <Downshift
        inputValue={inputValue}
        onStateChange={(changes, stateAndHelpers) => {
          if (changes.hasOwnProperty('inputValue')) {
            setInputValue(changes.inputValue);
          }
        }}
      >
        {...}
      </Downshift>
    </>
  )
}

I may be misunderstanding your problem though...

If you need a state change onBlur of the input field within downshift can you do this?

function Component (prop) {
  return (
    <Downshift>
      {({ setState, getInputProps }) => (
        <div>
          <input {...getInputProps({
            onBlur: () => {
              setState({ inputValue: 'Whatever I want' }, { type: 'CUSTOM STATE CHANGE TYPE' });
            }
          })} />
        </div>
      )}
    </Downshift>
  )
}

or, if you are trying to manipulate the inputValue onBlur I have had to do this to work around an issue with the blur state change type - might be a bug in downshift I'm not sure but this has worked for me.

  const stateReducerOverride = React.useCallback(
    (state, changes) => {
      let newChanges = changes;

      switch (changes.type) {
        case Downshift.stateChangeTypes.mouseUp:
        case Downshift.stateChangeTypes.blurInput:
          if (shouldBlurResetInput) {
            newChanges = {
              ...newChanges,
              inputValue: ''
            };
          }
          break;
      }

      return stateReducer(state, newChanges);
    },
    [stateReducer, shouldBlurResetInput]
  );

I'm not sure why the Downshift.stateChangeTypes.mouseUp is fired when I expect the blur event but I have not run into any issues with this yet, I may be relying on buggy behaviour here so take this one with a grain of salt.

@aaronnuu yes I think you are misunderstanding the issue. Not sure why the team closed this. The above code does not address the issue of being able to cycle in a new value into the inputValue state after initialization

@MatthewPardini I was in a similar boat to you, i think the solution that i found worked the best was to set a reference for the Downshift instance, and then call something like .reset on it, setting the inputValue as part of the reset. Have a look at something like this example on codesandbox

@haxxxton I was pretty hopeful of your suggested solution, but it did not work for my situation. The reason was that although .reset does tear down the component and rebuild, therefor triggering the initialInputValue to be evaluated again, it is only accessible inside an event handler which makes it inaccessible in this particular situation.

But it did lead to another solution. I placed a key={myState} on the downshift component so that when myState updates, it tears down and rebuilds. This seems to work.

Thanks for sparking the idea!

Was this page helpful?
0 / 5 - 0 ratings