Search code examples
reactjsreact-hooksuse-effectuse-state

Why does my component rerender at onChange and not onClick?


Im trying to figure out why does my component re-render only when I am typing on my textfield (Im guessing it changes because of the onChange part? ) where it does not re-render after I made the API call on my onClick button

The below is my code:

export default function HomePage() {
    const [searchBook, setSearchBook] = useState('');
    const [bookList, setBookList] = useState([]);
    
    return(

       <Box>

         <Box className='home-page-right-search-box' display='flex' flexDirection='row' alignItems='center' sx={{}}>
                <Box flex={3} sx={{ fontSize: '75px' }}>
                    Explore
                </Box>
                <Box className='home-page-right-search-textfield' flex={1} display='flex' flexDirection='row'>
                    <TextField id="outlined-basic" label="🔍   Search" variant="outlined" sx={{ boxShadow: '0px 0px 10px 2px #C0C0C0', width: '350px', mr: 2 }} onChange={(value) => {
                        setSearchBook(value.target.value)
                    }} />
                    <Button sx={{ color: '#364d8a' }} onClick={
                        async () => {
                            const response = await GoogleBookSearch(searchBook);
                            setBookList(response);
                        }}>
                        Search
                    </Button>
                </Box>
            </Box>
            <Box className='home-page-right-trending-container' sx={{ mt: 10, px: 2 }}>
                <Box sx={{ mb: 5, fontSize: '35px' }}>
                    Trending
                </Box>
                <Grid container spacing={5}>




              ////*****This is the place where it is not re-rendering after onClick******///


                    {bookList.map((book) => {
                        console.log('book: ', book)
                        return (
                            <Grid item >
                                <Box className='home-page-right-trending-list-item' textAlign='center' sx={{ mt: 3, mr: 2 }} >
                                    <Box className='trending-list-item-img-box'>
                                        <img className='trending-list-item-img' src={book.imageLink} alt='book image' />
                                    </Box>
                                    <Box className='trending-list-item-title' sx={{ mt: 1, fontSize: '20px', color: 'grey', width:'200px' }}>
                                        {book.title}
                                    </Box>
                                    <Box sx={{ mt: 1, fontSize: '15px', color: 'grey' }}>
                                        {book.author}
                                    </Box>
                                    <Box sx={{ mt: 1, fontSize: '20px' }}>
                                        ⭐️ {book.rating}
                                    </Box>
                                </Box>
                            </Grid>
                        );
                    })}

                </Grid>
            </Box>
       </Box>
    );
}

This is my API call:

export async function GoogleBookSearch(name) {
const bookDetails = [];
try {
    await fetch(`https://www.googleapis.com/books/v1/volumes?q=${name}`).then((result) => {
        result.json().then((item) => {
            const itemsFirstTen = item['items'].slice(0, 10);
            
            for (var i in itemsFirstTen) {
                // console.log( 'sales info: ', itemsFirstTen[i]['saleInfo']);
                bookDetails.push({
                    title: itemsFirstTen[i]['volumeInfo']['title'],
                    subtitle: itemsFirstTen[i]['volumeInfo']['subtitle'],
                    authors: itemsFirstTen[i]['volumeInfo']['authors'],
                    description: itemsFirstTen[i]['volumeInfo']['description'],
                    price: itemsFirstTen[i]['saleInfo']['saleability'] === 'FREE' || 'NOT_FOR_SALE' ? 0 : itemsFirstTen[i]['saleInfo']['listPrice']['amount'],
                    imageLink: itemsFirstTen[i]['volumeInfo']['imageLinks']['thumbnail'],
                })
            }
        })

    });


    // return response;

} catch (error) {
    console.log('error searching book: ', error);
}
    console.log('bookdetails: ', bookDetails)
    return bookDetails;
}

The API calls return exactly what I want and I've reformated it to the way I want it as well.

I just want my component to re-render to the latest search result and re-renders after onClick; not when I start typing again in the textfield then it renders out what my last search result was.

THanks


Solution

  • Issue

    The code wasn't rerendering correctly because the GoogleBookSearch code wasn't correctly waiting to populate the bookDetails array. I suspect it's due to nesting the result.json() promise chain and not returning it. In other words, the await fetch(....).then((result) => { ... }) Promise resolved and the rest of the GoogleBookSearch function ran and returned an empty bookDetails array before the nested result.json() Promise chain resolved and later mutated the bookDetails array.

    This is why clicking the search button wasn't triggering a rerender, but then later when you again typed in the search input, it would trigger a rerender and you'd see the mutated bookList state.

    export async function GoogleBookSearch(name) {
      const bookDetails = [];
      try {
        await fetch(`https://www.googleapis.com/books/v1/volumes?q=${name}`)
          .then((result) => {
            // (1) Nothing returned from here on, so the outer Promise resolved
            // (2) This start a new, nested Promise chain
            result.json()
              .then((item) => {
                const itemsFirstTen = item['items'].slice(0, 10);
                
                for (var i in itemsFirstTen) {
                  // (4) bookDetails array mutated!!
                  bookDetails.push({ ...book data... });
                }
            })
    
        });
      } catch (error) {
        console.log('error searching book: ', error);
      }
      console.log('bookdetails: ', bookDetails);
    
      // (3) bookDetails array returned
      return bookDetails;
    }
    

    Solution

    Don't nest Promise chains, Promises were created to eliminate "nesting hell". You could return the nested Promise and keep the Promise chain flattened.

    Don't mix async/await with Promise chains either though. Use a try/catch with the async/await code and keep the code looking "synchronous".

    Example:

    async function GoogleBookSearch(name) {
      try {
        const response = await fetch(
          `https://www.googleapis.com/books/v1/volumes?q=${name}`
        );
        const result = await response.json();
    
        return (result.items || []).slice(0, 10).map((book) => ({
          title: book.volumeInfo.title,
          subtitle: book.volumeInfo.subtitle,
          authors: book.volumeInfo.authors,
          description: book.volumeInfo.description,
          price:
            book.saleInfo.saleability === "FREE" || "NOT_FOR_SALE"
              ? 0
              : book.saleInfo.listPrice.amount,
          imageLink: book.volumeInfo.imageLinks.thumbnail
        }));
      } catch (error) {
        console.log("error searching book: ", error);
      }
    }
    

    Edit why-does-my-component-rerender-at-onchange-and-not-onclick