---
# System prepended metadata

title: React Hooks Example

---

# React Hooks Example

Real world component — `CategoryTypeahead`

## Original (class component)

<details>
  <summary>Code</summary>

```jsx
import Immutable from 'immutable';
import debounce from 'lodash/debounce';
import PropTypes from 'prop-types';
import { identity } from 'ramda';
import React from 'react';
import IPropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';

import * as EventSelectors from 'state/events/selectors';
import * as UttTaxonomyActions from 'state/utt/actions';
import * as UttTaxonomySelectors from 'state/utt/selectors';
import InternalProps from 'modules/internal-props';
import { SimpleSelectInput, SimpleTypeaheadTransport } from 'components/Inputs/Select';
import { UttMetadata } from 'models/events';

class CategoryTypeahead extends React.Component {
  static propTypes = {
    eventAnnotations: IPropTypes.listOf(PropTypes.string),
    getUttName: PropTypes.func,
    onAddCategory: PropTypes.func,
    renderOption: PropTypes.func,
    requestUttTaxonomy: PropTypes.func,
    uttMetadata: InternalProps.UttMetadata,
    uttTaxonomy: InternalProps.UttTaxonomy,
    uttTaxonomyLoading: PropTypes.bool,
  };

  static defaultProps = {
    uttMetadata: new UttMetadata(),
  };

  state = {
    transport: new SimpleTypeaheadTransport(),
    searchTerm: '',
  };

  componentDidMount() {
    this.debouncedLoadData();
  }

  componentDidUpdate(prevProps, prevState) {
    const { eventAnnotations, uttMetadata } = this.props;
    const { searchTerm } = this.state;
    const eventAnnotationsChanged = prevProps.eventAnnotations !== eventAnnotations;
    const uttMetadataChanged = prevProps.uttMetadata !== uttMetadata;
    const searchTermChanged = prevState.searchTerm !== searchTerm;
    if (eventAnnotationsChanged || uttMetadataChanged || searchTermChanged) {
      this.debouncedLoadData();
    }
  }

  debouncedLoadData = debounce(() => this.loadData(), 700);

  loadData = () => {
    const { eventAnnotations, requestUttTaxonomy, uttMetadata } = this.props;
    const { searchTerm } = this.state;
    const uttEntityIds = uttMetadata.uttEntityIds || Immutable.List();
    const withEntities = uttEntityIds.concat(eventAnnotations).join(',');
    requestUttTaxonomy({ searchTerm, withEntities });
  };

  onKeyUp = event => this.setState({ searchTerm: event.target.value });

  isInTaxonomy = id => this.getUttName(id) !== id;

  getUttName = id => {
    const { getUttName, uttTaxonomy } = this.props;
    return getUttName(uttTaxonomy, id);
  };

  renderOption = id => {
    const { renderOption, uttTaxonomy } = this.props;
    return this.isInTaxonomy(id) ? renderOption(uttTaxonomy, id) : <React.Fragment />;
  };

  getOptions() {
    const { uttMetadata, uttTaxonomy } = this.props;
    const uttEntityIds = uttMetadata.uttEntityIds || Immutable.List();
    return Immutable.List(uttTaxonomy?.uttNameById?.keys()).filterNot(uttEntityIds.contains);
  }

  render() {
    const { onAddCategory, uttTaxonomyLoading } = this.props;
    const { transport } = this.state;

    return (
      <SimpleSelectInput
        getOptionId={identity}
        getOptionName={id => this.getUttName(id)}
        isLoading={uttTaxonomyLoading}
        onChange={onAddCategory}
        onKeyUp={this.onKeyUp}
        options={this.getOptions()}
        optionsSort={options =>
          options.sort((a, b) => this.getUttName(a).localeCompare(this.getUttName(b)))
        }
        placeholder="Start typing to search for your Topic..."
        renderOption={this.renderOption}
        transport={transport}
      />
    );
  }
}

const mapStateToProps = createStructuredSelector({
  eventAnnotations: EventSelectors.selectEventAnnotations,
  uttTaxonomy: UttTaxonomySelectors.selectUttTaxonomy,
  uttTaxonomyLoading: UttTaxonomySelectors.uttTaxonomyLoading,
});

const mapDispatchToProps = {
  requestUttTaxonomy: UttTaxonomyActions.requestUttTaxonomy,
};

export default connect(mapStateToProps, mapDispatchToProps)(CategoryTypeahead);
```

</details>

