skip to Main Content

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


  1. Chosen as BEST ANSWER

    Thanks to Max Lysenko for providing a fix. Using setNotes(notes => ([...notes, note])) instead of setNotes([...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 called notes is empty. I gather that react stores the current value, which will be used to update the state and this can be accessed by using useState with a paramter.

    If my understanding is wrong or incomplete I'd appreciate an update.


  2. The solution

    As @MaxLysenko said, setNotes((notes) => ([ ...notes, data ])) and not setNotes([...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 the didMount ref), you can mentally replace setNotes([...notes, data]) with setNotes([...[], data]). This is why the result is an array with one note. Using the updater syntax uses the last notes value.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search