Home > database >  Why useContext doesn't re-render the component - React.js Laravel
Why useContext doesn't re-render the component - React.js Laravel

Time:02-21

I'm stucked... :)

I have a single view in Laravel where React appends to the specific ID. What I try to do is to open the pop-up after click the button using an useContext. Below is my code:

Globalcontext.js

import React from 'react';

export const initialState = {
    usersData: null,
    clickedNewUserButton: false,
    showAddUserPopup: false,
    isLoading: true,
};

export const GlobalContext = React.createContext(initialState);

UsersPageMain.js

import React, { useEffect, useContext } from 'react';
import ReactDOM from 'react-dom';
import { GlobalContext } from '../state/GlobalContext';
import FiltersButton from '../components/Users/FiltersButton';
import AddUserButton from '../components/Users/AddUserButton';
import UsersTable from '../components/Users/UsersTable';
import AddNewUserPopup from '../components/Users/AddNewUserPopup';

function UsersPageMain(){
    const initialState = useContext(GlobalContext);

    if(initialState.clickedNewUserButton){
        return (
            <GlobalContext.Provider value={initialState}>
                <div className='container users-list-page'>
                    <div className='row'>
                        <FiltersButton/>
                        <AddUserButton/>
                    </div>
                    <div className='row'>
                        <UsersTable></UsersTable>
                    </div>
                </div>
                <AddNewUserPopup/>
            </GlobalContext.Provider>
        )
    }else{
        return (
            <GlobalContext.Provider value={initialState}>
                <div className='container users-list-page'>
                    <div className='row'>
                        <FiltersButton/>
                        <AddUserButton/>
                    </div>
                    <div className='row'>
                        <UsersTable></UsersTable>
                    </div>
                </div>
            </GlobalContext.Provider>
        )
    }
}

export default UsersPageMain;

if (document.getElementById('user-list-page')) {
    ReactDOM.render(<UsersPageMain />, document.getElementById('user-list-page'));
}
UsersTable.js

import axios from 'axios';
import React, {useContext,useEffect,useState} from "react";
import { GlobalContext } from "../../state/GlobalContext";
import Preloader from '../Preloader';
import Conf from '../../conf/Conf';

export default function UsersTable(){
    const context = useContext(GlobalContext);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        axios.get('/api/get-all-users')
        .then(response => {
            context.usersData = response.data.data;
            setLoading(false);
        })
    })

    if(loading){
        return (
            <Preloader isLoading={loading}/>
        )
    }else{
        return (
            <>
                <Preloader isLoading={loading}/>
                <div className="col-12">
                    <div className="table-responsive rounded-table">
                        <table className="table">
                            <thead>
                                <tr>
                                    <th>ID</th>
                                    <th>Avatar</th>
                                    <th>Name</th>
                                    <th>Surname</th>
                                    <th>Group</th>
                                    <th>Email</th>
                                    <th>Actions</th>
                                </tr>
                            </thead>
                            <tbody>
                                {context.usersData.map((user,index) => {
                                    return (
                                        <tr key={user.email}>
                                            <th>{user.id}</th>
                                            <th>
                                                <div className="avatar" style={{backgroundImage: `url(${Conf().assetPath}uploads/avatars/${user.avatar})`}}></div>
                                            </th>
                                            <th>{user.name}</th>
                                            <th>{user.surname}</th>
                                            <th>Group</th>
                                            <th>{user.email}</th>
                                            <th>
                                                <button type="button" className="btn theme-edit-btn"><i className="fa-solid fa-pencil"></i></button>
                                                <button type="button" className="btn theme-delete-btn"><i className="fa-solid fa-delete-left"></i></button>
                                            </th>
                                        </tr>
                                    )
                                })}
                            </tbody>
                        </table>
                    </div>
                </div>
            </>
        )
    }
}
AddUserButton.js

import React, { useContext } from "react";
import { GlobalContext } from "../../state/GlobalContext";

export default function AddUserButton(){

    const context = useContext(GlobalContext);
    
    function triggerUserPopupClick(){
        context.clickedNewUserButton = true
    }
    
    return(
        <div className="col-xl-2 col-md-4 col-12">
            <button type="button" onClick={() => {triggerUserPopupClick()}} className="btn theme-primary-btn">Add New <i className="fa-solid fa-angles-right"></i></button>
        </div>
    )
}

The problem is exactly in the AddUserButton component where I'm trying to update global context to re-render main UsersPageMain component. I don't have any idea what I'm doing wrong... If you could, please give some tips what I need to do to achieve opening popup after clicking this component.

Here's the working snippet on the codesanbox, might it will be helpfull.

https://codesandbox.io/embed/adoring-bell-dv3tfc?fontsize=14&hidenavigation=1&theme=dark

CodePudding user response:

function triggerUserPopupClick(){
  context.clickedNewUserButton = true
}

React has only one1 way to cause a rerender: setting state. React can't tell that you mutated the context object here, so it will not rerender.

To make this work, you will need to set up your top level component to have a state variable. It will then make both the state and the setter function available via context.

// In globalcontext.js
export const initialState = {
    usersData: null,
    clickedNewUserButton: false,
    showAddUserPopup: false,
    isLoading: true,
};

export const GlobalContext = React.createContext({
  state: initialState,
  setState: () => {},
});

// In UsersPageMain.js
function UsersPageMain(){ 
  const [state, setState] = useState(initialState);
  
  // It's important to memoize this so you don't make a new object unless
  //     the state has changed. Otherwise, every render of UsersPageMain will
  //     necessarily trigger a render of every consumer of the context.
  const contextValue = useMemo(() => {
    return {
      state, 
      setState,
    };
  }, [state]);

  if(state.clickedNewUserButton){
    return (
      <GlobalContext.Provider value={contextValue}>
        // ...
      </GlobalContext.Provider>
    );
  } else {
    return (
      <GlobalContext.Provider value={contextValue}>
        // ...
      </GlobalContext.Provider>    
    )
  }
}

And then you'll call that setState function when you want to change it:

// In AddUserButtonjs
const { state, setState } = useContext(GlobalContext);
    
function triggerUserPopupClick(){
  setState({
    ...state,
    clickedNewUserButton: true
  });
}

Don't forget to update any other places where you're using the context so that they expect to get an object with a .state property, instead of getting the state directly.

1) Ok, technically there's a second way: force update in class components. But don't use that

CodePudding user response:

I figured out what was wrong. I wrote the async function and then into the useEffect I resolve the promise. The problem is that still I don't know why my axios was getting into the loop. Here's a working part of code :

import axios from 'axios';
import React, {useContext,useEffect,useState} from "react";
import { GlobalContext } from "../../state/GlobalContext";
import Preloader from '../Preloader';
import Conf from '../../conf/Conf';

async function fetchUsersData() {
    const response = await axios.get('/api/get-all-users');
    return response;
}

export default function UsersTable(){
    const { state, setState } = useContext(GlobalContext);
    useEffect( () => {
        fetchUsersData().then(response => {
            setState({
                ...state,
                usersData: response.data.data,
                isLoading: false,
            })
        })
    }, [null]);
    
    return (
        <></>
    )
}

CodePudding user response:

One another question. Does anyone know why my axios get in the loop? Problem is in the UsersTable component:

export default function UsersTable(){
    const { state, setState } = useContext(GlobalContext);

    useEffect(() => {
        axios.get('/api/get-all-users')
        .then(response => {
            setState({
                ...state,
                usersData: response.data.data,
                isLoading: false,
            })
        })
    })

    return (
        <></>
    )
}

The promise seems to be not resolved but gets into the loop, so my state is not updated and I'm getting a 429 error.

  • Related