3

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

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
Dedal
  • 107
  • 1
  • 1
  • 8
  • `console.log(price);` will output the old price. – evolutionxbox Apr 08 '21 at 16:37
  • 1
    How is it appending? It looks to me its a different value every time – Dario K Apr 08 '21 at 16:39
  • 1
    Also try adding a dependancy array to your useEffect hook [price] – Dario K Apr 08 '21 at 16:42
  • 1
    You are logging the current state, see https://stackoverflow.com/questions/54069253/usestate-set-method-not-reflecting-change-immediately. Also, your `useEffect` is missing a dependency array so you are starting new timeout "chains" each time the component renders. I'm guessing you meant to only start one when the component mounts, so use an empty dependency array (`[]`) to run the effect only once. Be sure to also capture the timeout id so you can cancel any running timers in a returned effect cleanup function. I also don't see any appending, your state is a number. – Drew Reese Apr 08 '21 at 16:49
  • Totally agree with @drew-reese but, as @dario-k said, why do you say that values are being appended? The `console.log` just displays a number that is being updated with different values – virgiliogm Apr 08 '21 at 16:51
  • @VirgilioGM My guess is it's the multiple timeouts that are enqueued, outputting multiple versions of state. – Drew Reese Apr 08 '21 at 16:53
  • Yes, of course. I was asking to OP why he/she asks that because it's not true – virgiliogm Apr 08 '21 at 16:59

2 Answers2

1

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.

seanplwong
  • 1,051
  • 5
  • 14
  • It may just be me being pedantic, but `setState` is 100% synchronous, it doesn't return a Promise and can't be awaited on. It enqueues an update and the React framework asynchronously processes these enqueued updates. – Drew Reese Apr 08 '21 at 17:05
  • 1
    According to react doc, calls to `setState` is async (https://reactjs.org/docs/faq-state.html#why-is-setstate-giving-me-the-wrong-value) – seanplwong Apr 08 '21 at 17:20
  • 1
    A function that doesn't return promise doesn't necessarily means it is synchronous, it just provide no means of notifying the caller when the operation is finished. `setTimeout` also doesn't return promise, but it is definitely performing task asynchronously. I guess `setState` is utilizing `requestAnimationFrame` under the hood, the value will be update in next frame. – seanplwong Apr 08 '21 at 17:22
  • 1
    Not really looking to get into a debate, but functions not declared `async` are simply and absolutely plain old synchronous functions. React state update functions, setTimeout, both are synchronous functions that enqueue a callback to be processed asynchronously. I think you might've misunderstood my comment. `setState` is a synchronous function, React state updates are asynchronous. "asynchronous" !== `async`. – Drew Reese Apr 08 '21 at 17:43
1

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
Drew Reese
  • 165,259
  • 14
  • 153
  • 181