skip to Main Content

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

Demo of the API call

3

Answers


  1. 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
    
    Login or Signup to reply.
  2. 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
    
    Login or Signup to reply.
  3. 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])

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search