How to fetch request in regular intervals using the useEffect hook? - reactjs

What I'm trying to do is fetch a single random quote from a random quote API every 5 seconds, and set it's contents to a React component.
I was able to fetch the request successfully and display it's contents, however after running setInterval method with the fetching method fetchQuote, and a 5 seconds interval, the contents are updated multiple times in that interval.
import { Badge, Box, Text, VStack, Container} from '#chakra-ui/react';
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const RandomQuotes = () => {
const [quote, setQuote] = useState<Quote>(quoteObject);
const [error, setError]: [string, (error: string) => void] = React.useState("");
const [loading, setLoading] = useState(true);
const fetchQuote = () => {
axios.get<Quote>(randomQuoteURL)
.then(response => {
setLoading(false);
setQuote(response.data);
})
.catch(ex => {
setError(ex);
console.log(ex)
});
}
setInterval(() => setLoading(true), 5000);
useEffect(fetchQuote, [loading, error]);
const { id, content, author } = quote;
return (
<>
<RandomQuote
quoteID={id}
quoteContent={content}
quoteAuthor={author}
/>
</>
);
}

When any state or prop value gets updated, your function body will re-run, which is called a re-render.
And you've put setInterval call in the main function(!!!), so each time the component re-renders, it will create another interval again and again. Your browser will get stuck after a few minutes.
You need this interval definition once, which is what useEffect with an empty second parameter is for.
Also, using loading flag as a trigger for an API call works, but semantically makes no sense, plus the watcher is expensive and not needed.
Here's a rough correct example:
useEffect(() => {
const myInterval = setInterval(fetchQuote, 5000);
return () => {
// should clear the interval when the component unmounts
clearInterval(myInterval);
};
}, []);
const fetchQuote = () => {
setLoading(true);
// your current code
};

Related

Unsubscribe from listener in hook or in screen component?

I created a hook to manipulate users data and one function is listener for users collection.
In hook I created subscriber function and inside that hook I unsubscribed from it using useEffect.
My question is is this good thing or maybe unsubscriber should be inside screen component?
Does my approach has cons?
export function useUser {
let subscriber = () => {};
React.useEffect(() => {
return () => {
subscriber();
};
}, []);
const listenUsersCollection = () => {
subscriber = firestore().collection('users').onSnapshot(res => {...})
}
}
In screen component I have:
...
const {listenUsersCollection} = useUser();
React.useEffect(() => {
listenUsersCollection();
}, []);
What if I, by mistake, call the listenUsersCollection twice or more? Rare scenario, but your subscriber will be lost and not unsubscribed.
But generally speaking - there is no need to run this useEffect with listenUsersCollection outside of the hook. You should move it away from the screen component. Component will be cleaner and less chances to get an error. Also, easier to reuse the hook.
I prefer exporting the actual loaded user data from hooks like that, without anything else.
Example, using firebase 9 modular SDK:
import { useEffect, useMemo, useState } from "react";
import { onSnapshot, collection, query } from "firebase/firestore";
import { db } from "../firebase";
const col = collection(db, "users");
export function useUsersData(queryParams) {
const [usersData, setUsersData] = useState(undefined);
const _q = useMemo(() => {
return query(col, ...(queryParams || []));
}, [queryParams])
useEffect(() => {
const unsubscribe = onSnapshot(_q, (snapshot) => {
// Or use another mapping function, classes, anything.
const users = snapshot.docs.map(x => ({
id: x.id,
...x.data()
}))
setUsersData(users);
});
return () => unsubscribe();
}, [_q]);
return usersData;
}
Usage:
// No params passed, load all the collection
const allUsers = useUsersData();
// If you want to pass a parameter that is not
// a primitive or a string
// memoize it!!!
const usersFilter = useMemo(() => {
return [
where("firstName", "==", "test"),
limit(3)
];
}, []);
const usersFiltered = useUsersData(usersFilter);
As you can see, all the loading and cleaning-up logic is inside the hook, and the component that uses this hook is as clear as possible.