## Refactor to use Hooks

### Part 1

Pretty much just convert the class to a functional component and use hooks

The benefits don't appear obvious — or at least seem a little arbitrary

- Readability is a _little_ better, at least in terms of number of lines...
- Testing [a single aspect] is still convoluted

*️⃣ The HoC `connect` is still being used (more on that later)

<details>
  <summary>Code</summary>

```jsx
import Immutable from 'immutable';
import PropTypes from 'prop-types';
import { identity } from 'ramda';
import React from 'react';
import IPropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { useDebounce } from 'react-use';
import { createStructuredSelector } from 'reselect';

import * as EventSelectors from 'state/events/selectors';
import * as UttTaxonomyActions from 'state/utt/actions';
import * as UttTaxonomySelectors from 'state/utt/selectors';
import InternalProps from 'modules/internal-props';
import { SimpleSelectInput, SimpleTypeaheadTransport } from 'components/Inputs/Select';
import { UttMetadata } from 'models/events';

const CategoryTypeahead = ({
  eventAnnotations,
  getUttName,
  onAddCategory,
  renderOption,
  requestUttTaxonomy,
  uttMetadata,
  uttTaxonomy,
  uttTaxonomyLoading,
}) => {
  const [searchTerm, setSearchTerm] = useState('');
  const [transport] = useState(() => new SimpleTypeaheadTransport());

  const uttEntityIds = uttMetadata.uttEntityIds || Immutable.List();
  const withEntities = uttEntityIds.concat(eventAnnotations).join(',');

  const [, cancel] = useDebounce(
    () =>
      requestUttTaxonomy({
        searchTerm,
        withEntities,
      }),
    700,
    [requestUttTaxonomy, searchTerm, withEntities],
  );

  // cancel the debounce if component unmounts
  useEffect(() => () => cancel, [cancel]);

  const handleSearch = useCallback(evt => setSearchTerm(evt.target.value), []);

  const getUttNameById = useCallback(id => getUttName(uttTaxonomy, id), [getUttName, uttTaxonomy]);

  const sortOptions = useCallback(
    options => options.sort((a, b) => getUttNameById(a).localeCompare(getUttNameById(b))),
    [getUttNameById],
  );

  const options = useMemo(
    () => Immutable.List(uttTaxonomy?.uttNameById?.keys()).filterNot(uttEntityIds.contains),
    [uttEntityIds, uttTaxonomy],
  );

  return (
    <SimpleSelectInput
      getOptionId={identity}
      getOptionName={getUttNameById}
      isLoading={uttTaxonomyLoading}
      onChange={onAddCategory}
      onKeyUp={handleSearch}
      options={options}
      optionsSort={sortOptions}
      placeholder="Start typing to search for your Topic..."
      renderOption={id =>
        getUttNameById(id) !== id ? renderOption(uttTaxonomy, id) : <React.Fragment />
      }
      transport={transport}
    />
  );
};

CategoryTypeahead.propTypes = {
  eventAnnotations: IPropTypes.listOf(PropTypes.string),
  getUttName: PropTypes.func,
  onAddCategory: PropTypes.func,
  renderOption: PropTypes.func,
  requestUttTaxonomy: PropTypes.func,
  uttMetadata: InternalProps.UttMetadata,
  uttTaxonomy: InternalProps.UttTaxonomy,
  uttTaxonomyLoading: PropTypes.bool,
};

CategoryTypeahead.defaultProps = {
  uttMetadata: new UttMetadata(),
};

const mapStateToProps = createStructuredSelector({
  eventAnnotations: EventSelectors.selectEventAnnotations,
  uttTaxonomy: UttTaxonomySelectors.selectUttTaxonomy,
  uttTaxonomyLoading: UttTaxonomySelectors.uttTaxonomyLoading,
});

const mapDispatchToProps = {
  requestUttTaxonomy: UttTaxonomyActions.requestUttTaxonomy,
};

export default connect(mapStateToProps, mapDispatchToProps)(CategoryTypeahead);
```

</details>

### Part 2

Let's **group** functionality and create a _new_ hook

- `searchTerm`, the keyUp handler (i.e. "onSearch"), and the debounce are functionaly interconnected
- Abstract this likeness into a custom hook called `useSearch`

<details>
  <summary>Code</summary>

