0

I am converting Class based to Function Based React Component but for some reason I am having trouble achieving the same result. I have a suspicion that it might be related to this.ref. Here is the code to let me know what I am doing wrong. Here is the link to codesandbox.

Here is the error I am getting: enter image description here

Class based component

import React, { Component } from 'react';

import { getSymbolData } from './utils.js';

import AsyncSelect from 'react-select/async';

class MyAsyncSelect extends Component {
  /* Select component reference can be used to get currently focused option */
  getFocusedOption() {
    return this.ref.select.select.state.focusedOption;
  }

  // we'll store lastFocusedOption as instance variable (no reason to use state)
  componentDidMount() {
    this.lastFocusedOption = this.getFocusedOption();
  }

  // Select component reference can be used to check if menu is opened */
  isMenuOpen() {
    return this.ref.select.state.menuIsOpen;
  }

  // This function will be called after each user interaction (click, keydown, mousemove).
  // If menu is opened and focused value has been changed we will call onFocusedOptionChanged
  // function passed to this component using props. We do it asynchronously because onKeyDown
  // event is fired before the focused option has been changed.
  onUserInteracted = () => {
    Promise.resolve().then(() => {
      const focusedOption = this.getFocusedOption();
      if (this.isMenuOpen() && this.lastFocusedOption !== focusedOption) {
        this.lastFocusedOption = focusedOption;
        this.props.onFocusedOptionChanged(focusedOption);
      }
    });
  };

  toggleClearable = () =>
    this.setState({ isClearable: !this.props.isClearable });

  onInputChange = (_, { action }) => {
    if (action === 'set-value') {
      this.props.onOptionSelected(this.getFocusedOption());
    }
  };

  loadOptions = async (inputText) => {
    const symbolData = await getSymbolData(inputText);
    console.log('symbolDATA: ', symbolData);
    return symbolData;
  };

  // we're setting onUserInteracted method as callback to different user interactions
  render() {
    console.log('RENDER Child');
    console.log('Props MyAsyncSelect', this.props);
    return (
      <div onMouseMove={this.onUserInteracted} onClick={this.onUserInteracted}>
        <AsyncSelect
          {...this.props}
          ref={(ref) => (this.ref = ref)}
          onKeyDown={this.onUserInteracted}
          onInputChange={this.onInputChange}
          loadOptions={this.loadOptions}
          autoFocus
          noOptionsMessage={() => 'Search symbol'}
          placeholder="Search Symbol"
          isClearable={this.props.isClearable} // allows us to clear the selected value either using the backspace button or the “x” button on the right side of the field
          clear // Removing all selected options using the clear button
          pop-value // Removing options using backspace
          loadingIndicator
        />
      </div>
    );
  }
}
export default MyAsyncSelect;

Function based component

import React, { useCallback, useEffect, useRef } from 'react';

import { getSymbolData } from './utils.js';

import AsyncSelect from 'react-select/async';

const MyAsyncSelect = (props) => {
  // let myRef = useRef();
  let myRef = React.createRef();
  const myRefLastFocusedOption = useRef();
  // let myRefLastFocusedOption = React.createRef();

  const getFocusedOption = useCallback(() => {
    console.log(myRef);
    // @ts-ignore
    return myRef.select.select.state.focusedOption;
  }, [myRef]);

  const isMenuOpen = () => {
    // @ts-ignore
    return myRef.select.state.menuIsOpen;
  };

  const onUserInteracted = () => {
    Promise.resolve().then(() => {
      const focusedOption = getFocusedOption();
      if (isMenuOpen() && myRefLastFocusedOption.current !== focusedOption) {
        myRefLastFocusedOption.current = focusedOption;
        props.onFocusedOptionChanged(focusedOption);
      }
    });
  };

  // const toggleClearable = () => setIsClearable(!isClearable);

  const onInputChange = (_, { action }) => {
    if (action === 'set-value') {
      props.onOptionSelected(getFocusedOption());
    }
  };

  const loadOptions = async (inputText) => {
    const symbolData = await getSymbolData(inputText);
    console.log('symbolDATA: ', symbolData);
    return symbolData;
  };

  useEffect(() => {
    myRefLastFocusedOption.current = getFocusedOption();
    // console.log('props', props);

    // return () => {
    //   cleanup;
    // };
  }, [getFocusedOption, myRefLastFocusedOption]);

  return (
    <div onMouseMove={onUserInteracted} onClick={onUserInteracted}>
      <AsyncSelect
        {...props}
        ref={(ref) => (myRef = ref)}
        // ref={myRef}
        onKeyDown={onUserInteracted}
        onInputChange={onInputChange}
        loadOptions={loadOptions}
        autoFocus
        noOptionsMessage={() => 'Search symbol'}
        placeholder="Search Symbol"
        isClearable={props.isClearable} // allows us to clear the selected value either using the backspace button or the “x” button on the right side of the field
        clear // Removing all selected options using the clear button
        pop-value // Removing options using backspace
        loadingIndicator
      />
    </div>
  );
};

