I set an interval inside useEffect to update data every 33 seconds if a state variable can_update is true.
The initial value for can_pdate = true. The problem is, even if I change can_update to false (using disable_update function), in the update_groups function it still comes as true.
const [can_update, set_can_update] = useState(true);
const [groups, set_groups] = useState([]);
const intervalRef = useRef();
useEffect(() => {
update_groups();
const update_interval = setInterval(() => {
update_groups();
}, 33000);
intervalRef.current = update_interval;
return () => {
clearInterval(intervalRef.current);
};
}, [project_data.id]);
const update_groups = () => {
if (can_update) {
UI.get(`/project/${project_data.id}/controllers/`).then(
(data) => {
set_groups(data.groups);
},
(error) => {
console.log("Не удалось загрузить список групп");
},
);
}
};
const enable_update = () => {
set_can_update(true);
};
const disable_update = () => {
set_can_update(false);
};
I've tried moving condition into
setInterval: `const update_interval = setInterval(() => {
if (can_update){ update_groups()};
}
and replacing setInterval for recursive setTimeout. No changes.
I've had somewhat similar code inside a class component, and there didn't seem to be any problems like this.
You need add can_update to useEffect deps, otherwise
all values
from the component scope (such as props and state) that change over
time and that are used by the effect.
https://reactjs.org/docs/hooks-effect.html
In your case useEffect was called once, inside it every 33 seconds a function update_groups was called with scoped value can_update = true.
React.useEffect(() => {
if (can_update) {
update_groups();
const update_interval = setInterval(() => {
update_groups();
}, 33000);
intervalRef.current = update_interval;
return () => {
clearInterval(intervalRef.current);
};
}
}, [project_data.id, can_update]);
const update_groups = () => {
UI.get(`/project/${project_data.id}/controllers/`).then(
data => {
set_groups(data.groups);
},
error => {
console.log('Не удалось загрузить список групп');
},
);
};
Related
I'm using setInterval in useEffect. When use not actively using tab, I't requesting like forever. It causing some memory issue. I have to, fetch data in every 3000ms, also stop it when user is not using this tab actively. How can I do such a thing?
I tried to use document.visibiltyState and I couldn't worked it.
My code:
useEffect(() => {
try {
const interval = setInterval(() => {
getTransactionGroupStats()
getTransactionGroups()
}, 3000)
getBinanceBalanceStats()
return () => {
clearInterval(interval)
}
} catch (error) {
console.error(error)
}
}, [])
Another alternative and a little more scalable could be that you create a custom hook to see if the user is active or not and every time it changes you execute the useEffect
useActive.ts
export const useActive = (time: number) => {
const [active, setActive] = useState(false)
const timer: any = useRef()
const events = ['keypress', 'mousemove', 'touchmove', 'click', 'scroll']
useEffect(() => {
const handleEvent = () => {
setActive(true)
if (timer.current) {
window.clearTimeout(timer.current)
}
timer.current = window.setTimeout(() => {
setActive(false)
}, time)
}
events.forEach((event: string) => document.addEventListener(event, handleEvent))
return () => {
events.forEach((event: string) => document.removeEventListener(event, handleEvent))
}
}, [time])
return active
}
YourApp.tsx
const active = useActive(3000)
useEffect(() => {
if(active){
try {
const interval = setInterval(() => {
getTransactionGroupStats()
getTransactionGroups()
}, 3000)
getBinanceBalanceStats()
return () => {
clearInterval(interval)
}
} catch (error) {
console.error(error)
}
}
}, [active])
I solved with this approach: https://blog.sethcorker.com/harnessing-the-page-visibility-api-with-react/
export function usePageVisibility () {
const [isVisible, setIsVisible] = useState(!document.hidden)
const onVisibilityChange = () => setIsVisible(!document.hidden)
React.useEffect(() => {
document.addEventListener('visibilitychange', onVisibilityChange, false)
return () => {
document.removeEventListener('visibilitychange', onVisibilityChange)
}
})
return isVisible
}
I'm trying to call API using debounce but in this case, API calling for every character,
for example, I type hello in search then it calls for he, hel, hell, and hello but I want only for final word hello
useEffect(() => {
updateDebounceWord(word);
}, [word]);
const updateDebounceWord = debounce(() => {
{
word.length > 1 && dictionaryApi();
}
});
function debounce(cb, delay = 500) {
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
cb(...args);
}, delay);
};
}
const dictionaryApi = async () => {
// inital state []
console.log("hited")
try {
const data = await axios.get(
`https://api.dictionaryapi.dev/api/v2/entries/${category}/${word}`
);
console.log("Fetched",word);
setMeanings(data.data);
} catch (e) {
console.log("error||", e);
}
};
In addition to Dilshans explanation, I wan't to suggest making a hook out of your debounce function, so you can easily reuse it:
const useDebounce = (cb, delay = 500) => {
const timer = useRef();
// this cleans up any remaining timeout when the hooks lifecycle ends
useEffect(() => () => clearTimeout(timer.current), [cb, delay]);
return useCallback(
(...args) => {
clearTimeout(timer.current);
timer.current = setTimeout(() => {
cb(...args);
}, delay);
},
[cb, delay]
);
};
use it like this in your components:
const updateDebounceWord = useDebounce((word) => {
console.log("api call here", word);
});
useEffect(() => {
updateDebounceWord(word);
}, [word, updateDebounceWord]);
You are using the debounce on render phase of the component. so each time when the component rebuild a new tree due to the state update, the updateDebounceWord will redeclare. Both current and workInProgress node of the component will not share any data. If you want to share the data between current and workInProgress tree use useRef or else put in global scope
A quick fix is, put the timer variable in global scope.
// keep this on global scope
let timer = null;
function debounce(cb, delay = 500) {
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
cb(...args);
}, delay);
};
}
export default function App() {
const [word, setWord] = useState("");
const sendReq = debounce((keyword) => {
apiReq(keyword);
})
useEffect(() => {
if (word.length > 0) {
sendReq(word);
}
}, [word, sendReq])
const apiReq = (keyword) => {
console.log('reached', keyword);
}
return (
<div className="App">
<input value={word} onChange={(e) => setWord(e.target.value)} />
</div>
);
}
Also put all the dependencies in the useEffect dep array otherwise it may not work as expected.
useEffect(() => {
updateDebounceWord(word);
}, [word, updateDebounceWord]);
In react native app, there are two components A and B in App.js which toggle with k state change.
App.js
...
const App = () => {
const [ k, setK ] = useState(false);
const toggleK = () => setK(!k);
if(k) {
return <A />;
}else {
return <B toggleK={toggleK} />;
}
};
...
In A, setInterval is initialized in useffect. It calls async function every 10 seconds. But when it unmounts on K state change in App.js, the cleaning function is not run (no A unmounting... is logged) and so does the clearInterval.
Any thing I'm doing wrong here?
...
const A = () => {
const [ someState, setSomeState ] = useState(...);
let timer;
useEffect(() => {
if(!timer) {
timer = setInterval(async () => await run_async_func(), 10000);
}
return () => {
console.log('A unmounting...');
clearInterval(timer);
};
}, [ someState ]);
};
...
Try this with useRef().
const A = () => {
const timer = useRef()
const [ someState, setSomeState ] = useState(...);
useEffect(() => {
if(!timer) {
timer.current = setInterval(async () => await run_async_func(), 10000);
}
return () => {
console.log('A unmounting...');
clearInterval(timer.current);
};
}, [ someState ]);
};
Resolved the issue
What was causing the issue?
A. Overlooked the someState in useEffect dependency array. Basically, cleaner function will only run if any variable in dependency array mutates. So, I used another useEffect without any dependencies (see below). Thanks #TayyabMazhar for pointing out this
...
const A = () => {
const [ someState, setSomeState ] = useState(...);
let timer;
useEffect(() => {
if(!timer) {
timer = setInterval(async () => await run_async_func(), 10000);
}
}, [ someState ]);
useEffect(() => {
return () => {
console.log('A unmounting...');
clearInterval(timer);
};
}, []);
};
...
When I use useEffect I can prevent the state update of an unmounted component by nullifying a variable like this
useEffect(() => {
const alive = {state: true}
//...
if (!alive.state) return
//...
return () => (alive.state = false)
}
But how to do this when I'm on a function called in a button click (and outside useEffect)?
For example, this code doesn't work
export const MyComp = () => {
const alive = { state: true}
useEffect(() => {
return () => (alive.state = false)
}
const onClickThat = async () => {
const response = await letsbehere5seconds()
if (!alive.state) return
setSomeState('hey')
// warning, because alive.state is true here,
// ... not the same variable that the useEffect one
}
}
or this one
export const MyComp = () => {
const alive = {}
useEffect(() => {
alive.state = true
return () => (alive.state = false)
}
const onClickThat = async () => {
const response = await letsbehere5seconds()
if (!alive.state) return // alive.state is undefined so it returns
setSomeState('hey')
}
}
When a component re-renders, it will garbage collect the variables of the current context, unless they are state-full. If you want to persist a value across renders, but don't want to trigger a re-renders when you update it, use the useRef hook.
https://reactjs.org/docs/hooks-reference.html#useref
export const MyComp = () => {
const alive = useRef(false)
useEffect(() => {
alive.current = true
return () => (alive.current = false)
}
const onClickThat = async () => {
const response = await letsbehere5seconds()
if (!alive.current) return
setSomeState('hey')
}
}
I am trying to access clearTimerInterval in clearTimer method but getting undefined , got the waring variable from inside React Hook will be lost after each render. in below code useEffect hook called once once then how variable clearTimerInterval got undefined?
function Child(props) {
let [timerCount, setTimer] = useState(0);
var clearTimerInterval;
useEffect(() => {
clearTimerInterval = setInterval(() => {
setTimer(timerCount => {
return timerCount + 1;
});
}, 1000);
return () => {
clearInterval(clearTimerInterval);
};
}, []);
function clearTimer() {
clearInterval(clearTimerInterval);
}
return (
<div>
<div>Timer {timer}</div>
<button onClick={clearTimer}>ClearTimer</button>
</div>
);
}
export default React.memo(Child);
If you need to save variables across re-renders use useRef which in this case acts like a class instance field, also note that mutations to refs does not trigger a re-render.
This will give you the ability to clear the interval from outside of useEffect
function Child(props) {
let [timerCount, setTimer] = useState(0)
const intervalRef = useRef(null)
useEffect(() => {
intervalRef.current = setInterval(() => {
setTimer(prevState => prevState + 1)
}, 1000)
return () => clearInterval(intervalRef.current)
}, [])
function clearTimer() {
clearInterval(intervalRef.current)
intervalRef.current = null
}
return (
<div>
<div>Timer {timerCount}</div>
<button onClick={clearTimer}>ClearTimer</button>
</div>
)
}
Try defining the variable inside of the hook.
useEffect(() => {
var clearTimerInterval;
clearTimerInterval = setInterval(() => {
setTimer(timerCount => {
return timerCount + 1;
});
}, 1000);
return () => {
clearInterval(clearTimerInterval);
};
}, []);