I’m playing around with a note taking web app and I’m having problems rendering the notes that are sent from the backend. To practice writing asynchronous code I implemented the app to ask the backend for a list of ids, one for each note file (). After the list is filled the idea is to make an API call for every element in the list, so notes can be rendered once they have loaded.
This is the main component
export const Notes = () => {
const [notes, setNotes] = useState<Note[]>([]);
const [pageIsLoading, setPageIsLoading] = useState(true);
const noteIdsResponse = useNoteIds();
useEffect(() => {
async function fetchNotes(noteIds: NoteIds) {
try {
noteIds.forEach(async id => {
return fetch("http://localhost:5086/api/notes/get/" + id)
.then(reponse => reponse.json())
.then(data => Note.parse(data))
.then(note =>{
setNotes([...notes, note])
})
})
} catch (error) {
console.error(error);
}
}
if(noteIdsResponse.data) {
fetchNotes(noteIdsResponse.data)
}
}, [noteIdsResponse.data])
useEffect(() => {
if(noteIdsResponse.loading) {
setPageIsLoading(true);
} else {
setPageIsLoading(false);
}
}, [noteIdsResponse.loading])
return(
<>
<h2>Some kind of note taking system</h2>
<Group Sx={{gap: "10px", marginBottom: "16px"}}>
<Button>
<Group gap={5}>
Add
<IconPlus/>
</Group>
</Button>
<Button>Save All</Button>
{pageIsLoading &&
<div className="lds-ring"><div></div><div></div><div></div><div></div></div>
}
</Group>
{noteIdsResponse.error &&
<Group gap={20}>
There was an error fetching the notes
<Button onClick={() => noteIdsResponse.refetch()}>
Retry
</Button>
</Group>}
<Group gap={20}>
{}
{notes.map((note, i) => <NoteArea key={i} note={note}/>)}
</Group>
</>
)
}
I replicated the issue in a smaller component without dependencies
export const NotesTest = () => {
const [notes, setNotes] = useState<Note[]>([]);
const didMount = useRef(false);
const noteIds = [1, 2, 3]
useEffect(() => {
async function fetchNotes(noteIds: number[]) {
try {
noteIds.forEach(async id => {
return fetch("http://localhost:5086/api/notes/get/" + id)
.then(reponse => reponse.json())
.then(data =>{
setNotes([...notes, data])
console.log(data)
})
})
} catch (error) {
console.error(error);
}
}
if(!didMount.current) {
fetchNotes(noteIds)
didMount.current = true;
}
}, [])
return(
<>
{notes.map((note, i) => {
return(
<div key={i}>
<h1>{note.title}</h1>
<p>{note.content}</p>
</div>
)
})}
</>
)
}
And the note type
type Note = {
title: string,
content: string,
dateTime: Date,
id: number,
}
All three promises return a valid result. I confirmed this with the print to the console right after the setNotes call. However, only the first result is rendered.
I managed to render all notes using Promise.all() inside of the try block, but as the notes are only rendered when all promises have resolved it kind of defeats the purpose of what im trying to achieve.
Promise.all(noteIds.map(async id => {
return fetch("http://localhost:5086/api/notes/get/" + id)
.then(reponse => reponse.json())
.then(data => Note.parse(data))
})).then((newNotes: Note[])=> setNotes([...notes, ...newNotes]))
2
Answers
Thanks to Max Lysenko for providing a fix. Using
setNotes(notes => ([...notes, note]))
instead ofsetNotes([...notes, note])
.My explanation of the issue is that react does not update the state until the end of the effect, so everytime
setNotes()
is callednotes
is empty. I gather that react stores the current value, which will be used to update the state and this can be accessed by usinguseState
with a paramter.If my understanding is wrong or incomplete I'd appreciate an update.
The solution
As @MaxLysenko said,
setNotes((notes) => ([ ...notes, data ]))
and notsetNotes([...notes, data])
.Why does it work
The initial value of
notes
is []. Since state variable’s value never changes within a render, and since you only run the useEffect once (because of thedidMount
ref), you can mentally replacesetNotes([...notes, data])
withsetNotes([...[], data])
. This is why the result is an array with one note. Using the updater syntax uses the lastnotes
value.