Watchers in React
Tags: react, webdev, javascript
Published: 2021-01-25 19:21:55

Developing a React app can be very productive and fun, but if you are used to working with Vue as I do, I bet you miss some of those great and useful features. One of those features is `Watchers`. In this blog post, we will try to find out how to improve the current React API and create a custom hook to help us "watch" for state changes.

## What are `Watchers` in React?

Before we try to understand what `Watchers` are specific in a React app, we should try to understand what `Watchers` are in general. `Watchers` are simple functions that listen for changes on one or more pieces of state. `Watchers` are very helpful and usually used when there is a requirement to interact with a component that lives outside the React world (side effects).

In the next example, we will build a search component. We will listen for changes in the input's value, and based on that, we will send a request to the server.

## Using `useEffect` to watch for value changes

The best start is to use the `useEffect` hook. It should help us listen for changes in the input value and trigger a request to the server based on these changes. Let's see how it goes:

```jsx
// SearchBox.jsx
import React, { useState, useEffect } from 'react'

export default function SearchBox() {
  const [value, setValue] = useState('')

  useEffect(() => {
    // Fetching logic...
  }, [ value ])

  return <input
    value={value}
    onChange={(e) => setValue(e.target.value)}
  />
}
```

This code should do the work, but two issues occur in the current implementation:
1. It fetches the data on the first render of the component (This is how `useEffect` works).
2. It fetches the data on each keystroke of the user (multiple times for no reason).

Let's try to resolve those issues.

## Using `useRef` to avoid first render fetching

To avoid triggering fetch on the first render of the component, we can use a flag variable to determine if the current function call is the first. To do so, we will use the `useRef` hook. Let's see an example:

```jsx
// SearchBox.jsx
import React, { useState, useEffect, useRef } from 'react'

export default function SearchBox() {
  const [value, setValue] = useState('')
  const isFirstRender = useRef(true)

  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false

      return
    }

    // Fetching logic...
  }, [ value ])

  return <input
    value={value}
    onChange={(e) => setValue(e.target.value)}
  />
}
```

This current code can do the job, but we can take it to the next step by wrapping the implementation into a custom hook.

## Create a custom hook: `useWatch`

By wrapping the `watcher` implementation into a custom hook, we will make the logic reusable and clean our component code.

```js
// useWatch.js
import { useEffect, useRef } from 'react'

export default function useWatch( callback, deps ) {
  const isFirstRender = useRef(true)

  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false

      return
    }

    callback();
  }, deps)
}
```

Here is the `SearchBox` component using the new hook we just created:

```jsx
// SearchBox.jsx
import React, { useState } from 'react'
import useWatch from '../hooks/useWatch'

export default function SearchBox() {
  const [value, setValue] = useState('')

  useWatch(() => {
    // Fetching logic...
  }, [ value ])

  return <input
    value={value}
    onChange={(e) => setValue(e.target.value)}
  />
}
```

In the next step, we will try to solve the second issue, when the fetching mechanism triggers every keystroke.

## Create a custom hook: `useDebouncedWatch`

To avoid multiple requests to the server, we could delay the fetching mechanism until the user stops typing. Then, and only then, we should trigger the function that fetches the data from the server.

```js
// useDebouncedWatch.js
import { useRef } from 'react'
import useWatch from './useWatch'

export default function useWatch( callback, deps, delay = 1000 ) {
  const timeoutHandler = useRef(null)

  useWatch(() => {
    if (timeoutHandler.current) {
      clearTimeout(timeoutHandler.current)
    }

    timeoutHandler.current = setTimeout(() => {
      callback();
    }, delay)
  }, deps)
}
```

This implementation waits 1000 milliseconds and only then calls the callback function (which will, in our case, fetch data from the server). If the value changes again before those 1000 milliseconds, the previous timer stops, and a new timer starts to count. This loop will go on and on until the user stops typing.

Here is the final code from our `SearchBox` component uses the new custom hook:

```jsx
// SearchBox.jsx
import React, { useState } from 'react'
import useDebouncedWatch from '../hooks/useDebouncedWatch'

export default function SearchBox() {
  const [value, setValue] = useState('')

  useDebouncedWatch(() => {
    // Fetching logic...
  }, [ value ])

  return <input
    value={value}
    onChange={(e) => setValue(e.target.value)}
  />
}
```

## Conclusion

As we can see, `Watchers` exists in the React world. We just had to peel the shell and expose it out. With only a few steps, we included this elegant API from Vue into React world.