How to create a debounce-like function using variables in React?

I have a component in React that essentially autosaves form input 3 seconds after the user's last keystroke. There are possibly dozens of these components rendered on my webpage at a time.
I have tried using debounce from Lodash, but that did not seem to work (or I implemented it poorly). Instead, I am using a function that compares a local variable against a global variable to determine the most recent function call.
Curiously, this code seems to work in JSFiddle. However, it does not work on my Desktop.
Specifically, globalEditIndex seems to retain its older values even after the delay. As a result, if a user makes 5 keystrokes, the console.log statement runs 5 times instead of 1.
Could someone please help me figure out why?
import React, {useRef, useState} from "react";
import {connect} from "react-redux";
import {func1, func2} from "../actions";
// A component with some JSX form elements. This component shows up dozens of times on a single page
const MyComponent = (props) => {
// Used to store the form's state for Redux
const [formState, setFormState] = useState({});
// Global variable that keeps track of number of keystrokes
let globalEditIndex = 0;
// This function is called whenever a form input is changed (onchange)
const editHandler = (e) => {
setFormState({
...formState,
e.target.name: e.target.value,
});
autosaveHandler();
}
const autosaveHandler = () => {
globalEditIndex++;
let localEditIndex = globalEditIndex;
setTimeout(() => {
// Ideally, subsequent function calls increment globalEditIndex,
// causing earlier function calls to evaluate this expression as false.
if (localEditIndex === globalEditIndex) {
console.log("After save: " +globalEditIndex);
globalEditIndex = 0;
}
}, 3000);
}
return(
// JSX code here
)
}
const mapStateToProps = (state) => ({
prop1: state.prop1,
prop2: state.prop2
});
export default connect(
mapStateToProps, { func1, func2 }
)(MyComponent);
Note: I was typing up answer on how I solved this previously in my own projects before I read #DrewReese's comment - that seems like a way better implementation than what I did, and I will be using that myself going forward. Check out his answer here: https://stackoverflow.com/a/70270521/8690857
I think you hit it on the head in your question - you are probably trying to implement debounce wrong. You should debounce your formState value to whatever delay you want to put on autosaving (if I'm assuming the code correctly).
An example custom hook I've used in the past looks like this:
export const useDebounce = <T>(value: T, delay: number) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value]);
return debouncedValue;
};
// Without typings
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
};
Which you can then use like so:
const [myValue, setMyValue] = useState<number>(0);
const debouncedValue = useDebounce<number>(myValue, 3000);
useEffect(() => {
console.log("Debounced value: ", debouncedValue);
}, [debouncedFormState]);
// without typings
const [myValue, setMyValue] = useState(0);
const debouncedValue = useDebounce(myValue, 3000);
useEffect(() => {
console.log("Debounced value: ", debouncedValue);
}, [debouncedFormState]);
For demonstration purposes I've made a CodeSandbox demonstrating this useDebounce function with your forms example. You can view it here:
https://codesandbox.io/s/brave-wilson-cjl85v?file=/src/App.js

React wont update state on useEffect

I am new in react. can anyone explain why the loading is not updating its value. on console the loading is 1
import React, { useState, useEffect } from "react";
function App() {
useEffect(() => {
hai();
}, []);
const [loading, setLoading] = useState(1);
const hai = () => {
console.log("............");
setLoading(2);
console.log(loading);
};
return <></>;
}
export default App;
Also if there are two state variables, and rearrange the set function, whole application breaks
const [loading, setLoading]=useState(false)
const [datas, setDatas]=useState([])
//works fine if loading is set second
const hai = () => {
setDatas(res) //fetched from external api
setLoading(true)
};
//throws error in applicaton
const hai = () => {
setLoading(true)
setDatas(res) //fetched from external api
};
console.log(datas)
You are testing your loading value in a wrong way,this is how you should be doing it:
useEffect(() => {
console.log(loading);
}, [loading]);
and remove console.log(loading) from the function hai
Whenever you want to access an updated value of some variable then put it inside an useEffect and put the value which you want to check inside the useEffect's dependency array.

