Watchers in ReactWatchers in React
Published on

Watchers in React


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:

// 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:

// 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.

// 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:

// 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.

// 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:

// 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.

Do you want to receive occasional mails about what‘s going on on this blog? Feel free to leave behind your email address.

Comments

The comment system is based on github issues.