```jsx
const useSearch = ({ requestUttTaxonomy, eventAnnotations, uttEntityIds }) => {
  const [searchTerm, setSearchTerm] = useState('');

  const withEntities = uttEntityIds.concat(eventAnnotations).join(',');

  const [, cancel] = useDebounce(
    () =>
      requestUttTaxonomy({
        searchTerm,
        withEntities,
      }),
    700,
    [requestUttTaxonomy, searchTerm, withEntities],
  );

  // cancel the debounce if component unmounts
  useEffect(() => () => cancel, [cancel]);

  return useCallback(evt => setSearchTerm(evt.target.value), []);
};
```

```diff
-  const [searchTerm, setSearchTerm] = useState('');
   const [transport] = useState(() => new SimpleTypeaheadTransport());

   const uttEntityIds = uttMetadata.uttEntityIds || Immutable.List();
-  const withEntities = uttEntityIds.concat(eventAnnotations).join(',');
-
-  const [, cancel] = useDebounce(
-    () =>
-      requestUttTaxonomy({
-        searchTerm,
-        withEntities,
-      }),
-    700,
-    [requestUttTaxonomy, searchTerm, withEntities],
-  );
-
-  // cancel the debounce if component unmounts
-  useEffect(() => () => cancel, [cancel]);
-
-  const handleSearch = useCallback(evt => setSearchTerm(evt.target.value), []);
+  const handleSearch = useSearch({ requestUttTaxonomy, eventAnnotations, uttEntityIds });
```

</details>

### Part 3

The same can be done with _options_

<details>
  <summary>Code</summary>

```jsx
const useOptions = ({ getUttNameById, uttEntityIds, uttTaxonomy }) => {
  const sortOptions = useCallback(
    options => options.sort((a, b) => getUttNameById(a).localeCompare(getUttNameById(b))),
    [getUttNameById],
  );

  const options = useMemo(
    () => Immutable.List(uttTaxonomy?.uttNameById?.keys()).filterNot(uttEntityIds.contains),
    [uttEntityIds, uttTaxonomy],
  );

  return { options, sortOptions };
};
```

```diff
  const [transport] = useState(() => new SimpleTypeaheadTransport());
  const uttEntityIds = uttMetadata.uttEntityIds || Immutable.List();

  const handleSearch = useSearch({ requestUttTaxonomy, eventAnnotations, uttEntityIds });
  const getUttNameById = useCallback(id => getUttName(uttTaxonomy, id), [getUttName, uttTaxonomy]);
-
-  const sortOptions = useCallback(
-    options => options.sort((a, b) => getUttNameById(a).localeCompare(getUttNameById(b))),
-    [getUttNameById],
-  );
-
-  const options = useMemo(
-    () => Immutable.List(uttTaxonomy?.uttNameById?.keys()).filterNot(uttEntityIds.contains),
-    [uttEntityIds, uttTaxonomy],
-  );
+  const { options, sortOptions } = useOptions({ getUttNameById, uttEntityIds, uttTaxonomy });
```

</details>
<br />

- Can `useOptions` be split further?
- What are the pros and cons of doing that?

### Part 4

The HoC from `react-redux` can be replaced with hooks too

- `useDispatch` and `useSelector`
- This simplifies the arguments for `useSearch` because it can use `useDispatch` internally

<details>
  <summary>Code</summary>

```diff
-const useSearch = ({ requestUttTaxonomy, eventAnnotations, uttEntityIds }) => {
+const useSearch = uttEntityIds => {
   const [searchTerm, setSearchTerm] = useState('');

+  const eventAnnotations = useSelector(EventSelectors.selectEventAnnotations);
   const withEntities = uttEntityIds.concat(eventAnnotations).join(',');

+  const dispatch = useDispatch();
   const [, cancel] = useDebounce(
     () =>
-      requestUttTaxonomy({
-        searchTerm,
-        withEntities,
-      }),
+      dispatch(
+        UttTaxonomyActions.requestUttTaxonomy({
+          searchTerm,
+          withEntities,
+        }),
+      ),
     700,
-    [requestUttTaxonomy, searchTerm, withEntities],
+    [dispatch, searchTerm, withEntities],
   );
```