Loading Data with useEffect

So I have a Route that loads a dashboard component, and a sidebar with different links to this dashboard component I'm attempting to use useEffect to load the appropriate data from the backend when the component is loaded
const Dashboard = ({match}) => {
const dispatch = useDispatch()
const [loading, setLoading] = useState(true)
const thing = useSelector(state => state.things)[match.params.id]
const fetchData = async () => {
setLoading(true)
await dispatch(loadStuff(match.params.id))
setLoading(false)
}
useEffect(() => {
fetchData()
}, [match]);
return loading
? <div>Loading</div>
: <div>{thing.name}</div>
}
This works well enough for the first load. However when I click the NavLink on the sidebar to change { match }, thing.name blows up. I would expect, since match is a dependency on useEffect, that it would restart the load cycle and everything would pause until the load is complete, instead it appears to try to render immediately and the API call is not made at all. If I remove thing.name, I see the api call is made and everything works.
I keep running into this, so I appear to have a fundamental misunderstanding of how to predictably load data with redux when a component is mounted. What am I doing wrong here?
Have you wrapped your component with withRouter of react router dom?
import React, { useEffect } from 'react';
import { withRouter } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
const Dashboard = ({ match }) => {
const dispatch = useDispatch()
const [loading, setLoading] = useState(true)
const thing = useSelector(state => state.things)[match.params.id]
const fetchData = async () => {
setLoading(true)
await dispatch(loadStuff(match.params.id))
setLoading(false)
}
useEffect(() => {
fetchData()
}, [match.params.id]);
return loading
? <div>Loading</div>
: <div>{thing.name}</div>
}
export default withRouter(Dashboard);
Use match.params.id in the useEffect as comparison of object(match) should not be done.
{} === {} /* will always be false and useEffect will be called every time
irrespective of match.params.id change. */

Infinite loop in useEffect when setting state

I've got a question about useEffect and useState inside of it.
I am building a component:
const [id, setId] = useState(0);
const [currencies, setCurrencies] = useState([]);
...
useEffect(()=> {
const getCurrentCurrency = async () => {
const response = await fetch(`https://api.exchangeratesapi.io/latest?base=GBP`);
const data = await response.json();
const currencyArray = [];
const {EUR:euro ,CHF:franc, USD: dolar} = data.rates;
currencyArray.push(euro, dolar/franc,1/dolar);
console.log("currencyArray", currencyArray);
setCurrencies(currencies => [...currencies, currencyArray]);
}
getCurrentCurrency();
}, [id, currencies.length]);
Which is used for making a new API request when only id change. I need to every time that ID change make a new request with new data. In my case now I have infinite loop. I try to use dependencies but it doesn't work as I expected.
You changing a value (currencies.length), which the useEffect depends on ([id, currencies.length]), on every call.
Therefore you cause an infinite loop.
useEffect(() => {
const getCurrentCurrency = async () => {
// ...
currencyArray.push(euro, dolar / franc, 1 / dolar);
// v The length is changed on every call
setCurrencies(currencies => [...currencies, currencyArray]);
};
getCurrentCurrency();
// v Will called on every length change
}, [id,currencies.length]);
You don't need currencies.length as a dependency when you using a functional useState, currencies => [...currencies, currencyArray]
useEffect(() => {
const getCurrentCurrency = async () => {
...
}
getCurrentCurrency();
}, [id]);
Moreover, as it seems an exchange application, you might one to use an interval for fetching the currency:
useEffect(() => {
const interval = setInterval(getCurrency, 5000);
return () => clearInterval(interval);
}, []);
you can just call the useEffect cb one the component mounted:
useEffect(()=>{
//your code
// no need for checking for the updates it they are inside the component
}, []);

Resources