0

I have a modify-button that displays a modal component according to the value of the state property "modalVisible", in other terms the modify-button trigger an event handler which update the state property but since setState is asynchronous, the modal doesn't display properly (sometimes it shows sometimes not)

Is there an alternative to show the modal immediately after clicking modify button in react ?

here is my code i have 4 Components :

The first is Todo component which is used to display the 2 other components: DisplayTasks component (to do list) and the DisplayCompletedTasks (tasks completed),

The fourth is Modal component (just an input text to change the name of the task) which is displayed when clicking on the button modify-button

First component file TaskManagements.js

import React from 'react'
import {DisplayTasks} from './DisplayTasks' 
import DisplayCompletedTasks from './DisplayCompletedTasks'

export class Todo extends React.Component{
  constructor(props){
    super(props);
    
    this.state={
        task: '',
        tasks: [],
        completedTasks: [],
        inputVisible: false,
        completedVisible: false,
        modalVisible: false,
        buttonClicked: '' 
    };
  }
  toggleInputDisplay = ()=>{
    this.setState(state=>({inputVisible: !state.inputVisible}));
  }
  handleChange = (event)=>{
    // Handle input checkboxes that handle completed tasks (the alternative is to use event.target.type==="checkboxes")
    if(event.target.name==="choosed"){
      //document.querySelector("input[value='" + event.target.value + "']").style.background-color = ""
      let arr = this.state.tasks;
      arr[event.target.value].checked = !arr[event.target.value].checked;
      this.setState({tasks: arr});
    } 
    // Handle the text input storing the text within the task state property 
    else if(event.target.type==="text"){
      this.setState({task: event.target.value});
    }
  }
  addTask = (event)=>{
    const arr = this.state.tasks;
    arr.push({task: this.state.task, checked: false});
    this.setState(state=>({tasks: arr, task: ''}));
  }
  removeTask = (event)=>{
    const arr = this.state.tasks;
    arr.splice(event.target.id,1);
    this.setState(state=>({tasks: arr}));
  }
  modifyTask = (event)=>{
    this.setState({modalVisible: true});
  }
  handleParam = (event)=>{
    const name = event.target.name;
    let arr = this.state.tasks;
    arr.forEach(task=>(task.checked = false));
    this.setState(state=>({completedVisible: !state.completedVisible, buttonClicked: name, 
      tasks: arr}));
    console.log(this.state.tasks);
  }
  handleChoosedTasks = (event)=>{
    //const inputVar = document.querySelectorAll("[value][name='completed']:checked");
    this.setState(state=>({tasks: state.tasks.filter(task=>(!task.checked)), completedVisible: false}));
    if(this.state.buttonClicked === 'complete'){
      const completedTasks = this.state.tasks.filter(task=>(task.checked));
      this.setState(state=>({completedTasks: state.completedTasks.concat(completedTasks)}));
      console.log('completed:' + this.state.completedTasks);
    }
  }
  render(){
      console.log(this.state.tasks);
      return(
        <div className="h-100">
            <button className="mt-4 btn btn-outline-primary" onClick={this.toggleInputDisplay}>Add Task</button>
            <div className="mt-5 ">{!this.state.inputVisible ? '' : (
              <div className="mb-4">
                <input className="mr-1"type="text" value={this.state.task} onChange={this.handleChange}/>
                <button className="btn btn-outline-secondary mr-1" onClick={this.addTask}>Add</button>
                <button className="btn btn-outline-secondary" onClick={this.toggleInputDisplay}>Cancel</button>
              </div>
                )      
              }
              <div className="row p-0 col-6 mx-auto ">
                <span style={{"paddingLeft": "14%"}} className=" mb-0 ml-0 col-10 ">Tasks</span>
              <button id="complete" name="complete" style={{"fontSize": "14px"}} className="btn btn-success col p-0" onClick={this.handleParam}>
                complete</button>
              <button id="remove" name="remove" style={{"fontSize": "14px","marginLeft":"5px"}} className="btn btn-danger col p-0" onClick={this.handleParam}>
                remove</button>
              </div>
              <DisplayTasks tasks={this.state.tasks} removeTask={this.removeTask} 
              completedVisible={this.state.completedVisible} handleChange={this.handleChange} 
              handleChoosedTasks={this.handleChoosedTasks} modifyTask={this.modifyTask} modalVisible={this.state.modalVisible}/>
              <span className=" mb-0 ">Completed</span>
              <DisplayCompletedTasks completedTasks={this.state.completedTasks}/>
            </div>
            
        </div>
      );
  }
}