export default MyAsyncSelect;
4
  • 2
    Just use useRef in the functional component. Access it as myRef.current, etc. Where in your sandbox is the functional component version? Commented Oct 29, 2020 at 22:50
  • @DrewReese it works! Thank you! Commented Oct 29, 2020 at 22:52
  • 2
    @JohnJohn The problem with createRef() in a function component is that it will create a new object each time the function component is called. Whereas useRef() will only create a new object the first time it is called. The second, third, etc. call it will return the same object it has initially created (assuming you follow the Rules of Hooks). Commented Oct 29, 2020 at 23:01
  • @DrewReese here is functional component codesandbox.io/s/twilight-smoke-mmwsm?file=/src/components/… Commented Oct 29, 2020 at 23:01

1 Answer 1

3

Here is the working solution based on the sugestions from @DrewReese and @3limin4t0r

import React, { useCallback, useEffect, useRef } from 'react';

import { getSymbolData } from './utils.js';

import AsyncSelect from 'react-select/async';

const MyAsyncSelect = (props) => {
  const myRef = useRef();
  const myRefLastFocusedOption = useRef();

  const getFocusedOption = useCallback(() => {
    // @ts-ignore
    return myRef.current.select.select.state.focusedOption;
  }, [myRef]);

  const isMenuOpen = () => {
    // @ts-ignore
    return myRef.current.select.state.menuIsOpen;
  };

  const onUserInteracted = () => {
    Promise.resolve().then(() => {
      const focusedOption = getFocusedOption();
      if (isMenuOpen() && myRefLastFocusedOption.current !== focusedOption) {
        myRefLastFocusedOption.current = focusedOption;
        props.onFocusedOptionChanged(focusedOption);
      }
    });
  };

  const onInputChange = (_, { action }) => {
    if (action === 'set-value') {
      props.onOptionSelected(getFocusedOption());
    }
  };

  const loadOptions = async (inputText) => {
    const symbolData = await getSymbolData(inputText);
    console.log('symbolDATA: ', symbolData);
    return symbolData;
  };

  useEffect(() => {
    myRefLastFocusedOption.current = getFocusedOption();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <div onMouseMove={onUserInteracted} onClick={onUserInteracted}>
      <AsyncSelect
        {...props}
        ref={myRef}
        onKeyDown={onUserInteracted}
        onInputChange={onInputChange}
        loadOptions={loadOptions}
        autoFocus
        noOptionsMessage={() => 'Search symbol'}
        placeholder="Search Symbol"
        isClearable={props.isClearable} // allows us to clear the selected value either using the backspace button or the “x” button on the right side of the field
        clear // Removing all selected options using the clear button
        pop-value // Removing options using backspace
        loadingIndicator
      />
    </div>
  );
};

export default MyAsyncSelect;
Sign up to request clarification or add additional context in comments.

1 Comment

As a slight optimisation you can change ref={ref => myRef.current = ref} into just ref={myRef}. By providing the ref object it will update the current property by default. If you want to update any other property you need to use the callback variant. See: reactjs.org/docs/hooks-reference.html#useref

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.