I have some recoil state, that i want to reset.
import { useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil';
...
//should be used for flushing the global recoil state, whenever a user submits an invoice
const resetLabelInvoiceState = useResetRecoilState(labelInvoiceState);
const resetMetaDataState = useResetRecoilState(metadataState);
const resetGlobalAnnotationsState = useResetRecoilState(globalAnnotationState)
I have made function, that i suppoes to reset all the states like this. I have both tried with and without the reset function.
const flushRecoilState = () =>{
console.log('flushed state')
return(
resetLabelInvoiceState(),
resetMetaDataState(),
resetGlobalAnnotationsState()
)
}
...
flushRecoilState()
return history.push('/historyinvoices')
...
When i check the state it is not reset. Is it because the `useResetRecoilState´ is not working properly from the library, is not implemented properly, or is there some other problem.
I could just use the regular useRecoilState hook, and just set the state back to the default value.
Does anybody know why this could be?
I had the same problem today, it turns out to be my own fault. Just put it here for future reference.
My problem was that I changed the set method in the selector, if you customized the set method, you need to check if the incoming value is a DefaultValue.
const targetYear = selector({
key: 'targetYear',
get: ({get}) => get(targetYearAtom),
set: ({set, get}, method) => {
const currentTargetYear = get(targetYearAtom);
switch(method) {
case 'prevYear':
set(targetYearAtom, currentTargetYear - 1);
return;
case 'nextYear':
set(targetYearAtom, currentTargetYear + 1);
return;
default:
if (method instanceof DefaultValue) {
set(targetYearAtom, method);
}
return;
}
},
})
Related
I have an onClick event on an input field called handleLinkChange that validates its content (a YouTube link) and if it's valid calls a function that extract its YouTube ids.
Within the onClick event I update the links state so the user sees if the link is valid and the extraction of ids is in progress (indicated by a loading wheel in the input field). At this point the state updates as desired in the DOM. However, when I arrive in the getIdsFromLink function after setting the state, the links state is still at the initial value. This makes it impossible to alter the links, for example to replace the loading indicator of the link with a checkmark when the ids have been parsed.
// Links state
const [links, setLinks] = useState([{ link: '', state: IS_VALID, errorMessage: '' }])
// onChange event handler
const handleLinkChange = (event, index) => {
const clonedLinks = [...links]
let value = decodeURI(event.target.value.trim())
const { passesRegex, validityMessage } = checkLinkValidity(value)
clonedLinks[index] = {
link: value,
state: passesRegex ? IS_VALIDATING_SERVER_SIDE : IS_ERROR,
errorMessage: validityMessage,
}
setLinks(clonedLinks)
if (clonedLinks[index] !== '' && passesRegex) {
getIdsFromLink(clonedLinks[index].link, index)
}
}
// Parser
const getIdsFromLink = (link, index) => {
console.log('links state', links) // initial state
socket.emit('getIds', link)
socket.on('idsParsed', newIds => {
console.log('links state after parse', links) // initial state
})
}
// Shortened return
return (
links.map((link, index) => (
<Input
value={link.link}
onChange={event => handleLinkChange(event, index)}
/>
{link.link && (
<FontAwesomeIcon
icon={link.state === IS_VALID ? faCheck : link.state === IS_ERROR ? faTimes : faSpinner}
/>
)}
)
))
I know that states are asynchronous and I also tried watching for state changes with useEffect, but I'm unable to refactor my code in that way and I have another state object that heavily depends on the links state, so I gave up on that one.
Even when I try to use flushSync to update the state synchronously, I have the same effect.
I very much appreciate your help! Thanks!
I'm gonna answer my question as I figured it out thanks to Nick in the comments.
Basically I kept the handleLinkChange function unchanged. But in the getIdsFromLink function, when the link has been parsed from the server - that's where I have to update the state - I used setLinks with a function as parameter. The function receives an argument which resembles the current state. All I had to do is to make a copy of the object (for the rerender to work), then make my changes to it and finally return the new object.
const getIdsFromLink = (link, index) => {
socket.emit('getIds', link)
socket.once('idsParsed', ids => {
// Function parameter to get current state
setLinks(_currentLinks => {
const currentLinks = [..._currentLinks] // copy to force rerender
currentLinks[index].state = IS_VALID
currentLinks[index].ids = ids
console.log('finished currentLinks', currentLinks) // newest state
return currentLinks
})
})
}
I have a react app that uses the MS Graph API (so it's a bit difficult to post a minimal reproducible example). It has a state variable called chats that is designed to hold the result of fetching a list of chats from the graph API. I have to poll the API frequently to get new chats.
I query the chats endpoint, build an array of newChats and then setChats. I then set a timeout that refreshes the data every 10 seconds (it checks for premature invocation through the timestamp property stored in the state). If the component is unmounted, a flag is set, live (useRef), which stops the refresh process. Each chat object is then rendered by the Chat component (not shown).
Here's the code (I've edited by hand here to remove some irrelevant bits around styles and event propagation so it's possible that typo's have crept in -- it compiles and runs in reality).
const Chats = () => {
const [chats, setChats] = useState({ chats: [], timestamp: 0 });
const live = useRef(true);
const fetchChats = () => {
if (live.current && Date.now() - chats.timestamp < 9000) return;
fetchData(`${baseBeta}/me/chats`).then(res => {
if (res.value.length === chats.chats.length) return;
const chatIds = chats.chats.map(chat => chat.id);
const newChats = res.value.filter(chat => !chatIds.includes(chat.id));
if (newChats.length > 0) {
setChats(c => ({ chats: [...c.chats, ...newChats], timestamp: Date.now() }));
}
setTimeout(fetchChats, 10000);
});
};
useEffect(() => {
fetchChats();
return () => (live.current = false);
}, [chats]);
return (
<div>
{chats.chats.map(chat => (
<Chat chat={chat} />
))}
</div>
);
};
The Chat component must also make some async calls for data before it is rendered.
This code works, for a second or two. I see the Chat component rendered on the screen with the correct details (chat member names, avatars, etc.), but almost before it has completed rendering I see the list elements being removed, apparently one at a time, though that could just be the way its rendered -- it could be all at once. The list collapses on the screen, showing that the chat state has been cleared out. I don't know why this is happening.
I've stepped through the code in the debugger and I can see the newChats array being populated. I can see the setChats call happen. If I put a breakpoint on that line then it is only invoked once and that's the only line that sets that particular state.
So, what's going on? I'm pretty sure React isn't broken. I've used it before without much trouble. What's changed recently is the inclusion of the refresh code. I'm suspicious that the reset is taking away the state. My understanding is that the fetchChats method will be rendered every time the chats state changes and so should see the current value of the chats state. Just in case this wasn't happening, I passed the chats state from the useEffect like this:
useEffect(() => {
fetchChats(chats);
return () => (live.current = false);
}, [chats]);
With the necessary changes in fetchChats to make this work as expected. I get the same result, the chats state is lost after a few seconds.
Edit
Still Broken:
After #Aleks answer my useEffect now looks like this:
useEffect(() => {
let cancel = null;
let live = true;
const fetchChats = () => {
if (Date.now() - chats.timestamp < 9000) return;
fetchData(`${baseBeta}/me/chats`).then(res => {
if (res.value.length === chats.chats.length) return;
const chatIds = chats.chats.map(chat => chat.id);
const newChats = res.value.filter(chat => chat.chatType === "oneOnOne" && !chatIds.includes(chat.id));
if (newChats.length > 0 && live) {
setChats(c => ({ chats: [...c.chats, ...newChats], timestamp: Date.now() }));
}
cancel = setTimeout(fetchChats, 10000);
});
};
fetchChats();
return () => {
live = false;
cancel?.();
};
}, []);
The result of this is that the chats are loaded, cleared, and loaded again, repeatedly. This is better, at least they're reloading now, whereas previously they would disappear forever. They are reloaded every 10 seconds, and cleared out almost immediately still.
Eventually, probably due to random timings in the async calls, the entries in the list are duplicated and the 2nd copy starts being removed immediately instead of the first copy.
There are multiple problems. First this
setTimeout(fetchChats, 10000); will trigger
useEffect(() => {
fetchChats(chats);
return () => (live.current = false);
}, [chats])
You will get 2 fetches one after another.
But the bug you're seeing is because of this
return () => (live.current = false);
On second useEffect trigger, clean up function above with run and live.current will be forever false from now on.
And as Nikki9696 said you you need to clear Timeout in clean up function
The easiest fix to this is, probably
useEffect(() => {
let cancel = null;
let live = true;
const fetchChats = () => {
// not needed
//if ( Date.now() - chats.timestamp < 9000) return;
fetchData(`${baseBeta}/me/chats`).then(res => {
//this line is not needed
//if (res.value.length === chats.chats.length) return;
// remove all the filtering, it can be done elsewhere where
// you can access fresh chat state
//const chatIds = chats.chats.map(chat => chat.id);
//const newChats = res.value.filter(chat =>
//!chatIds.includes(chat.id));
if (res.value?.length > 0&&live) {
setChats(c => ({ chats: [...c.chats, ...res.value], timestamp: Date.now() }));
cancel = setTimeout(fetchChats, 10000);
}
});
};
fetchChats()
return () => { live=false; if(cancel)window.clearTimeout(cancel) };
}, []);
Edit: typo cancel?.() to window.clearTimeout(cancel);
Ok, I have an idea what's happening and how to fix it. I am still not sure why it is behaving like this, so please comment if you understand it better than me.
Basically, for some reason I don't understand, the function fetchChats only ever sees the initial state of chats. I am making the mistake of filtering my newly fetched list against this state, in which the array is empty.
If I change my useEffect code to do this instead:
setChats(c => {
return {
chats: [
...c.chats,
...res.value.filter(cc => {
const a = c.chats.map(chat => chat.id);
return !a.includes(cc.id);
})
],
timestamp: Date.now()
};
});
Then my filter is passed the current value of the state for chats rather than the initial state.
I thought that because the function containing this code is in the function that declares the chat state, whenever that state changed the whole function would be rendered with the new value of chats making it available to its nested functions. This isn't the case here and I don't understand why.
The solution, to only trust the values of the state that is handed to me during the setState (setChats) call, works fine and I'll go with it, but I'd love to know what is wrong with reading the state directly.
I use Redux for state management of React JS but I have an event handler relevant to a button in my class component like below:
handleStartBtn = e => {
const name = document.getElementById("nameInput").value;
const error = "You must set a value in this field";
this.props.createPoll(name);
if (!this.props.currentPoll) return;
cookies.set("assignments", `EHS_${this.props.currentPoll.uuid}`, {
path: "/"
});
this.props.history.push("/polling");
};
The createPoll function is in my actions file. after creating a poll I dispatched a success function and it hits the store then it changes the currentPoll property. but it seems like it will leave null after calling createPoll dispatched I must clicked on startBtn twice till the store to be updated. how can I use the value of store after dispatch method call?
You can try using componentDidUpdate() life cycle method for this issue.
handleStartBtn = e => {
const name = document.getElementById("nameInput").value;
const error = "You must set a value in this field";
this.props.createPoll(name);
};
componentDidUpdate(prevProps,prevState){
if(prevProps.currentPoll !== this.props.currentPoll){
cookies.set("assignments", `EHS_${this.props.currentPoll.uuid}`, {
path: "/"
});
this.props.history.push("/polling");
}
}
when you dispatch the action through this.props.createPoll(name); you redux state of currentPoll will be changed and then this change takes place the componentDidUpdate will be triggered and in here you can check it (though a proper condition) and continue what needs to be done after that.
I have the following react code, that represents a text input.
onChangeDestination(url, index) {
this.setState(prevState => {
const rules = [...prevState.rules];
rules[index] = { ...rules[index], url};
if (isURL(url+'')) {
testURL(url).then(r=> {
var url_status = r.data.response
rules[index] = { ...rules[index], url_status};
})
} else {
var url_status = "No URL"
rules[index] = { ...rules[index], url_status};
}
return { rules };
});
};
In English:
If the URL passes isURL() validation, then use custom function testURL() to see what the HTTP status of that URL is (using axios).
In my template, there's a {props.url_status} for the relevant bit.
The issue is, even though it's logging to the console the desired behaviour, it doesn't seem to be updating the viewport reliably, which I think is linked to the promise.
What am I doing wrong?
You could achieve it by converting your function to be asynchronous and calling your promise (if necessary) before your setState. This solution uses the easier to read async/await syntax and a ternary condition to choose the correct status value :
const url_status = isURL(url + '') ? (await testURL(url)).data.response : "No URL"
This line will execute your promise and wait for it only if isURL return true, if so it will return the response part and if not, it will send out "No URL".
Full code :
async onChangeDestination(url, index) {
const url_status = isURL(url + '') ? (await testURL(url)).data.response : "No URL"
this.setState(prevState => {
const rules = [...prevState.rules];
rules[index] = {
...rules[index],
url,
url_status
};
return { rules };
});
};
I recommend to use more components with some particular single task only. In this case: you might need a stateless Input component with an onChange and a value prop. Its parent could be some container where the onChange triggers some async request and has a state like url_status. As #Nit commented set the state in the promise then clause. This url_status will be a prop of some other component, so in case of the prop changes that component will re-render automatically... In most of the cases you do not need to use states at all.
I see from a million other questions that there is a callback that can be used after setState is called. In fact, I use it in my code example here in my questionChangeSystemCallback function.
I'm unsure how to take advantage of this in my situation. Here is my code (stripped for simplicity sake)
The main flow works like this: A question changes, it calls it's callback, questionChangeSystemCallback. From there it updates it's value in state. When done updating it's value, it checks for additional things to do and calls actionExecuter as needed.
//note, the part that matters is effectively the forEach loop at the bottom that's within the setState questionsData callback.
questionChangeSystemCallback(Q) {
// updates current state of questionsData, checks for action string, executes any actions
if (Q == null) {
console.log("questionChangeSystemCallback required field, question, is null");
}
let updatedQuestionData = this.getQuestionDataWithUpdatedValue(Q);
this.setState({ questionsData: updatedQuestionData }, () => {
// after state is set, check if there are additional actions needed based on the actionOptions
if (Q.props.actions) {
let { actions } = Q.props;
let qval = Q.state.value;
let commandString = actions[qval];
if (commandString) {
let actionsToDo = commandString.split('&');
actionsToDo.forEach((action) => {
this.actionExecuter(action);
});
}
}
});
}
actionExecuter does this... basically just a switch statement to call showTab with a true or fall element:
actionExecuter = (actionString) => {
let splitActionString = actionString.split('::');
if (splitActionString.length !== 2) {
//TODO: Throw error
}
switch (splitActionString[0]) {
case "ShowTab":
this.showTab(splitActionString[1], true);
break;
case "HideTab":
this.showTab(splitActionString[1], false);
break;
default:
console.log("Requested action '" + splitActionString[0] + "' not recognized");
}
}
showTab looks like this, effectively adding tabName to this.state.hiddenTabs if toShow is true and removing tabName from this.state.hiddenTabs if it's false... and then setting the state.hiddenTabs to the new array.
showTab(tabName, toShow) {
let newHiddenTabs = this.state.hiddenTabs.slice();
console.log("After copy: ", newHiddenTabs);
let cleanTabName = tabName.replace(/ /g, '');
if (toShow) {
// remove all instances from newHiddenTabs
while (newHiddenTabs.includes(cleanTabName)) {
let index = newHiddenTabs.indexOf(cleanTabName);
if (index > -1) {
newHiddenTabs.splice(index, 1);
}
}
console.log("After removal: ", newHiddenTabs);
} else {
// add tabName to newHiddenTabs
newHiddenTabs.push(cleanTabName);
console.log("After addition: ", newHiddenTabs);
}
console.log("Before setting state: ", newHiddenTabs);
this.setState({ hiddenTabs: newHiddenTabs }, ()=> {
console.log("STATE after setting state: ", this.state.hiddenTabs);
}
);
}
Using that host of console logs, I'm learning 1) the logic here works and 2) that if I have more than one 'action', and thus showTab gets called twice... only the data from the SECOND call ends up in state. Further, the render method does not get called afterward.
As an example:
initial this.state.hiddenTabs = ["WaterQuality","FieldForm","EWI","EDI"]
I have added a console.log("RENDER") to the top of my render function.
I run, as actions, a ShowTab(EDI, true) and a ShowTab(EWI, false).
The following is the output:
After copy: (4) ["WaterQuality", "FieldForm", "EWI", "EDI"]
**(correct)**
After removal: (3) ["WaterQuality", "FieldForm", "EWI"]
**(correct)**
Before setting state: (3) ["WaterQuality", "FieldForm", "EWI"]
**(correct)**
After copy: (4) ["WaterQuality", "FieldForm", "EWI", "EDI"]
**(nope - same initial state as first time through)**
After addition: (5) ["WaterQuality", "FieldForm", "EWI", "EDI", "EWI"]
**(given it's input, correct, but overall wrong)**
Before setting state: (5) ["WaterQuality", "FieldForm", "EWI", "EDI", "EWI"]
**(given it's input, correct, but overall wrong)**
RENDER
**(why are we rendering now... and why only once)**
STATE after setting state: (5) ["WaterQuality", "FieldForm", "EWI", "EDI", "EWI"]
**(this is the (erroneous) value from the second time through)**
STATE after setting state: (5) ["WaterQuality", "FieldForm", "EWI", "EDI", "EWI"]
**(this is the (erroneous) value from the second time through)**
Your setState calls are getting batched. Depending on where you're calling setState, React will batch them automatically and only perform a render, once the whole batch is finished.
Problem in your case is probably here:
let newHiddenTabs = this.state.hiddenTabs.slice();
When you have multiple actions, this function gets called multiple times and react is batching setState. Since the updates where not flushed yet, when it performs this action again, the state isn't updated yet!
My suggestion: Extract this to another function and use the other setState signature, which takes a function with prevState and props as argument.
It'd look somewhat like this:
showTab(tabName, toShow) {
const processTabs = (hiddenTabs) => {
let cleanTabName = tabName.replace(/ /g, '');
if (toShow) {
hiddenTabs = hiddenTabs.filter((tab) => tab !== cleanTabName)
} else {
hiddenTabs.push(cleanTabName)
}
return hiddenTabs;
}
this.setState((prevState, props) => ({ hiddenTabs: processTabs([...prevState.hiddenTabs])}), () => {
console.log("STATE after setting state: ", this.state.hiddenTabs);
})
}
Edit: Sorry, accidentally sent the answer incomplete before D: