SolidJS: How to trigger refetch of createResource? - javascript

I have a createResource which is working:
const fetchJokes = async (programmingOnly) => {
alert(programmingOnly);
return (await fetch(`https://official-joke-api.appspot.com/jokes/${programmingOnly?'programming/':''}ten`)).json();
}
//...
const [jokes, { mutate, refetch }] = createResource(programmingOnly(), fetchJokes);
Now I want to change the programmingOnly boolean via it's signal:
const [programmingOnly, setProgrammingOnly] = createSignal(true);
//...
Programming Only: <input type="checkbox" checked={programmingOnly()}
onInput={()=>{setProgrammingOnly(!programmingOnly());refetch();}}> </input>
But this does not work, the alert fires upon subsequent attempts, but with undefined as arg, and nothing happens with the data.
What is the SolidJS way of approaching this?

I believe the problem here is the signal is getting set with false every time. Since programmingOnly is a signal it should be accessed as a function to retrieve its value.
Ie..
setProgrammingOnly(!programmingOnly)
// should be
setProgrammingOnly(!programmingOnly())
You should not need to call refetch or do anything else for the example to work.

Related

React DOM not re-rendering after state change of array property

I do understand that this problem is very common and most people might find this as a duplicate but I am at my wits end and that is why I am here.
I have a React component called App is a functional component.
Start of App component
function App() {
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [name, setName] = useState('');
const [isNameSelected, setIsNameSelected] = useState(false);
...
There is a child component which is acting erratically at the moment and it is part of the return components of the App function.
Section of Code in return statement of the App component:
<ListGroup className="typeahead-list-group">
{!isNameSelected &&
results.length > 0 &&
results.map((result: Result) => (
<ListGroupItem
key={result.cik}
className="typeahead-list-group-item"
onClick={() => onNameSelected(result)}
>
{result.cik + ' | ' + result.name}
</ListGroupItem>
))}
</ListGroup>
A change to to results is handled by the component FormControl's onChange function here also part of the return statement of App:
<FormControl
placeholder="Entity/CIK"
id="searchInput"
type="text"
autoComplete="off"
onChange={handleInputChange}
value={name}
/>
handleInputChange is defined in App function as:
const handleInputChange = (e: any) => { // Triggers when search bar is changed
// remove error bubble
setAlertMessage(new AlertData('', false));
const nameValue = e.target.value; // get the new input
setName(nameValue); // set the new input
// even if we've selected already an item from the list, we should reset it since it's been changed
setIsNameSelected(false);
setResults([]); // clean previous results
if (nameValue.length > 1) { // if the input is more than 1 character
setIsLoading(true); // set loading to true
updateSearchInput(nameValue) // get the results
.then((res) => {
setResults(res as React.SetStateAction<never[]>); // set the results
setIsLoading(false); // set loading to false
})
.catch((error) => {
// error bubble
let strError = error.message;
strError = strError.split(':').pop();
let errorMessage: AlertData = new AlertData(strError, true); // create error message for empty search
setAlertMessage(errorMessage); // set alert message
// loading spinner
setIsLoading(false);
});
}
}
However when there is an input change in form control, like typing in an entire word, the search functionality works, populating the DOM with suggested words. However when I clear the value in FormControl really fast (by pressing Backspace/Delete several times in quick succession), then the search results stay. Doing it slow or selecting and clearing it all at once however does not show this erratic behavior.
I have used console.log to print out the value of results in the an empty component like this:
{console.log(results) && (<div><div/>)}
in return statement of App to see what the contents of results are. However it does show that results value were not updated by setResults().
This problem however does not exist for the other states utilized here. Why?
EDIT
From the answer accepted below from #ghybs. This is a timeline of what might be happening with the call:
Enter search
await call runs but request response is slow so takes a while.
Delete all the keyword in search
results is made empty with setResults([]) in handleInputChange call.
await call finishes. setResults(res as React.SetStateAction<never[]>) runs making results non-empty.
You very probably just have plenty concurrent requests (1 per key stroke, including back space?), and unordered results from your updateSearchInput async function: the last received one overwrites your results, but that one may not originate from your last key stroke (the one that removed the last character from your textarea).
Typically if an empty search is faster than a search with plenty words, the results from empty input do clear your results, but then these are filled again by the results of a previous search.

React state disappearing

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.

Function A depends on previous function to update the state, but Function A still tries to render before the update

An Example I have linked below, that shows the problem I have.
My Problem
I have these two functions
const updatedDoc = checkForHeadings(stoneCtx, documentCtx); // returns object
documentCtx.setUserDocument(updatedDoc); // uses object to update state
and
convertUserDocument(stoneCtx, documentCtx.userDocument);
// uses State for further usage
The Problem I have is, that convertUserDocument runs with an empty state and throws an error and then runs again with the updated state. Since it already throws an error, I cannot continue to work with it.
I have tried several different approaches.
What I tried
In the beginning my code looked like this
checkForHeadings(stoneCtx, documentCtx);
// updated the state witch each new key:value inside the function
convertUserDocument(stoneCtx, documentCtx.userDocument);
// then this function was run; Error
Then I tried the version I had above, to first put everything into an object and update the state only once.
HavingconvertUserDocument be a callback inside of checkForHeadings, but that ran it that many times a matching key was found.
My current try was to put the both functions in seperate useEffects, one for inital render and one for the next render.
const isFirstRender = useRef(true);
let init = 0;
useEffect(() => {
init++;
console.log('Initial Render Number ' + init);
console.log(documentCtx);
const updatedDoc = checkForHeadings(stoneCtx.stoneContext, documentCtx);
documentCtx.setUserDocument(updatedDoc);
console.log(updatedDoc);
console.log(documentCtx);
isFirstRender.current = false; // toggle flag after first render/mounting
console.log('Initial End Render Number ' + init);
}, []);
let update = 0;
useEffect(() => {
update++;
console.log('Update Render Number ' + update);
if (!isFirstRender.current) {
console.log('First Render has happened.');
convertUserDocument(stoneCtx.stoneContext, documentCtx.userDocument);
}
console.log('Update End Render Number ' + update);
}, [documentCtx]);
The interesting part with this was to see the difference between Codesandbox and my local development.
On Codesandbox Intial Render was called twice, but each time the counter didn't go up, it stayed at 1. On the other hand, on my local dev server, Initial Render was called only once.
On both version the second useEffect was called twice, but here also the counter didn't go up to 2, and stayed at 1.
Codesandbox:
Local Dev Server:
Short example of that:
let counter = 0;
useEffect(()=> {
counter++;
// this should only run once, but it does twice in the sandbox.
// but the counter is not going up to 2, but stays at 1
},[])
The same happens with the second useEffect, but on the second I get different results, but the counter stays at 1.
I was told this is due to a Stale Cloruse, but doesn't explain why the important bits don't work properly.
I got inspiration from here, to skip the initial render: https://stackoverflow.com/a/61612292/14103981
Code
Here is the Sandbox with the Problem displayed: https://codesandbox.io/s/nameless-wood-34ni5?file=/src/TextEditor.js
I have also create it on Stackblitz: https://react-v6wzqv.stackblitz.io
The error happens in this function:
function orderDocument(structure, doc, ordered) {
structure.forEach((el) => {
console.log(el.id);
console.log(doc);
// ordered.push(doc[el.id].headingHtml);
// if (el.children?.length) {
// orderDocument(el.children, doc, ordered);
// }
});
return ordered;
}
The commented out code throws the error. I am console.loggin el.id and doc, and in the console you can see, that doc is empty and thus cannot find doc[el.id].
Someone gave me this simple example to my problem, which sums it up pretty good.
useEffect(() => {
documentCtx.setUserDocument('ANYTHING');
console.log(documentCtx.userDocument);
});
The Console:
{}
ANYTHING
You can view it here: https://stackblitz.com/edit/react-f1hwky?file=src%2FTextEditor.js
I have come to a solution to my problem.
const isFirstRender = useRef(true);
useEffect(() => {
const updatedDoc = checkForHeadings(stoneCtx.stoneContext, documentCtx);
documentCtx.setUserDocument(updatedDoc);
}, []);
useEffect(() => {
if (!isFirstRender.current) {
convertUserDocument(stoneCtx.stoneContext, documentCtx.userDocument);
} else {
isFirstRender.current = false;
}
}, [documentCtx]);
Moving isFirstRender.current = false; to an else statement actually gives me the proper results I want.
Is this the best way of achieving it, or are there better ways?

VueJS and Firestore: Updating a document property happens twice

I got a click handler that, when clicked, for some reason is updating my value twice (incrementing or decrementing twice). It doesn't happen all the time, but I would say 90% of the time. I want to prevent that and only update the value once.
Let me explain.
First, here's my button template code:
<b-button
variant="link"
class="mb-2"
#click="clueHandler(comment)"
>
Here is the script code for the clueHandler:
async clueHandler(comment) {
const inClues =
this.clues.findIndex((clue) => clue.id === comment.id) > -1
if (this.loggedIn) {
if (inClues) {
await this.$store.dispatch('removeCommentFromCluesFeed', comment)
console.log('dispatched from clueHandler')
} else if (!inClues) {
await this.$store.dispatch('addCommentToCluesFeed', comment)
console.log('dispatched from clueHandler')
}
} else {
this.$router.replace('/login')
}
},
Here's the logic in Vuex for updating the Firestore document property:
async addCommentToCluesFeed({ state }, comment) {
try {
const cluesFeedDoc = this.$fireStore
.collection(`users/${state.userProfile.uid}/clues`)
.doc(comment.id)
await cluesFeedDoc.set(comment)
console.log('clue added from addCommentToCluesFeed action')
await this.$fireStore
.collection('comments')
.doc(comment.id)
.update({
clueVotes: parseInt(comment.clueVotes) + 1 // <<<<------------HERE !!
})
console.log('clue vote increased from addCommentToCluesFeed action')
} catch (error) {
console.error(
'error adding clue from addCommentToCluesFeed action',
error
)
}
},
Here's a screenshot of the area the handler is updating:
What happens is that when you click ONCE on the template button with the clueHandler, the document property clueVotes is incremented twice (or decremented twice depending on whether the clue was already set by the user previously).
Here is an example of what the database shows when a user clicks the button for the first time:
It should only be clueVotes: 1.
Anyone have any advice or thoughts on what is happening or how I can fix? I am assuming this is going to be related to not fully realizing the promise/race completion, etc. Thanks!
I found an excellent explanation of why I was having this issue. Here it is:
https://fireship.io/snippets/firestore-increment-tips/
Basically, I needed to return a special value that can be used with set() or update() that tells the server to increment the field's current value by the given value.
Here's my code update:
.update({
clueVotes: this.$fireStoreObj.FieldValue.increment(1)
})

Why can't my function read the actual value of my component's state?

For a school project my group is building a table that's filled with city-data via a database-call.
The skeleton of the table-component is as such:
import React, { useState } from 'react'
function Table(props) {
const [ cities, setCities ] = useState([])
const [ pageNum, setPageNum ] = useState(0)
useEffect(()=> { // this sets up the listener for the infinite scroll
document.querySelector(".TableComponent").onscroll = () => {
if (Math.ceil(table.scrollHeight - table.scrollTop) === table.clientHeight) showMoreRows()
}
//initial fetch
fetchData(0)
},[])
async function showMoreRows() {
console.log("Show more rows!")
await fetchData(pageNum)
}
async function fetchData(page) {
// some code, describing fetching
// EDIT2 start
console.log(pageNum)
// EDIT2 end
const jsonResponse = await {}// THE RESPONSE FROM THE FETCH
if(page) {
setCities([...cities, ...jsonResponse])
console.log("page is true", page)
setPageNum(pageNum + 1)
} else {
console.log("page is false", page) // this always runs and prints "page is false 0"
setCities([...cities, ...jsonResponse])
setPageNum(1)
}
}
return <div className="TableComponent"> { pageNum }
<!-- The rest of the component -->
</div>
}
The table features an "infinite-scrolling"-feature, so when you scroll to the bottom of the page it prints "Show more rows!" and runs fetchData(pageNum) to get more data. At this point, after the initial fetch, the pageNum-variable should be 1 or more, but for some reason the function acts as if it is 0. I put the pageNum-variable on display in the JSX, and I can see that it is 1, but it still prints out "page is false 0" when ever I run it.
When I try to google the issue, it seems the only similar thing could be that I try to read a useState-variable too soon after using setPageNum (before the redraw), but that isn't the case here as far as I can see. I give it plenty of time between tries, and it always says pageNum is zero.
Any ideas as to what I am doing wrong, and how this makes sense in any way?
Thanks for any help!
EDIT: Just tried the code I wrote over, and it seemed to work - however the full code I have doesn't work. Anyone have any ideas about problems related to this, even if the above code might work?
EDIT2: I added a console.log(pageNum) to the fetchData-function, and tested a bit, and it seems that whatever I put into the initial value in useState(VALUE) is what is being printed. That makes NO sense to me.
Help.
EDIT3: Added await, already had it in real code
EDIT4: I've tried at this for a while, but realized as I am using react that I could move the scroll-listener I have down to the JSX-area, and then it worked - for some reason. It now works. Can't really mark any answers as the correnct ones, but the problem is somewhat solved.
Thanks all who tried to help, really appreciate it.
Your staleness issues are occurring because React is not aware of your dependencies on the component state.
For example, useEffect ensures that value of showMoreRows from the scope of the initial render will be called on every scroll. This copy of showMoreRows refers to the initial value of pageNum, but that value is "frozen" in a closure along with the function and won't change when the component state does. Hence the scroll listening won't work as it needs to know the current state of pageNum.
You can resolve the issues by using callbacks to "hookify" showMoreRows and fetchData and declare their dependence on the component state. You must then declare the dependence of useEffect on these callbacks and use a clean-up function to handle the effect being invoked more than once.
It would look something like this (I haven't tried running the code):
import React, { useState } from 'react'
function Table(props) {
const [ cities, setCities ] = useState([])
const [ pageNum, setPageNum ] = useState(0)
useEffect(()=> {
// Run this only once by not declaring any dependencies
fetchData(0)
}, [])
useEffect(()=> {
// This will run whenever showMoreRows changes.
const onScroll = () => {
if (Math.ceil(table.scrollHeight - table.scrollTop) === table.clientHeight) showMoreRows()
};
document.querySelector(".TableComponent").onscroll = onScroll;
// Clean-up
return () => document.querySelector(".TableComponent").onscroll = null;
}, [showMoreRows])
const showMoreRows = React.useCallback(async function () {
console.log("Show more rows!")
await fetchData(pageNum)
}, [fetchData, pageNum]);
const fetchData = React.useCallback(async function (page) {
// some code, describing fetching
// EDIT2 start
console.log(pageNum)
// EDIT2 end
const jsonResponse = await {}// THE RESPONSE FROM THE FETCH
if(page) {
setCities([...cities, ...jsonResponse])
console.log("page is true", page)
setPageNum(pageNum + 1)
} else {
console.log("page is false", page) // this always runs and prints "page is false 0"
setCities([...cities, ...jsonResponse])
setPageNum(1)
}
}, [setCities, cities, setPageNum, pageNum]);
return <div className="TableComponent"> { pageNum }
<!-- The rest of the component -->
</div>
}
This might not totally solve the problem (it's hard to tell without more context), but useEffect runs every render, so things like that 'initial' fetchData(0) are going to run every update, which would probably give you the result from page = 0 every time in that conditional in fetchData.
It's hard to say without more context, but I have one guess.
Try using
setCities(value => [...value, ...jsonResponse])
instead of
setCities([...cities, ...jsonResponse])
Also make sure you use await for resolving promises for requests like:
const jsonResponse = await ...
You can console log it to check if they are not pending and that you get the right property if it's a nested object.

Categories

Resources