1

I'm starting to learn React, more specifically ReactJS, and now I've got my NodeJS backend, where I'm storing an array of projects, which contains: id(uuid), url(string), techs(array of strings), title(string) and likes(number).

My component is rendering a div for each of the projects that my backend sends to the frontend, and each div will contain all the attributes of the project, and also a button that will trigger a route on my backend to increment the number of likes. It was supposed to update that number at the same time of the button click, and I'd like to do that without needing to request all the list of projects to the API again.

Well, my component is like this:

import React, { useContext, useEffect, useState } from 'react';
import { ThemeContext } from '../../providers/ThemeContext';
import { Button } from '@material-ui/core';
import axios from '../../services/axios';

function ListProjects() {
    const [projects, setProjects] = useState<any[]>([]);
    const { theme } = useContext(ThemeContext);

    const updateProjects = async () => {
        const res = await axios.get('list');
        res.data.status === 1 && setProjects(res.data.projects);
    };

    useEffect(() => {
        updateProjects();
    }, []);

    const onLikeProject = async (id: string) => {
        const newObj = (await axios.post(`${id}/like`)).data.updatedProject;
        const objIndex = projects.findIndex(i => i.id === id);
    
        let projectsCopy = projects;
        /* console.log(projects[objIndex]); */
        projectsCopy[objIndex] = newObj;
        /* console.log(projectsCopy[objIndex]); */
        setProjects(projectsCopy);
        /* console.log(projects[objIndex]); */
    };

    return (
        <div style={{ 
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            minHeight: 'calc(100vh - 64px)', 
            minWidth: '100vw', 
            backgroundColor: theme.bg, 
            color: theme.color
        }}>
            { projects && projects.map((p, i) => <div key={p.id} style={{
                display: 'flex',
                flexDirection: 'column',
                width: '550px',
                height: '300px',
                border: '1px solid blue',
                marginBottom: '10px'
            }}>
                <h3>Title: { p.title }</h3>
                <h3>URL: { p.url }</h3>
                <h3>Techs: { p.techs.map((t: any, i: any) =>  i+1 < p.techs.length ? `${t}, ` : t) }</h3>
                <h3>Likes: { p.likes }</h3>
                <Button color="primary" variant="contained" onClick={() => onLikeProject(p.id) }>Like</Button>
            </div>) }
        </div>
    );
}

export default ListProjects;

As you can see, when the page loads, the projects state is updated with the API list response. I have already tried using the same updateProjects() function to update the list when the user click the Like button, but this triggers another api call, and that will delay a little, and making an api list request on every single like request, would break a possible limit of requests the service has available. So I tried the code inside the onLikeProject(), but it doesn't update the UI, even if the projects array is being updated.

How should I proceed? Thanks

João Casarin
  • 674
  • 2
  • 9
  • 27

1 Answers1

2

Issue

State mutation. projectsCopy is a reference to the state object projects, it is mutated and saved back into state, so the state array reference is never updated and React bails on any rerendering.

const onLikeProject = async (id: string) => {
    const newObj = (await axios.post(`${id}/like`)).data.updatedProject;
    const objIndex = projects.findIndex(i => i.id === id);

    let projectsCopy = projects;
    projectsCopy[objIndex] = newObj; // <-- state mutation
    setProjects(projectsCopy);
};

Solution

Shallow copy the state object into a new array reference.

const onLikeProject = async (id: string) => {
  const newObj = (await axios.post(`${id}/like`)).data.updatedProject;
  const objIndex = projects.findIndex(i => i.id === id);

  let projectsCopy = [...projects]; // <-- shallow copy state array
  projectsCopy[objIndex] = newObj; // <-- then update index
  setProjects(projectsCopy);
};

A more canonical method is to map the previous state to the next state.

const onLikeProject = async (id: string) => {
  const newObj = (await axios.post(`${id}/like`)).data.updatedProject;
    
  setProjects(projects => projects.map((project) => project.id === id ? newObj : project));
};
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Its also worth mentioning, that `projectsCopy` is a reference to `projects`, in the original approach. – Keimeno Mar 30 '21 at 18:10
  • Wow, amazing explanation, thank you both! I was considering using the map function, but did want to implement it another way... Also, I was studying the spread operator yesterday and completely forgot about it in this case, but didn't know that using '=' wouldn't work, because the last 'console.log', which logs the projects array, it is updated, but never re-rendered. Anyways, thanks guys! – João Casarin Mar 30 '21 at 18:46
  • Sorry using this same post, but for deleting the object with that id inside the array, should i use something like `setProjects(projects.filter(p => p.id !== id));`? I tried it but the list doesn't update on the UI again... I tried using the filter with the [...projects] like this: `setProjects([...projects].filter(p => p.id !== id));`, but didn't work too. – João Casarin Mar 30 '21 at 20:16
  • If I need to create a new post just regarding this issue, I'll promptly do it. – João Casarin Mar 30 '21 at 20:16
  • @JoãoCasarin That is also a standard way to remove an element from an array, though I always suggest using a functional update, i.e. `setProjects(projects => projects.filter(p => p.id !== id))`. I don't see any immediate reason why this wouldn't work, but sure, let's see what you are actually doing. Can you create a new SO post for this? Feel free to (at) me here with a link to it and I can take a look when I can. – Drew Reese Mar 30 '21 at 20:26
  • 1
    @DrewReese I really appreciate your help man, but it seems I found the problem.. It was my backend, where I was sending the status 204, and this was blocking the content to be sent to frontend... And on frontend I was checking if some data was '1', but it was actually undefined... Thanks man! – João Casarin Mar 30 '21 at 21:08
  • Hello again @DrewReese how u doing? Well, I've got another issue on my react-learning process, and as you told me I could tag you there, I was trying to do it, but no one was tagged hehe, here I am asking if you could go check the question! This is the link: [question](https://stackoverflow.com/questions/67010253/react-modal-updater-doesnt-refresh-page-after-setstate?noredirect=1#comment118449004_67010253) Anyways, thanks! – João Casarin Apr 08 '21 at 23:03