Search code examples
javascriptreactjsreact-hooksmobxmobx-react

React Store does not return getMethod's value first time


I am new to React, Mobx stack. I bumped into a problem where my Store does not reflect its value immediately after the first call. when I make the second call, it returns the first value. I am unable to understand why it happens since I have a long way to go in React, Mobx stack.

My Store Class

export default class ServiceTemplateStore {

    loadingInitial = false;

    serviceTemplateDetail: any | undefined;

    constructor() {
        makeAutoObservable(this)
    }

    setLoadingInitial = (state: boolean) => {
        this.loadingInitial = state;
    }


    get getServiceTemplateDetail() {
        return this.serviceTemplateDetail;
    }

    
    loadServiceTemplateDetail = async (id: string) => {
        this.loadingInitial = true;

        try {

            const serviceTemplateDetail = await agent.serviceTemplateDetail.details(id);

            runInAction(() => {
                console.log("Service loaded");
                this.serviceTemplateDetail = serviceTemplateDetail;
                console.log(this.serviceTemplateDetail); //Here I can see it immediately outputs the value.
                this.setLoadingInitial(false);
            });
            
        } catch (error) {
            console.log(error);
            this.setLoadingInitial(false);
        }
    }

}

Child component

Here is where I am using the Store, when I use the useEffect Hook it actually populates the data, but when I run in inside onClick function it does not populate the value.

What my understanding is that Axios GET call executes as expected, also it returns the value back and assigns it to serviceTemplateDetail variable. but when I am accessing the value via getServiceTemplateDetail method it returns null for the first time, and when I click the Import Button next time it returns the old value which was returned from GET Request

import DataGrid, { Selection } from "devextreme-react/data-grid";
import Popup, { Position, ToolbarItem } from "devextreme-react/popup";
import { Column } from "devextreme-react/tree-list";
import { observer } from "mobx-react-lite";
import React, { Component, useCallback, useEffect, useState } from 'react'
import { useStore } from "../store/Store";


export default observer(function ImportServiceTemplate(props: importServiceProp) {

    const [ serviceTemplateIds, setServiceTemplateIds] = useState([]);
    const { serviceTemplateStore } = useStore();
    const { serviceTemplateList, loadServiceTemplateList, loadServiceTemplateDetail, getServiceTemplateDetail, serviceTemplateDetail } = serviceTemplateStore;
    
    useEffect(() => {
        if (serviceTemplateList?.length === 0 || serviceTemplateList === undefined) loadServiceTemplateList();
    }, [loadServiceTemplateList])


    const closeButtonOption = {
        icon: 'close',
        text: 'Close',
        onClick: ()=>{
            props.onCloseImportModel();
        }
    };

    //Added a useEffect to test..
    //UseEffect returns the value but not my Import function below
    useEffect(() => {
        console.log("use effect");
        console.log(JSON.stringify(getServiceTemplateDetail));
    }, [getServiceTemplateDetail])
    

    //Actual function that needs to work.
    const importButtonOption = {
        icon: 'download',
        text: 'Import',
        onClick: () => {

            if(serviceTemplateIds){

                loadServiceTemplateDetail(String(serviceTemplateIds))
                .then(()=>{
                    console.log("Callback");
                    console.log(JSON.stringify(serviceTemplateDetail));     // Getting undefined as output.
                    props.importingServiceTemplate(getServiceTemplateDetail); //Passing the imported value to parent component
                });

            }
            
        }
    };

    const onSelectionChanged = (e: any) => {
        console.log("Processing", e.selectedRowKeys);
        setServiceTemplateIds(e.selectedRowKeys);
    };

    return (
        <>

            <Popup
                visible={props.isImportVisible}
                dragEnabled={false}
                closeOnOutsideClick={false}
                showCloseButton={false}
                showTitle={true}
                title="Service Template"
                width={800}
                height={280}>
                <Position
                    at="center"
                    my="center"
                />

                <DataGrid
                    id="serviceTemplateGrid"
                    key="ServiceTemplateId"
                    keyExpr="ServiceTemplateId"
                    focusedRowEnabled={true}
                    onSelectionChanged={onSelectionChanged}
                    selectedRowKeys={serviceTemplateIds}
                    dataSource={serviceTemplateList}
                    showRowLines={true}
                    showBorders={true}>

                    <Selection mode="multiple" showCheckBoxesMode="onClick" />
                    <Column dataField="Name" caption="Name" />
                    <Column dataField="CustomerName" caption="Customer Name" />
                    <Column dataField="BaseCurrencyCode" caption="Currency" />
                    <Column dataField="Description" caption="Description" />

                </DataGrid>
    
                <ToolbarItem
                    widget="dxButton"
                    toolbar="bottom"
                    location="before"
                    options={closeButtonOption}
                />

                <ToolbarItem
                    widget="dxButton"
                    toolbar="bottom"
                    location="after"
                    options={importButtonOption}
                />
                
                <div>{JSON.stringify(getServiceTemplateDetail)}</div>

            </Popup>
        </>
    )

})

interface importServiceProp{
    isImportVisible: boolean;
    onCloseImportModel: Function
    importingServiceTemplate: any
}

Solution

  • It is not React or MobX problem, just a regular javascript closure.

    When you create importButtonOption function it remembers all the variables around it, and serviceTemplateDetail is equals undefined at first time. So after you invoke loadServiceTemplateDetail serviceTemplateDetail is changed in the store, but inside importButtonOption function it is still an old value which was remembered when the function was created.

    Hope it makes sense. Basically you just need to read about closures and how they work.

    There is a little guide in React docs

    What you can do for example is remove destructuring of values and dereference them as you need, like that:

    loadServiceTemplateDetail(String(serviceTemplateIds))
      .then(()=>{
          console.log("Callback");
          console.log(JSON.stringify(serviceTemplateStore .serviceTemplateDetail));     // reference it from the store to get actual value
          props.importingServiceTemplate(getServiceTemplateDetail); //Passing the imported value to parent component
      });