I created an API
endpoint that gives product title back when the user tries to search. Now on the frontend side, I will make an API call to that endpoint when enters some keystrokes on the input field. So I have written that component in React
as a class-based component. It works fine. But now I wanted to convert that component in the newer version of React
by using React
hooks.
My class-based implementation works fine. What I did is when the user enters some keystrokes. I debounce
i.e. delays the execution of the passed function as an argument. The function is handleSearchChange()
which takes value from the field and checks if value
string is greater than 1 character then after the specified delay makes an API call which in response gives back some results.
The server filter the results from the following data:
[
{
"title": "Cummings - Nikolaus",
"description": "Assimilated encompassing hierarchy",
"image": "https://s3.amazonaws.com/uifaces/faces/twitter/michalhron/128.jpg",
"price": "$20.43"
},
{
"title": "Batz, Kiehn and Schneider",
"description": "Public-key zero tolerance portal",
"image": "https://s3.amazonaws.com/uifaces/faces/twitter/attacks/128.jpg",
"price": "$58.97"
},
{
"title": "Borer, Bartell and Weber",
"description": "Programmable motivating system engine",
"image": "https://s3.amazonaws.com/uifaces/faces/twitter/craighenneberry/128.jpg",
"price": "$54.51"
},
{
"title": "Brekke, Mraz and Wyman",
"description": "Enhanced interactive website",
"image": "https://s3.amazonaws.com/uifaces/faces/twitter/vaughanmoffitt/128.jpg",
"price": "$53.28"
},
{
"title": "Willms and Sons",
"description": "Compatible next generation superstructure",
"image": "https://s3.amazonaws.com/uifaces/faces/twitter/madcampos/128.jpg",
"price": "$49.82"
}
]
Class-based implementation:
//#region Global imports
import React, { Component, ReactElement } from 'react';
import _ from 'lodash';
import axios from 'axios';
//#endregion Global imports
//#region Types
type Data = object;
type StateType = {
isLoading: boolean;
results: Data[];
value: string | undefined;
}
//#endregion Types
//#region Component
const initialState = {
isLoading: false,
results: [],
value: '',
};
export class SearchInputV1 extends Component<{}, StateType> {
// React component using ES6 classes no longer autobind `this` to non React methods.
constructor(props: Readonly<{}>) {
super(props);
this.state = initialState;
this.getSearchResults = this.getSearchResults.bind(this);
this.handleSearchChange = this.handleSearchChange.bind(this);
}
// Function to make an API call
async getSearchResults() {
try {
const { value } = this.state;
const { data } = await axios.get(`http://localhost:3000/api/products?q=${value}`);
this.setState(prevState => ({ ...prevState, isLoading: false, results: data }));
} catch (e) {
console.error(e);
}
}
handleSearchChange(event: React.ChangeEvent<HTMLInputElement>) {
const { target } = event;
const val = target.value;
this.setState(prevState => ({ ...prevState, isLoading: true, value: val }));
console.log('Method debounce : Type value is : ', val);
setTimeout(() => {
const { value } = this.state;
if (typeof value === 'string' && value.length < 1) {
return this.setState(prevState => ({ ...prevState, ...initialState }));
}
// Makes an API call
this.getSearchResults();
}, 300);
};
render(): ReactElement<any> {
const { value, results } = this.state;
return (
<div>
<label htmlFor="search"/>
<input type="text" value={value} id="search" name="query"
onChange={_.debounce(this.handleSearchChange, 500, { leading: true })}/>
<div>
{results.map((element, index) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
return <p key={index}>{element.title}</p>;
})}
</div>
</div>
);
}
}
//#endregion Component
Now here is the problem, In my React
hook implementation when I make an API
call then it would never stop it is making an infinite API calls to the server.
What I’m doing wrong and How to fix it?
Hooks implementaion:
//#region Global imports
import React, { useState, useEffect } from 'react';
import _ from 'lodash';
import axios from 'axios';
//#endregion Global imports
//#region Types
type Data = object;
type StateType = {
isLoading: boolean;
results: Data[];
value: string | undefined;
}
//#enregion Types
//#region Component
const initialState = {
isLoading: false,
results: [],
value: '',
};
export const SearchInputV2 = () => {
const [state, setState] = useState<StateType>(initialState);
// Whenever state will be change useEffect will trigger.
useEffect(() => {
const getSearchResults = async () => {
try {
const { value } = state;
const { data } = await axios.get(`http://localhost:3000/api/products?q=${value}`);
setState(prevState => ({ ...prevState, isLoading: false, results: data }));
} catch (e) {
console.error(e);
}
};
// After the specified delay makes an API call
const timer = setTimeout(() => {
const { value } = state;
if (typeof value === 'string' && value.length < 1) {
return setState(prevState => ({ ...prevState, ...initialState }));
}
// Makes an API call
getSearchResults();
}, 300);
// This will clear Timeout when component unmont like in willComponentUnmount
return () => {
clearTimeout(timer);
};
}, [state]);
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const {target} = event;
const val = target.value;
setState(prevState => ({ ...prevState, isLoading: true, value: val }));
console.log('Method debounce : Type value is : ', val);
};
const { value, results } = state;
return (
<div>
<label htmlFor="search-v"/>
<input type="text" value={value} id="search-v" name="query"
onChange={_.debounce(handleSearchChange, 500, { leading: true })}/>
<div>
{results.map((element, index) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
return <p key={index}>{element.title}</p>;
})}
</div>
</div>
);
};
//#endregion Component
3
Answers
Your
timer
is changing thestate
and so does thegetSearchResults
.useEffect
will call anytime when thestate
changes. That is why, the API is being called in infinite loop. Try something like below:you cant place state as dependency, remove state and try setState instead
In your
useEffect(()=>{}, [])
. The[]
means everytime what’s inside those brackets change it will run the function inside the useEffect. In your state so eveytime a new result comes in it will run the effect, the effect get a new result everytime thus causing that infinite call. Use instead[state.value]
. But IMO it’s better to have those as separate[value, setValue] = useState('')
,[isLoading, setIsLoading] = useState(false)
,[result, setResult] = useState([])
. So you could’ve haveuseEffect(()=>{}, [value])