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:
- It fetches the data on the first render of the component (This is how
useEffect
works). - 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.
Latest comments (0)