moroshko / react-autosuggest

WAI-ARIA compliant React autosuggest component
http://react-autosuggest.js.org
MIT License
5.97k stars 587 forks source link

focusInputOnSuggestionClick throwing error when creating input ref #469

Open marclemagne opened 7 years ago

marclemagne commented 7 years ago

Came across an error with focusInputOnSuggestionClick when trying to automatically focus the input field.

https://codepen.io/marclemagne/pen/RjRdvr

  1. Create an input using renderInputComponent
  2. Create a ref to the input on the class (e.g., autosuggestInput)
  3. Type c, and wait for suggestions to appear
  4. Observe in the console the following error:
autosuggest.js:698 Uncaught TypeError: Cannot read property 'focus' of undefined
    at Object.onSuggestionClick [as onClick] (autosuggest.js:698)
    at Item._this.onClick (autosuggest.js:2941)
    at Object.ReactErrorUtils.invokeGuardedCallback (react-dom.js:9017)
    at executeDispatch (react-dom.js:3006)
    at Object.executeDispatchesInOrder (react-dom.js:3029)
    at executeDispatchesAndRelease (react-dom.js:2431)
    at executeDispatchesAndReleaseTopLevel (react-dom.js:2442)
    at Array.forEach (<anonymous>)
    at forEachAccumulated (react-dom.js:15423)
    at Object.processEventQueue (react-dom.js:2645)

Questions I have:

  1. Is there a problem with how we're trying autofocus the input field?
  2. Is there a more canonical way to set focus when the component loads?

Thanks!

moroshko commented 7 years ago

Are you using renderInputComponent only to autofocus the input? If yes, check out this example that doesn't use renderInputComponent.

If you really want to customize the input, here is how you should access it: Codepen

iki commented 6 years ago

@moroshko I'm getting the same error often. I need to use Material-UI Next TextField. It's a functional stateless component, so I had to wrap it, so refs can be attached to it. But it still throws the above error.

import TextField from 'material-ui/TextField'

// Wrap function or functional stateless component as react class component.
// Useful e.g. if you need to reference the component in React 16.
class Componentize extends React.Component<any, null> {
  public render() {
    const { Component, component, ...props } = this.props
    return Component ? <Component {...props} /> : component(props)
  }
}

const renderInput = props => <Componentize Component={TextField} {...props} />

It's at https://github.com/moroshko/react-autosuggest/blob/master/src/Autosuggest.js#L382

marclemagne commented 6 years ago

Thanks, @moroshko! Sorry I'm just returning to this now.

Some follow-up with my experience and how (for better or worse) I was able to get things working.

Initially, I was working with an input styled-component and my goal was to have it focus automatically.

public input: HTMLInputElement;

public componentDidMount() {
  this.input.focus();
}

public storeInputReference = (autosuggest) => {
  if (autosuggest != null) {
    this.input = autosuggest.input;
  }
}

public renderInputComponent = (inputProps) => {
  return (
    <TextInput {...inputProps} innerRef={inputProps.ref} ref={null} />
  );
}

And in the Autosuggest component add:

renderInputComponent={this.renderInputComponent}
ref={this.storeInputReference}

Note the innerRef in the renderInputComponent method. This is what allows you to access the input element within a styled-component.

I am (still) not actually 100% clear on what the ref={null} is for, but everything was working as I needed. I saw it in another issue thread.


Eventually I extended our TextInput component to focus or select itself which added a complication with my usage of AutoSuggest.

I upgraded the styled-component to be a React class component and had it create its own local ref in order to focus/select.

What I was doing above no longer worked. The TextInput would focus, as expected, and AutoSuggest would function nicely after removing the focus-related code in the AutoSuggest parent class—but I was back to getting the JavaScript TypeError.

So here's what I am doing now:

I removed ref={this.storeInputReference} from the Autosuggest component. I removed the componentDidMount lifecycle method, as well.

public input: HTMLInputElement;

public renderInputComponent = (inputProps) => {
  return (
    <TextInput
      focused
      {...inputProps}
      getRef={(input) => this.input = input}
      ref={() => inputProps.ref(this.input)}
    />
  );
}

In our TextInput component I added a new prop called getRef which returns its local version of this.input in its componentDidMount lifecycle method. In TextInput this.input ref is set in the standard way (ref={input => this.input = input}).

Back to where I'm using AutoSuggest, I noticed that inputProps.ref was returning a function no matter if ref={...} was a prop on Autosuggest or not. So I am using the getRef prop from my TextInput component to get the TextInput input ref and setting that reference locally where I'm using AutoSuggest. Then in the ref prop being passed into TextInput, I am calling that inputProps.ref function to (ostensibly) let Autosuggest know about the reference to the input.

No more error and everything is working correctly.

It's not exactly elegant but it seems to be working. I am sure there has got to be a cleaner, simpler way to achieve this. At the end of the day, though, it seems that AutoSuggest needs a reference to the input or it's going to throw an error.

I am wondering if AutoSuggest should do an existence check on this.input before trying to focus using it.

I hope this proves to be somewhat helpful for anybody else having similar issues.

kylorhall commented 6 years ago

@marclemagne Thanks so much for that last comment. There's like a dozen issues around refs, but this one finally solved it for me. Debugging a custom input that is a styled-component, not necessarily needing the ref as you do, ref={null} was the key.

Stephane-Ag commented 2 years ago

Unbelievable. This error was really not clear to me. But after trying so many things, the solution ended being as simple as moving the ref that was being assigned from the renderInputComponent function return to the actual Autosuggest component.

...
  const renderInputComponent = inputCompProps => (
    <input
      {...inputCompProps}
      // ref={inputRef} // NOT HERE
      styleName="input-field"
    />
  );

  return (
    <div>
      <Autosuggest
        id={id}
        alwaysRenderSuggestions={alwaysRenderSuggestions}
        focusInputOnSuggestionClick={false}
        getSuggestionValue={getSuggestionValue}
        highlightFirstSuggestion
        inputProps={inputProps}
        onSuggestionsClearRequested={handleSuggestionsClearRequested}
        onSuggestionSelected={handleSuggestionSelected}
        onSuggestionsFetchRequested={handleSuggestionsFetchRequested}
        ref={inputRef} // GOES HERE
        renderInputComponent={renderInputComponent}
        renderSuggestion={renderSuggestion}
        suggestions={suggestions}
      />
    </div>
  );
...