second file DisplayTasks.js

import React from 'react'
import ModifiedModal from './ModifiedModal.js'
import './DisplayTasks.css'

export class DisplayTasks extends React.Component{
  
  render(){
    return(
      <div id="tasks" style={{"height": "40vh"}} className="mt-2 mb-5 col-6 border border-primary mx-auto ">
        {!this.props.modalVisible? '' : <ModifiedModal />}
      <div style={{"height": "87%"}} className=" col-12 overflow-auto">{!this.props.tasks.length ? '' : this.props.tasks.map((task,index)=>
      (
        <div key={`task-${index}`} className="mt-3 mr-0 d-flex p-0 w-100 border">
          <div id="parent-task" style={!task.checked ? {...this.style}: {...this.style,"backgroundColor":"yellow"}} className="col-12  ml-0 p-0 d-flex">{!this.props.completedVisible ? '' 
          : (<label id="c-customized" className="border">
             <input name="choosed" type="checkbox" value={index}
             onChange={this.props.handleChange}/>
             <svg width="30" height="30">
               <g>
                 <circle className="c-b" cx="15" cy="15" r="14" stroke="magenta"/>
                 <polyline className="c-m" points="6,14 12,20 23,9"/>
               </g>
             </svg>
             </label>)}
          <strong className="ml-0 col-11 d-inline-block align-self-center">
            {task.task}
          </strong>
           
           <button id="modify-button" className="btn btn-primary btn-circle mr-1"><i value={index} className="fas fa-pencil-alt"
             onClick={this.props.modifyTask}></i></button>
          </div>
        </div>
        )
       )
      }
      </div>
      {!this.props.completedVisible ? '' 
      : <button id="choosed-confirmed" className="d-flex btn btn-success" onClick={this.props.handleChoosedTasks}>
        <span className="mx-auto align-self-center">Ok</span></button>}
      </div>
    )
  }
}

The fourth is the Modal

import React from 'react'

export default function ModifiedModal(props){
  console.log("modifiedModal");
  return <div className="Modal d-flex ">
    <label>
      <button id="x-button"></button>
      <span>Modify the Task</span>
      <input type="text" />
    </label>
  </div>
}

Edit: how the react developers displays immediatly the modal just after clicking ? because it's not intuitive to click on a button and wait until the state update, i can't detect what is the pros of using react over vanilla js if it is not make things easy

rafik
  • 31
  • 7

4 Answers4

1

I think your main issue here is that each of your functions (handleChange, addTask, removeTask and handleParam) are mutating the state. You should never mutate the state - that can lead to unexpected behaviour. Object in javascript (like Arrays) are being passed by reference. When you are doing this:

addTask = (event)=>{
    const arr = this.state.tasks;
    arr.push({task: this.state.task, checked: false});
    this.setState(state=>({tasks: arr, task: ''}));
  }

You are actually changing the state before the setState. Because arr = this.state.tasks, and you are performing a push - which modifies both arr and this.state.tasks. You should write something like this:

addTask = (event)=>{
    const arr = [...this.state.tasks];
    arr.push({task: this.state.task, checked: false});
    this.setState(state=>({tasks: arr, task: ''}));
  }

You can read more about it here: https://daveceddia.com/why-not-modify-react-state-directly/

nir shabi
  • 367
  • 1
  • 15
0

You can write your logic in callback of setState. example:

this.setState({a: 2}, ()=> console.log(this.state.a})
0

If you want to make sure your components are showing the most current data, you will need to use a callback in setState. This will wait to set the state of your component, then you can call another function afterwards to do the things you want with updated state variables.

this.setState({modalVisible: true}, ()=> //do something here)
Michael
  • 1,454
  • 3
  • 19
  • 45
0

Sorry, after trying to debug the problem, the problem is not the setState but the clickEvent which is not fired as it should because i set the onClick attribute on an icon<i></i> (not on the button itself, i didn't notice it), the problem is solved.

Note: Since the original question is about an atelrnative to update the state immediatly, i want to mention that there is an alternative way to force the render but is not encouraged: 1/ forceUpdate and 2/ using key attribute Can you force a React component to rerender without calling setState?

rafik
  • 31
  • 7