Proper use of on_change with st.experimental_data_editor

Summary

I want to use the on_change parameter in the st.experimental_data_editor so each time a user interacts with the dataframe the new modified dataframe is saved and stored.

Steps to reproduce

def update(conn, edited_df, column, notes):
     for row_1, row_2, row_3 in zip(edited_df['col1'], edited_df['col2'], edited_df['col3']):
         cursor.execute('INSERT INTO mytable VALUES %s, %s, %s', (value1, value2, value3,))
       # some more code

edited_df=st.experimental_data_editor(df, key=f"editor{number}", num_rows='dynamic', on_change=update, args=(conn, edited_df, name, column[0], notes))

I understand that the general problem here is that I am passing ‘edited_df’ as an argument to on_change and ‘edited_df’ is not yet assigned as it waits for user input and the callback function is run first before the script is rerun from top to bottom.

From what I have read there has to be a way to do this with session state but I can’t seem to totally wrap my head around it yet.

Any help would be greatly appreciated :slight_smile:

Here is one way to do it

import pandas as pd
import streamlit as st

df = pd.DataFrame(
    {
        "col1": [1, 2, 3, 4, 5],
        "col2": [10, 20, 30, 40, 50],
        "col3": [100, 200, 300, 400, 500],
    }
)

if "df_value" not in st.session_state:
    st.session_state.df_value = df


def update(edited_df):
    for row_1, row_2, row_3 in zip(
        edited_df["col1"], edited_df["col2"], edited_df["col3"]
    ):
        st.write(
            "INSERT INTO mytable VALUES %s, %s, %s",
            (
                row_1,
                row_2,
                row_3,
            ),
        )
    # some more code


edited_df = st.experimental_data_editor(
    df,
    key="editor",
    num_rows="dynamic",
)

if edited_df is not None and not edited_df.equals(st.session_state["df_value"]):
    # This will only run if
    # 1. Some widget has been changed (including the dataframe editor), triggering a
    # script rerun, and
    # 2. The new dataframe value is different from the old value
    update(edited_df)
    st.session_state["df_value"] = edited_df

You can see it in action here:

Did not work unfortunately. Which is odd because I use very similar code to exactly what you provided at a different point in my script. As a matter of fact, at other points in my script, the code does exactly what I want. The exact problem n this section is that when I modify a value in edited_df, the first modified value is saved, but when i modify a second value in edited_df, the script rerun resets the second modified value, but keeps the first modified value. This problem continues no matter how many modifications I make. Eventually, all modifications can be saved, but it requires double input for essentially all of them.

I am wondering if it is because of the nature of df. In this specific section of my code, df is not hard-coded, but rather pulled from a database, specifically, it is pulled from ‘mytable.’ I am wondering if the fact that I am updating ‘mytable’ with my function, and then calling df again with every script rerun is essentially reseting the original value of edited_df. When I include st.write(st.sesion_state) I can see that the first modification is saved, but upon the second modification, it resets. See what I mean below


def update(conn, edited_df, column, notes):
     for row_1, row_2, row_3 in zip(edited_df['col1'], edited_df['col2'], edited_df['col3']):
         cursor.execute('INSERT INTO mytable VALUES %s, %s, %s', (value1, value2, value3,))

def check_if_exists(conn, name):
    cursor=conn.cursor()
    ## some code
    if exits:
            st.write("## Data in 'mytable'! Continue modifying?")
            
            #Getting table for query
            cursor.execute(f"""SELECT * FROM mytable WHERE id = %s
            """, (id,))

            rows=cursor.fetchall()

            df=pd.DataFrame(rows)
            if "df_value" not in st.session_state:
                st.session_state.df_value = df 
            edited_df=st.experimental_data_editor(df, num_rows='dynamic')
            try:
                if not edited_df.equals(st.session_state["df_value"]):
                    update(conn, edited_df, name, workout_number)
                    st.session_state['df_value'] = edited_df

I run similar code somewhere else in script where I pull from a database ad modify in st.experimental_data_editor, but each modification doesn’t modify the original table that represents df, and that works as expected so curious if you think that is the problem.

So I was able to solve it, but frankly, I am not entirely sure how/why it works. I defined a new function that sets the value of st.session_state[‘df_value’] to edited_df and put that in the on_change parameter of edited_df, however, when I “st.write(st.session_state[‘df_value’])” the values never actually update. In my previous problem, where it always took two modifications for the changes to take effect, I found the value of st.session_state[‘df_value’] to lag behind the edited_df modifications by one modification. So with each rerun, my edited_df would reset to the value of session_state[‘df_value’]. Now the values for st.session_state[‘df_value’] dont change at all, but my edited_df doesn’t reset with every second modification. See my solution below

def update(conn, edited_df, column, notes):
     for row_1, row_2, row_3 in zip(edited_df['col1'], edited_df['col2'], edited_df['col3']):
         cursor.execute('INSERT INTO mytable VALUES %s, %s, %s', (value1, value2, value3,))

def change_state(edited_df):
      st.session_state['df_value']=edited_df


def check_if_exists(conn, name):
    cursor=conn.cursor()
    ## some code
    if exits:
            st.write("## Data in 'mytable'! Continue modifying?")
            
            #Getting table for query
            cursor.execute(f"""SELECT * FROM mytable WHERE id = %s
            """, (id,))

            rows=cursor.fetchall()

            df=pd.DataFrame(rows)
            if "df_value" not in st.session_state:
                st.session_state.df_value = df
            edited_df=st.session_state['df_value']
            edited_df=st.experimental_data_editor(edited_df, num_rows='dynamic', on_change=change_state, args=(edited_df,))
            update(edited_df)

Any explanation behind why this somehow solved my issue, or why the issue was occuring in the first place would be appreciated :sweat_smile:

5 Likes

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.