Problem
My component renders every second instead of only rendering if the state passed into setCurrentlyPlaying() is different. How can I fix this behavior?
Background
I would like to check an API every 1 second. Aside from the first render, I only want my component to rerender when the incomingSong differs from the currentlyPlayingSong. I'm banking on the State hook's ability to only trigger a rerender if the passed in state is different than the previous based on the Object.is comparison algorithm. Even if I only return(<div></div>); in my component it continues to render every 1 second, so it seems like the problem stems from this logic. I test this by playing NOTHING on my Spotify so it always goes into the first if statement and it still rerenders every second (prints the console.log("RENDERED") statement).
const [currentlyPlayingSong, setCurrentlyPlayingSong] = useState({ type: 'Now Playing', name: 'No Song Playing', artists: [], imageUrl: ''});
console.log("RENDERED")
useEffect(() => {
console.log("initial effect including updateSongs");
const updateSongs = async () => {
const result = await getCurrentlyPlaying();
var incomingSong = {};
// no song is playing
if(result.data === "") {
incomingSong = { type: 'Now Playing', name: 'No Song Playing', artists: [], imageUrl: ''};
}
// some song is playing
else {
var songJSONPath = result.data.item;
incomingSong = {
type: 'Now Playing',
name: songJSONPath.name,
id: songJSONPath.id,
artists: songJSONPath.artists.map(artist => artist.name + " "),
imageUrl: songJSONPath.album.images[songJSONPath.album.images.length - 2].url,
};
}
// console.log("currentlyPlayingSong: ", currentlyPlayingSong)
// console.log("incomingSong: ", incomingSong);
// this line!!!!
setCurrentlyPlayingSong(incomingSong);
}
updateSongs();
const timer = setInterval(updateSongs, 1000);
return () => clearInterval(timer);
},[]);
Console logging in the body of a functional component is not actually an accurate measure for how often a component is rendered to the DOM, you should do this in an useEffect hook.
Also, updateSongs creates a new incomingSong object reference each time it's invoked, and since react uses shallow reference equality it will trigger a rerender every time you setCurrentlyPlayingSong(incomingSong);. You should do this conditional check manually before updating state with a new value. How you determine equality between the objects is up to you. id seems a good choice.
Check if the song id is different and only update state when it is actually a different song.
if (currentlyPlayingSong.id !== incomingSong.id) {
setCurrentlyPlayingSong(incomingSong);
}
code
const [currentlyPlayingSong, setCurrentlyPlayingSong] = useState({
type: 'Now Playing',
name: 'No Song Playing',
artists: [],
imageUrl: '',
});
useEffect(() => {
console.log("RENDERED"); // <-- logs when component is rendered to DOM
});
useEffect(() => {
console.log("initial effect including updateSongs");
const updateSongs = async () => {
const result = await getCurrentlyPlaying();
let incomingSong = {};
// no song is playing
if (result.data === "") {
...
}
// some song is playing
else {
...
}
// check if song id is different and only update if true
if (currentlyPlayingSong.id !== incomingSong.id) {
setCurrentlyPlayingSong(incomingSong);
}
}
updateSongs();
const timer = setInterval(updateSongs, 1000);
return () => clearInterval(timer);
},[]);
Assuming a song's name indicates the identity of a song. You can add an if statement before updating the state.
if (currentlyPlayingSong.name !== incomingSong.name) {
setCurrentlyPlayingSong(incomingSong);
}
This will make sure to update the state only when the song has changed. If name isn't the correct property to check then replace it with the correct one.
React has no way of understanding that it can prevent the state update as you're passing a new object every time. See this to know more about react compares different values.
Related
I have been trying to figure something out in my React cryptocurrency project. So I have a function that basically polls for a response from the api every 6 seconds. It is supposed to start polling the moment the component renders, so I have it in a useEffect. It has default parameters being passed in for the polling to happen right after render. The polling works in terms that it polls and get the response based on the default values. It also takes in the user inputs and returns a response based on it, however, after the next poll, the arguments being passed to the api for polling are back to their defaults. In other words, the arguments being passed into the polling function on the next polls are all back to the default values I passed in, and not a continuation of the current state of the arguments.
Here is where I define pair, which lives above the useEffect:
const baseAsset = transactionType === TRANSACTION_TYPES.BUY ? selectedCurrencyState.selectedToCurrency : selectedCurrencyState.selectedFromCurrency;
const quoteAsset = transactionType === TRANSACTION_TYPES.SELL ? selectedCurrencyState.selectedToCurrency : selectedCurrencyState.selectedFromCurrency;
const pair = baseAsset && quoteAsset ? `${baseAsset}/${quoteAsset}` : '';
Here is the function that gets polled:
const handleInitPoll = useCallback((baseAndQuote, side, value) => {
setIsLoading(true);
getSwapPrice(baseAndQuote, side, value || 0)
.then((res) => {
if (res.error) {
setErrorMessage(res.error);
} else if (res.price) {
setIsLoading(false);
setSwapPriceInfo(res);
}
});
}, [pair, transactionType, fromCurrencyAmount]);
And here is the useEffect that polls it:
useEffect(() => {
if (isLoggedIn) {
getSwapPairs()
.then((res) => {
setSwapInfo(res.markets);
// Set state of selected currencies on render
if (transactionType === TRANSACTION_TYPES.BUY) {
setSelectedCurrencyState({
...selectedCurrencyState,
selectedToCurrency: (quoteAsset !== symbol) ? symbol : ''
});
}
});
if (symbol === selectedCurrencyState.selectedToCurrency) {
actions.updateChartQuote(selectedCurrencyState.selectedFromCurrency);
}
if (pair && transactionType && symbol === baseAsset) {
handleInitPoll(pair, transactionType, fromCurrencyAmount);
}
const timer = setInterval(handleInitPoll, 6000, pair, transactionType, fromCurrencyAmount);
return () => {
clearInterval(timer);
};
}
setSelectedCurrencyState({ ...selectedCurrencyState, selectedFromCurrency: 'SHIB', selectedToCurrency: 'DASH' });
}, [pair, transactionType, fromCurrencyAmount, symbol]);
Here is the debouncing method:
const debounceOnChange = useCallback(debounce(handleInitPoll, 500, pair, transactionType, fromCurrencyAmount), []);
And here is where the user is entering the input to debounce the api call when a user input is detected onChange:
const handleAssetAmount = (e) => {
const { value } = e.target;
const formattedAmount = handleAssetAmountFormat(value);
setFromCurrencyAmount(formattedAmount);
validateInputAmount(formattedAmount);
debounceOnChange(pair, transactionType, formattedAmount);
};
You should put pair in a useState hook. otherwise, everytime when this component is redenrering. the following part will be executed:
const pair = baseAsset && quoteAsset ? `${baseAsset}/${quoteAsset}` : '';
That's why you got a initial value again.
useState can help you to preserve the value for the whole component lifecircle unless you call set***().
I am trying to create a function for a state of rated movies in Zustand.
The state consists of an array of objects, example with two entries:
ratedMovies: [
{ imdbID: "tt0076759", userRating: "5" },
{ imdbID: "tt0080684", userRating: "10" },
]
Below is the function managing ratedMovies changes. Here is where the issue lies. I want it to check whether an object with the same imdbID is present in ratedMovies state. And if so to update the value of it, instead of adding another object with the same imdbID but a new value.
If I try to change the rating of one of the movies from the above state example (with them in the state ofc), I get the IF console check and the app crashes with the error:
TypeError: Cannot create property 'userRating' on number '0'
If the state is empty or I change the rating of other movies, I get the ELSE console check, but they are still not added into the state.
addUserRating: (rating) => {
console.log("rating object", rating)
set((state) => {
if (state.ratedMovies.find((movie) => movie.imdbID === rating.imdbID)) {
console.log("add rating IF")
let index = state.ratedMovies.findIndex(
(movie) => movie.imdbID === rating.imdbID
)
index.userRating = rating.userRating
return [index, ...state.ratedMovies]
} else {
console.log("add rating ELSE")
return [rating, ...state.ratedMovies]
}
})
}
the onChange function on the input where one can rate a movie creates an identical object as in the state array and passes it to the function managing the state of ratedMovies:
const changeUserMovieRating = (event) => {
const movieRating = {
imdbID: modalDetails.imdbID,
userRating: event.target.value,
}
console.log(movieRating)
addUserRating(movieRating)
}
Output of which is:
{imdbID: 'tt0120915', userRating: '2'}
I hope i explained everything clearly, and I will highly appreciate any tips on how to solve this issue, thanks in advance!
Sorry but this whole apprach I had at the time of asking this question had no sense and had to be reworked completely.
I decided not to add the parameter during the fetch, as in another component the same data could be fetched. So I decided to instead keep the value of the 'userRating' in the local storage and if the fetched movie was already once rated by the 'user', the value would be displayed.
I have read several cases on stackoverflow regarding missing dependencies in useEffect:
example :
How to fix missing dependency warning when using useEffect React Hook?
Now the case I have is, I use useEffect for pagination:
Here's the source code:
react-router-dom configuration
{ path: "/note", name: "My Note", exact: true, Component: Note },
Note Component
const Note = (props) => {
const getQueryParams = () => {
return window.location.search.replace("?", "").split("&").reduce((r, e) => ((r[e.split("=")[0]] = decodeURIComponent(e.split("=")[1])), r),
{}
);
};
const MySwal = withReactContent(Swal);
const history = useHistory();
// Get Queries On URL
const { page: paramsPage, "per-page": paramsPerPage } = getQueryParams();
// Queries as state
const [queryPage, setQueryPage] = useState(
paramsPage === undefined ? 1 : paramsPage
);
const [queryPerPage, setQueryPerPage] = useState(
paramsPerPage === undefined ? 10 : paramsPerPage
);
// Hold Data Records as state
const [notes, setNotes] = useState({
loading: false,
data: [],
totalData: 0,
});
useEffect(() => {
console.log(queryPage, queryPerPage);
setNotes({
...notes,
loading: true,
});
// Fetching data from API
NoteDataService.getAll(queryPage, queryPerPage)
.then((response) => {
setNotes({
loading: false,
data: response.data,
totalData: parseInt(response.headers["x-pagination-total-count"]),
});
return true;
})
.catch((e) => {
MySwal.fire({
title: e.response.status + " | " + e.response.statusText,
text: e.response.data,
});
});
return false;
}, [queryPage, queryPerPage]);
const { loading, data, totalData } = notes;
...
So there are two problems here:
There is a warning React Hook use Effect has missing dependencies: 'MySwal' and 'notes'. Either include them or remove the dependency array. You can also do a functional update 'setNotes (n => ...)' if you only need 'notes' in the 'setNotes' call. If I add notes and MySwal as dependencies, it gives me a continuous loop.
When I access the "note" page, the Note component will be rendered.
Then, with pagination: / note? Page = 2 & per-page = 10, it went perfectly.
However, when returning to "/ note" the page does not experience a re-render.
Strangely, if a route like this / note? Page = 1 & per-page = 10, returns perfectly.
Does my useEffect not run after pagination?
First of all, move your API call inside of useEffect. After your data is fetched, then you can change the state.
useEffect(() => {
//Fetch the data here
//setState here
},[]) //if this array is empty, you make the api call just once, when the `component mounts`
Second Argument of useEffect is a dependancy array, if you don't pass it, your useEffect will trigger in every render and update, which is not good. If you parss an empty array, then it makes just one call, if you pass a value, then react renders only if the passed value is changed.
I am trying to set the state in request data method for a string variable like this below
const ViewChangeRequest = () => {
const [requestStageValue, setRequestStage] = useState('');
const { data: requestData, loading: requestDataLoading, error: requestDataError } =
useQuery(GET_SPECIFICREQUEST, {
variables: { requestinputs: { id } },
});
if (requestData != null) {
setRequestStage(requestData.allRequests[0].requestStage.name); // getting error at here
requestData.allRequests.map((code) => {
requestDatasource.push({
id: code.id,
section: code.masterSection.name,
createdBy: code.createdBy,
type: code.requestType.name,
status: code.requestStage.name,
createat: code.createdAt,
});
return null;
});
}
};
export default withRouter(ViewChangeRequest);
Depends upon the requeststage value, i am verifying conditions like this below
if (requestStageValue === 'Request Submitted') {
stepNumber = 0;
} else if (requestStageValue === 'In Review') {
stepNumber = 1;
} else {
stepNumber = 2;
}
I am getting an error Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop at this line setRequestStage(requestData.allRequests[0].requestStage.name)
I am not able to understand where i am doing wrong while setting up the state to string varaiable.
Could any one please help me out from this situation that would be very grateful to me, many thanks in advance.
Well you're running setRequestStage in the function. This will trigger a state update, which means the functions runs again (since state updates trigger re-renders), which means setRequestStage runs again, which means the state updates again, so functions runs again etc. etc. - infinite loop.
If you're wanting to set an initial state based on requestData, do it when declaring it in useState:
const [requestStageValue, setRequestStage] = useState(
requestData !== null ? requestData.allRequests[0].requestStage.name : ''
);
How to re-render the data? I update the data but it is not output.
Sample code on the line 34.
setInterval(() => {
let d= this.state.data;
d.push({
image: 'https://placekitten.com/200/240',
text: 'Chloe3',
})
this.setState({
data: d
})
}, 1500)
I'm using the plugin: pinreact-native-sortable-list
You are not properly updating your state variable. You are mutating the state by inserting a new element into the data array. This change is not triggering any state update, hence render method is not called. As per React document, we should not mute the state directly
Never mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable.
Change the state update as (using ES6)
setInterval(() => {
let d= this.state.data;
const newRecord = {
image: 'https://placekitten.com/200/240',
text: 'Chloe3',
};
this.setState({
data: [ ...d, newRecord]
})
}, 1500);
Or using Array.concat
this.setState({
data: this.state.data.concat([newRecord])
});
Hope this will help!