Reputation: 381
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
Upvotes: 5
Views: 2110
Reputation: 190
Your timer
is changing the state
and so does the getSearchResults
. useEffect
will call anytime when the state
changes. That is why, the API is being called in infinite loop. Try something like below:
//#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);
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);
}
}
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const {target} = event;
const val = target.value;
setState(prevState => ({ ...prevState, isLoading: true, value: val }));
setTimeout(() => {
getSearchResults();
}, 300);
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
Upvotes: 1
Reputation: 1204
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 have useEffect(()=>{}, [value])
Upvotes: 2
Reputation: 577
you cant place state as dependency, remove state and try setState instead
// This will clear Timeout when component unmont like in willComponentUnmount
return () => {
clearTimeout(timer);
};
}, [state]); // here remove state and put setState or [] empty array
Upvotes: -1