React useState hook is appending values instead of overwriting

Viewed 639

So i want to get live price update every 5 seconds therefore i put recursive setTimeout into the function. Unfortunately right now the price feed is working this way:

  1. At the start console outputs are: undefined and current price
  2. After price change outputs are: undefined, old price, new current price
  3. After next price change: undefined, oldest price, older price, new current price

why does it appending the values instead of overwriting them as usual?

import React, { useState, useEffect } from 'react';
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import Livebsv from './livebsv.js';

const CoinGecko = require('coingecko-api');
const CoinGeckoClient = new CoinGecko();


export default function Calculatorbuy() {
    const [value, setValue] = useState(0);
    
    const [price, setPrice] = useState();

    useEffect(() => {
        const fetchPrice = async () => {
            const result = await CoinGeckoClient.simple.price({
                ids: "bitcoin-cash-sv",
                vs_currencies: "pln",
            });
            setPrice(parseFloat(result.data.['bitcoin-cash-sv'].pln));
            console.log(price);
            setTimeout(fetchPrice, 5000)
        }; 
        fetchPrice(); 
    })

Text

2 Answers

There are a number of problems.

First, calling useEffect without dependencies means it will be ran after each rerender. ie fetchPrice on its own would runs recursively to fetch and update state, each update cause the component to rerender which triggers the useEffect and thus the setTimeout stack up.

SEE: https://reactjs.org/docs/hooks-effect.html#example-using-hooks

change it to

useEffect(
  () => {
    // your code here
  },
  [],
);

then it will run only on mount.

Second, as mentioned in the comments, setState is asynchronous, setting it and then logging it immediately will show the old result.

Issue

  1. You are console logging your state right after it was enqueued, which will only ever log the state from the current render cycle, not what was enqueued for the next render cycle.
  2. Your useEffect hook runs every time the component renders, so you are starting multiple timeouts. I suspect the "appending" you are seeing is the result of duplicate timeouts running the same callback.

Solution

  1. Use an empty dependency array to run the effect once when the component mounts.
  2. Use an interval to run the fetching.
  3. Return a cleanup function to clear any running intervals.
  4. Console log the updated state in its own effect with dependency.

Code:

useEffect(() => {
  const fetchPrice = async () => {
    const result = await CoinGeckoClient.simple.price({
      ids: "bitcoin-cash-sv",
      vs_currencies: "pln",
    });
    setPrice(parseFloat(result.data.['bitcoin-cash-sv'].pln));
  };

  fetchPrice(); // <-- first initial fetch

  const timerId = setInterval(fetchPrice, 5000); // <-- start interval

  return () => clearInterval(timerId); // <-- return cleanup
}, []); // <-- run on mount

useEffect(() => {
  console.log(price); // <-- log updated state
}, [price]); // <-- run on price update
Related