Search code examples
reactjsreact-hooksstaterenderingrender

Component doesnt rerender on state change


My problem is that my component doesnt rerender, when my state changes. I am managing my state in a custom Hook and after an put request to my backend my state gets updated. This works completely fine, but the content of my page doesnt get refreshed when changing my sate after the put request.

Component:

import React, { useEffect, useState } from 'react';
import { CONTROLLERS, useBackend } from '../../hooks/useBackend';
import Loading from '../Alerts/loading';
import {Table} from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import DropdownForm from '../Forms/dropdown';

function AdminPanel() {

    const headers = ['ID', 'Title', 'Release Date', 'Producer', 'Director', 'Status', 'UTC Time', '#', '#'];

    const [error, setError] = useState(false);
    const [loaded, setLoaded] = useState(false);
    const [requests, backend] = useBackend(error, setError);

    useEffect(() => {
        backend(CONTROLLERS.REQUESTS.getRequestsAdmin());
    }, [])


    useEffect(() => {
        setLoaded(requests !== undefined);
        console.log(requests);
    }, [requests])

    const handleUpdate = (e, result) => {
        backend(CONTROLLERS.REQUESTS.put({requestStatus: result, accessToken: localStorage.accessToken}, e));
    }




    if(!loaded) return <Loading/>
    if(error) return <p>No Access</p>
    return(
        <>
        <DropdownForm  items={['A-Z', 'Z-A', 'None']} title={'Filter'} first={2} setHandler={setFilter}/>
        <DropdownForm items={['+20', '+50', 'All']} title={'Count'} first={0} setHandler={setCount}/>
        {/* <DropdownForm/> */}
        <Table bordered  hover responsive="md">
            <thead>
                <tr>
                  {headers.map((item, index) => {
                     return( <th className="text-center" key={index}>{item}</th> );
                  })}
                </tr>
            </thead>
            <tbody>
                  {requests.map((item, index) =>{
                      return(
                      <tr>
                          <td>{index + 1}</td>
                          <td>{item.movie.movieTitle}</td>
                          <td>{item.movie.movieReleaseDate}</td>
                          <td>{item.movie.movieProducer}</td>
                          <td>{item.movie.movieDirector}</td>
                          <td>{(item.requestStatus === 1 ? 'Success' : item.requestStatus ===2 ? 'Pending' : 'Denied')}</td>
                          <td className="col-md-3">{item.requestDate}</td>
                          {/* <td><span  onClick={() => handleDelete(item.requestID)}><i  className="fas fa-times"></i></span></td> */}
                          <td><span  onClick={() => handleUpdate(item.requestID, 3)}><i  className="fas fa-times"></i></span></td>
                          <td><span  onClick={() => handleUpdate(item.requestID, 1)}><i  className="fas fa-check"></i></span></td>
                      </tr>);
                  })}
            </tbody>
        </Table>
        </>
      );
  }
// }

export default AdminPanel;

customHook:

import axios from "axios";
import { useEffect, useRef, useState } from "react";
import notify from "../Components/Alerts/toasts";

const BASE_URL = 'https://localhost:44372/api/';
const R = 'Requests/'; const M = 'Movies/'; const U = 'Users/';


const buildParams = (url, type, header, param) => {
    return {url: url, type: type, header: header, param: param};
}



export const CONTROLLERS = {
    REQUESTS: {     
        getRequestsAdmin: () => buildParams(`${R}GetRequestsAdmin`, 'post', true, {accessToken: 
}


export const  useBackend = (error, setError) => {
    const [values, setValues] = useState([]);


     async function selectFunction(objc) {
        switch(objc.type) {
            case 'put':  return buildPutAndFetch(objc.url, objc.param, objc.header);break;   
            default: console.log("Error in Switch");
        }
    }
     
     async function buildPutAndFetch(url, param, header) {
        const finalurl = `${BASE_URL}${url}`;
            return axios.put(finalurl, param, {headers: {
                'Authorization': `Bearer ${(localStorage.accessToken)}`
            }})
            .then(res => {
                if(res.data && 'accessToken' in res.data) localStorage.accessToken = res.data.accessToken;
                else {
//When an object gets updated, the backend returns the updated object and replaces the old one with the //new one. 
                const arr = values;
                const found = values.findIndex(e => e[(Object.keys(res.data))[0]] == res.data.requestID);
                arr[found] = res.data;
                setValues(arr);
                }
                setError(false);
                return true;
            })
            .catch(err => {
                setError(true);
                return false;
            })
        }
    }
   

    function response(res) {
        setValues(res.data)
        setError(false);
    }

    
    return [values, 
            async (objc) =>  selectFunction(objc)];
}


Solution

  • It's likely due to the fact that your buildPutAndFetch function is mutating the values array in state, rather than creating a new reference. React will bail out on state updates if the reference doesn't change.

    When you declare your arr variable, it's setting arr equal to the same reference as values, rather than creating a new instance. You can use the spread operator to create a copy: const arr = [...values].

    It's also worth noting that because this is happening asynchronously, you may want to use the function updater form of setValues to ensure you have the most current set of values when performing the update.

    setValues(prev => {
        const arr = [...prev];
        const found = prev.findIndex((e) => e[Object.keys(res.data)[0]] == res.data.requestID);
        arr[found] = res.data;
        return arr;
    });