React event handler doesn't assign mode as expected, evaluates as null

265 views Asked by At

I have a page that looks like this:

enter image description here

When clicking the row's edit button, the component is supposed to set the selected entity to that row and put itself into "EDIT" mode.

However, when displaying a box in the upper right (component called toast here), the mode is evaluated to null on the very first click:

enter image description here

Here's the code:

import React, { Component } from 'react';

import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Button } from 'primereact/button';
import { Toast } from 'primereact/toast';

import 'primereact/resources/themes/luna-blue/theme.css';
import 'primereact/resources/primereact.min.css';
import 'primeicons/primeicons.css';

const modes =
{
    VIEW:   'VIEW',
    ADD:    'ADD',
    EDIT:   'EDIT',
    REMOVE: 'REMOVE'
}

class TestManager extends Component
{
    constructor()
    {
        super();

        let persons = [
            {
                "id": 3,
                "zipCode": "",
                "cityName": "",
                "streetName": "",
                "houseNbr": "",
                "firstName": "Ahmed",
                "lastName": "Al-Thiab",
                "gender": "MALE",
            },
            {
                "id": 4,
                "zipCode": "",
                "cityName": "",
                "streetName": "",
                "houseNbr": "",
                "firstName": "Stefan",
                "lastName": "Antalovics",
                "gender": "MALE",
            },
            {
                "id": 20,
                "zipCode": "",
                "cityName": "",
                "streetName": "",
                "houseNbr": "",
                "firstName": "Christian",
                "lastName": "Attina",
                "gender": "MALE",
            },
            {
                "id": 15,
                "zipCode": "",
                "cityName": "",
                "streetName": "",
                "houseNbr": "",
                "firstName": "Filmon",
                "lastName": "Berhane",
                "gender": "MALE",
            },
            {
                "id": 2,
                "zipCode": "64895",
                "cityName": "Darmstadt",
                "streetName": "",
                "houseNbr": "",
                "firstName": "Johannes",
                "lastName": "Loczewski",
                "gender": "MALE",
            },
            {
                "id": 1,
                "zipCode": "22880",
                "cityName": "Wedel",
                "streetName": "Rosengarten",
                "houseNbr": "6",
                "firstName": "Karsten",
                "lastName": "Wutzke",
                "gender": "MALE",
            }
        ]
        
        this.state = {entities: persons,
                      selectedEntity: null,
                      mode: null};
        
        this.onRowEdit = this.onRowEdit.bind(this);
        this.onRowRemove = this.onRowRemove.bind(this);
        
        this.actions = this.actions.bind(this);
    }
    
    render()
    {
        var header = "Person Manager (" + this.state.entities.length + ")"

        return (
            <div style={{ maxWidth: 1000, marginLeft: "auto", marginRight: "auto", marginTop: 10, marginBottom: 10 }}>
                
                <Toast ref={(el) => this.toast = el} />
                
                <DataTable value={this.state.entities}
                           header={header}
                           dataKey="id"
                           selection={this.state.selectedEntity}
                           selectionMode="single"
                           sortField='lastName'
                           sortOrder={1}
                           resizableColumns
                           columnResizeMode="fit"
                           className="p-datatable-striped">
                    <Column field="id" header='ID' sortable style={{width:'7.5%'}} />
                    <Column field="gender" header='Sal.' body={this.salutation} sortable style={{width:'10%'}} />
                    <Column field='lastName' header='Last Name' sortable style={{width:'15%'}} />
                    <Column field='firstName' header='First Name' sortable style={{width:'15%'}} />
                    <Column field='streetName' header='Street' body={this.street} sortable style={{width:'20%'}} />
                    <Column field='zipCode' header='ZIP' sortable style={{width:'10%'}} />
                    <Column field='cityName' header='City' sortable style={{width:'10%'}} />
                    <Column field="actions" body={this.actions} style={{width:'12.5%'}} />
                </DataTable>
            </div>
        );
    }

    actions(rowData)
    {
        return (
            <>
                <div style={{textAlign: "center"}}>
                    <Button icon="pi pi-pencil"
                            tooltip="Edit"
                            onClick={() => this.onRowEdit(rowData)}
                            className="p-button-sm p-button-raised p-button-rounded p-button-outlined" />
                    <Button icon="pi pi-trash"
                            tooltip="Remove"
                            className="p-button-sm p-button-raised p-button-rounded p-button-outlined"
                            onClick={() => this.onRowRemove(rowData)}
                            style={{marginLeft: 5}} />
                </div>
            </>
        );
    }

    onRowEdit(rowData)
    {
        this.setState({selectedEntity: rowData});
        this.setState({mode: modes.EDIT});

        console.log("Last name: " + rowData.lastName);
        console.log("Mode: " + this.state.mode);

        this.toast.show({ severity: 'info', summary: 'Editing Person', detail: 'Name: ' + rowData.lastName + ", " + this.state.mode, life: 3000 });
    }

    onRowRemove(rowData)
    {
        this.setState({selectedEntity: rowData});
        this.setState({mode: modes.REMOVE});

        this.toast.show({ severity: 'info', summary: 'Removing Person', detail: 'Name: ' + rowData.lastName + ", " + this.state.mode, life: 3000 });
    }

    salutation(rowData)
    {
        var gender = rowData['gender'];

        if ( gender )
        {
            switch( gender )
            {
                case "MALE":
                    return "Mr";
                
                case "FEMALE":
                    return "Mrs";
                
                default:
                    return "Error";
            }
        }

        return null;
    }

    street(rowData)
    {
        var streetName = rowData['streetName'];
        var houseNumber = rowData['houseNbr'];

        return houseNumber ? streetName + " " + houseNumber : streetName;
    }
}

export default TestManager;

QUESTION:

What's wrong here?

According to this

Does React keep the order for state updates?

there shouldn't be any problems with the method callback, a.k.a. as event handler, updating the mode

onRowEdit(rowData)
{
    this.setState({selectedEntity: rowData});
    this.setState({mode: modes.EDIT});

    console.log("Last name: " + rowData.lastName);
    console.log("Mode: " + this.state.mode);

    this.toast.show({ severity: 'info', summary: 'Editing Person', detail: 'Name: ' + rowData.lastName + ", " + this.state.mode, life: 3000 });
}

this.state.mode is always null on the very first click. On subsequent clicks, no matter which row, the correct mode is being shown.

Why?

How do I fix this?

BTW: this.state.selectedEntity on the line using toast is null as well.

I'm having trouble understanding this. -> self-learner

3

There are 3 answers

0
ray On BEST ANSWER

The state updates won't be (necessarily) reflected in this.state until the next render.

From the docs:

setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback

You already know the mode, so you can either pass modes.EDIT instead of this.state.mode:

this.toast.show({ detail: "..." + modes.EDIT, life: 3000 });

or use the callback form of setState which ensures that state has updated before calling the subsequent toast code:

this.setState({mode: modes.EDIT}, () => this.toast.show( ... ))

Note: it's possible that your console.log call will show the updated value because console.log runs asynchronously. (Between the time you call it and the time it appears in the console the state may have changed.)

0
AudioBubble On

setState is async. You can't expect your state to have changed yet.

Try this:

this.setState({mode: modes.EDIT}, () => {
  console.log("Mode: " + this.state.mode);
});
0
Michale Rezene On

Well it is a fair question but setState is async. So either use callback function inside it or use Promise to execute some setState after another setState

this.setState( (mode) => mode : modes.EDIT)

or

Promise.all(#First Async).then(#Second Async)