I'm new to react, today I encounter a very strange case, what I want todo is to get the data from server side every 5 seconds, but it doesn't work as expected, the problem can be simplied to this:
npx create-react-app my-app
cd my-app
edit the App.js
import { useState } from "react";
function App() {
const [count, setCount] = useState(0)
const increment = () => {
// simulate the querying, for example, checking the status of server
console.log('Current count value:', count)
setCount(count + 1)
}
setInterval(increment, 1000)
return (
<span>Count: {count}</span>
);
}
export default App;
I want to update the state count every 1s, and I think the output in console would be
Current count value: 0
Current count value: 1
Current count value: 2
...
However, the output is very strange, click the link to see the output
(I cant' insert image in the content)
chrome console output
Thanks
You need to start interval in useEffect and clear when component unmount
import React, { useState, useEffect } from "react";
function App() {
const [count, setCount] = useState(0)
useEffect(() => {
const increment = () => {
// simulate the querying, for example, checking the status of server
console.log('Current count value:', count)
setCount(prev => prev + 1) // You can't use count state because it's will trigger useEffect and cause infinite loop. You need to use callback to update from previous value
}
const intervalId = setInterval(increment, 1000) // setInterval return interval id
return () => {
// Clear interval using intervalId
// This function run when component unmount
clearInterval(intervalId)
}
}, [])
return (
<span>Count: {count}</span>
);
}
export default App;
You can use setTimeout instead setinterval-is-moderately-evil
const DURATION = 1000;
const [count, setCount] = React.useState(0);
React.useEffect(() => {
setTimeout(() => {
setCount(count + 1);
}, DURATION);
});
You can read more of how to use setInterval in react here making-setinterval-declarative-with-react-hooks
You need to use useEffect hook to keep track of changes in the component's states.
trying to invoke a function inside the body of your component will make that function to re-invoke everytime any DOM is updated. The best practice to initiate your component is to put it inside a useEffect hook without dependencies.
useEffect(() => {
setInterval(increment, 1000)
}, []);
Now if you want to keep track of a state when it's updated you can use anotther useEffect and add the state in dependency array. Like:
useEffect(() => {
console.log('incremented', count)
}, [count]);
Here is an edited version of your code:
import { useState, useEffect } from "react";
function App() {
const [count, setCount] = useState(0)
// a function that changes count state
const increment = () => {
setCount(prevState => prevState + 1)
}
// when component is mounted
useEffect(() => {
setInterval(increment, 1000)
}, []);
// when count state changes
useEffect(() => {
console.log('incremented', count)
}, [count]);
return (
<span>Count: {count}</span>
);
}
export default App;
Related
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
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
};
I'm checking if a component is unmounted, in order to avoid calling state update functions.
This is the first option, and it works
const ref = useRef(false)
useEffect(() => {
ref.current = true
return () => {
ref.current = false
}
}, [])
....
if (ref.current) {
setAnswers(answers)
setIsLoading(false)
}
....
Second option is using useState, which isMounted is always false, though I changed it to true in component did mount
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
return () => {
setIsMounted(false)
}
}, [])
....
if (isMounted) {
setAnswers(answers)
setIsLoading(false)
}
....
Why is the second option not working compared with the first option?
I wrote this custom hook that can check if the component is mounted or not at the current time, useful if you have a long running operation and the component may be unmounted before it finishes and updates the UI state.
import { useCallback, useEffect, useRef } from "react";
export function useIsMounted() {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
Usage
function MyComponent() {
const [data, setData] = React.useState()
const isMounted = useIsMounted()
React.useEffect(() => {
fetch().then((data) => {
// at this point the component may already have been removed from the tree
// so we need to check first before updating the component state
if (isMounted()) {
setData(data)
}
})
}, [...])
return (...)
}
Live Demo
Please read this answer very carefully until the end.
It seems your component is rendering more than one time and thus the isMounted state will always become false because it doesn't run on every update. It just run once and on unmounted. So, you'll do pass the state in the second option array:
}, [isMounted])
Now, it watches the state and run the effect on every update. But why the first option works?
It's because you're using useRef and it's a synchronous unlike asynchronous useState. Read the docs about useRef again if you're unclear:
This works because useRef() creates a plain JavaScript object. The only difference between useRef() and creating a {current: ...} object yourself is that useRef will give you the same ref object on every render.
BTW, you do not need to clean up anything. Cleaning up the process is required for DOM changes, third-party api reflections, etc. But you don't need to habit on cleaning up the states. So, you can just use:
useEffect(() => {
setIsMounted(true)
}, []) // you may watch isMounted state
// if you're changing it's value from somewhere else
While you use the useRef hook, you are good to go with cleaning up process because it's related to dom changes.
This is a typescript version of #Nearhuscarl's answer.
import { useCallback, useEffect, useRef } from "react";
/**
* This hook provides a function that returns whether the component is still mounted.
* This is useful as a check before calling set state operations which will generates
* a warning when it is called when the component is unmounted.
* #returns a function
*/
export function useMounted(): () => boolean {
const mountedRef = useRef(false);
useEffect(function useMountedEffect() {
mountedRef.current = true;
return function useMountedEffectCleanup() {
mountedRef.current = false;
};
}, []);
return useCallback(function isMounted() {
return mountedRef.current;
}, [mountedRef]);
}
This is the jest test
import { render, waitFor } from '#testing-library/react';
import React, { useEffect } from 'react';
import { delay } from '../delay';
import { useMounted } from "./useMounted";
describe("useMounted", () => {
it("should work and not rerender", async () => {
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
callback(isMounted())
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
expect(callback.mock.calls).toEqual([[true]])
unmount();
expect(callback.mock.calls).toEqual([[true]])
})
it("should work and not rerender and unmount later", async () => {
jest.useFakeTimers('modern');
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
(async () => {
await delay(10000);
callback(isMounted());
})();
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
await waitFor(() => expect(callback).toBeCalledTimes(0));
jest.advanceTimersByTime(5000);
unmount();
jest.advanceTimersByTime(5000);
await waitFor(() => expect(callback).toBeCalledTimes(1));
expect(callback.mock.calls).toEqual([[false]])
})
})
Sources available in https://github.com/trajano/react-hooks-tests/tree/master/src/useMounted
This cleared up my error message, setting a return in my useEffect cancels out the subscriptions and async tasks.
import React from 'react'
const MyComponent = () => {
const [fooState, setFooState] = React.useState(null)
React.useEffect(()=> {
//Mounted
getFetch()
// Unmounted
return () => {
setFooState(false)
}
})
return (
<div>Stuff</div>
)
}
export {MyComponent as default}
If you want to use a small library for this, then react-tidy has a custom hook just for doing that called useIsMounted:
import React from 'react'
import {useIsMounted} from 'react-tidy'
function MyComponent() {
const [data, setData] = React.useState(null)
const isMounted = useIsMounted()
React.useEffect(() => {
fetchData().then((result) => {
if (isMounted) {
setData(result)
}
})
}, [])
// ...
}
Learn more about this hook
Disclaimer I am the writer of this library.
Near Huscarl solution is good, but there is problem with using these hook with react router, because if you go from example news/1 to news/2 useRef value is set to false because of unmount, but value keep false. So you need init ref value to true on each mount.
import {useRef, useCallback, useEffect} from "react";
export function useIsMounted(): () => boolean {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
isMountedRef.current = true;
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
It's hard to know without the larger context, but I don't think you even need to know whether something has been mounted. useEffect(() => {...}, []) is executed automatically upon mounting, and you can put whatever needs to wait until mounting inside that effect.
I am facing an abnormal output on the browser from React while using useEffect hook.
I would request you to please have a look at the code. You can copy and paste the code on any online IDE that supports React to visualize the behavior on the browser.
I want the counter to increment after every 1 second. But with the code it stucks after 10.
import { useState, useEffect } from "react";
function App() {
const initialState = 0;
const [count, setCount] = useState(initialState);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 1000);
// return () => {
// clearInterval(interval);
// };
}, [count]);
return (
<div className="App">
<h1>{count}</h1>
</div>
);
}
export default App;
I want to know the reason for that. Why is it happening?
But when I do cleanup with useEffect to do componentWillUnmoint() it behaves normal and renders the counter every second properly. I have intentionally comment cleanup part of code useEffect.
You are adding an interval on every render, soon enough, your thread will be overloaded with intervals.
I guess you wanted to run a single interval, its done by removing the closure on count by passing a function to state setter ("functional update"):
import { useState, useEffect } from "react";
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return (
<div className="App">
<h1>{count}</h1>
</div>
);
}
export default App;
I'm trying to storage a single state and I cannot do that apparently because of a infinite loop. Could you help me?
import React, { useState, useEffect } from "react";
const App = () => {
const [rows, setRows] = useState("Inicial State");
function init() {
const data = localStorage.getItem("my-list");
if (data) {
setRows(JSON.parse(data));
}
localStorage.setItem("my-list", JSON.stringify(rows));
}
useEffect(() => {
init();
});
return (
<div>
<button onClick={() => setRows("Loaded state!")}>Load!</button>
<div>{rows}</div>
</div>
);
};
export default App;
You call init() every time component re-render. Document how to use useEffect here: https://reactjs.org/docs/hooks-effect.html. You should only call one time like componentDidMount in class component by:
useEffect(() => {
init();
}, []);
useEffect(() => {
localStorage.setItem("my-list", JSON.stringify(rows));
}, [rows]);
If you are using useEffect for initialisation, it needs to have an empty dependency array to make sure it only runs onthe first render, not on every render:
useEffect(() => init(), []);