```diff
-const CategoryTypeahead = ({
-  eventAnnotations,
-  getUttName,
-  onAddCategory,
-  renderOption,
-  requestUttTaxonomy,
-  uttMetadata,
-  uttTaxonomy,
-  uttTaxonomyLoading,
-}) => {
+const CategoryTypeahead = ({ getUttName, onAddCategory, renderOption, uttMetadata }) => {
+  const uttTaxonomy = useSelector(UttTaxonomySelectors.selectUttTaxonomy);
+  const uttTaxonomyLoading = useSelector(UttTaxonomySelectors.uttTaxonomyLoading);
+
   const [transport] = useState(() => new SimpleTypeaheadTransport());
   const uttEntityIds = uttMetadata.uttEntityIds || Immutable.List();

-  const handleSearch = useSearch({ requestUttTaxonomy, eventAnnotations, uttEntityIds });
+  const handleSearch = useSearch(uttEntityIds);
```

```diff
-const mapStateToProps = createStructuredSelector({
-  eventAnnotations: EventSelectors.selectEventAnnotations,
-  uttTaxonomy: UttTaxonomySelectors.selectUttTaxonomy,
-  uttTaxonomyLoading: UttTaxonomySelectors.uttTaxonomyLoading,
-});
-
-const mapDispatchToProps = {
-  requestUttTaxonomy: UttTaxonomyActions.requestUttTaxonomy,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(CategoryTypeahead);
+export default CategoryTypeahead;
```

</details>

## End Result

```jsx
import Immutable from 'immutable';
import PropTypes from 'prop-types';
import { identity } from 'ramda';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useDebounce } from 'react-use';

import * as EventSelectors from 'state/events/selectors';
import * as UttTaxonomyActions from 'state/utt/actions';
import * as UttTaxonomySelectors from 'state/utt/selectors';
import InternalProps from 'modules/internal-props';
import { SimpleSelectInput, SimpleTypeaheadTransport } from 'components/Inputs/Select';
import { UttMetadata } from 'models/events';

const useOptions = ({ getUttNameById, uttEntityIds, uttTaxonomy }) => {
  const sortOptions = useCallback(
    options => options.sort((a, b) => getUttNameById(a).localeCompare(getUttNameById(b))),
    [getUttNameById],
  );

  const options = useMemo(
    () => Immutable.List(uttTaxonomy?.uttNameById?.keys()).filterNot(uttEntityIds.contains),
    [uttEntityIds, uttTaxonomy],
  );

  return { options, sortOptions };
};

const useSearch = uttEntityIds => {
  const [searchTerm, setSearchTerm] = useState('');
  const eventAnnotations = useSelector(EventSelectors.selectEventAnnotations);
  const withEntities = uttEntityIds.concat(eventAnnotations).join(',');

  const dispatch = useDispatch();
  const [, cancel] = useDebounce(
    () =>
      dispatch(
        UttTaxonomyActions.requestUttTaxonomy({
          searchTerm,
          withEntities,
        }),
      ),
    700,
    [dispatch, searchTerm, withEntities],
  );

  // cancel the debounce if component unmounts
  useEffect(() => () => cancel, [cancel]);

  return useCallback(evt => setSearchTerm(evt.target.value), []);
};

const CategoryTypeahead = ({ getUttName, onAddCategory, renderOption, uttMetadata }) => {
  const uttTaxonomy = useSelector(UttTaxonomySelectors.selectUttTaxonomy);
  const uttTaxonomyLoading = useSelector(UttTaxonomySelectors.uttTaxonomyLoading);

  const [transport] = useState(() => new SimpleTypeaheadTransport());
  const uttEntityIds = uttMetadata.uttEntityIds || Immutable.List();

  const handleSearch = useSearch(uttEntityIds);
  const getUttNameById = useCallback(id => getUttName(uttTaxonomy, id), [getUttName, uttTaxonomy]);
  const { options, sortOptions } = useOptions({ getUttNameById, uttEntityIds, uttTaxonomy });

  return (
    <SimpleSelectInput
      getOptionId={identity}
      getOptionName={getUttNameById}
      isLoading={uttTaxonomyLoading}
      onChange={onAddCategory}
      onKeyUp={handleSearch}
      options={options}
      optionsSort={sortOptions}
      placeholder="Start typing to search for your Topic..."
      renderOption={id =>
        getUttNameById(id) !== id ? renderOption(uttTaxonomy, id) : <React.Fragment />
      }
      transport={transport}
    />
  );
};

CategoryTypeahead.propTypes = {
  getUttName: PropTypes.func,
  onAddCategory: PropTypes.func,
  renderOption: PropTypes.func,
  uttMetadata: InternalProps.UttMetadata,
};

CategoryTypeahead.defaultProps = {
  uttMetadata: new UttMetadata(),
};

export default CategoryTypeahead;
```
