Regressions, changes in Streamlit

Hello everyone,

I have been interested in Streamlit since the beginning of the year. Initially, I was very enthusiastic about the product and its simplicity, but now I feel that as soon as we go beyond basic use, especially when defining our own components, the product’s stability is lacking.

Unfortunately, I can’t prove it, but what used to work no longer does, even when reverting to earlier versions of Streamlit. Of course, some might say that I must be imagining things, that the code I developed doesn’t work and never did.

I’ll provide a recent example where everyone can judge and critique the code I wrote and perhaps show me that I am wrong.

I used “Replit” to create a small Streamlit application that defines an external component, which is just a button using React and Ant Design.

You can access my code using the following link: Sign Up - Replit

The page of interest displays three buttons and the value returned when clicking on any of them.

When switching from one button to another, everything is fine: the value “true” is returned for the clicked button, and “false” is returned for the other buttons.

The two buttons on the left are “custom” buttons (external components). The button on the right is a standard Streamlit button (st.button).

Now, let’s click multiple times in a row on the same button. The standard Streamlit button always returns “true”, while the “custom” buttons alternate between “true” and “false,” which is absurd.

Why? I have no idea. Some “clicks” result in a re-instantiation of the component without activating the JavaScript code…

If you know the answer to what, for me, is a mystery, I would be grateful if you could explain it.

Kind regards.

Some additional explanations:

In theory, implementing a component that returns a true or false value should be an easy task, but in reality, it is quite complicated.

JavaScript code cannot simply return true or false. Once the button has been used, it must be reinstantiated to function correctly in the next iteration and indicate that its default value is false. The only way to ensure it will be reinstantiated is to implement a counter and increment the counter each time the button is pressed.

So if we are looking at the actual implementation we have:

In the implementation of the react component:


class Button(ReactComponent):

    def click():
        Streamlit.setComponentValue(self.props.counter + 1)

    def render():
        props = dict()
        props[
            'onClick'] = self.props.onClick if self.props.onclick else self.click
        props['type'] = self.props.type
        props['disabled'] = self.props.disabled
        props['danger'] = self.props.danger
        props['size'] = self.props.size
        props['ghost'] = self.props.ghost
        props['block'] = self.props.block
        props['shape'] = self.props.shape
        props['loading'] = self.props.loading
        props['iconPosition'] = self.props.iconPosition
        if self.props.icon:
            props['icon'] = React.createElement(icons[self.props.icon], {})
        return React.createElement(antd.Button, props, self.props.label)

self.props is an object containing the parameters of the component. The “counter” property is incremented by one when the button is pressed and the value is returned to the python caller.

The python code encapsulating the component is the following:

def use_Button(component):

    class Component:

        def __init__(self, id=None, **d):
            id = f'Component_{id}'
            if id not in st.session_state:
                st.session_state[id] = dict(old=0, new=0)
            result = component(id=id,
                               counter=st.session_state[id]['new'],
                               default=st.session_state[id]['new'],
                               **d)
            if result != st.session_state[id]['new']:
                st.session_state[id]['new'] = result
            if st.session_state[id]['old'] != st.session_state[id]['new']:
                self.result = True
                st.session_state[id]['old'] = st.session_state[id]['new']
            else:
                self.result = False

        def get(self):
            return self.result

    return Component

The first interesting instruction is:

result = component(id=id, counter=st.session_state[id]['new'],
default=st.session_state[id]['new'], **d)

Here we are defining a component with 3 important parameters: an id, a counter and a default value which is the last value of the counter or 0 is the counter has not been used.

2 cases can occur:

a) the result is equal to the default value ==> that means that the javascript code has NOT been called meaning that the button has not been pushed.

b) the result is different of the last recorder value of the button ==> that means that the javascript code has been activated due to a push on the button

Everything is OK if we switch from one button to another.

But if we push the same button several times there are clicks that are not followed of javascript code activation.

The default value of a component should be taken into account only at the first iteration when the component has not been displayed yet.

During subsequent iterations, the JavaScript code should be activated systematically.

This is not the case in this current example

Hi, I think the key to understanding this behaviour lies in how Streamlit handles the state of widgets and re-renders components. When you click on a button, Streamlit re-runs the script from top to bottom, which causes a re-rendering of the component. This is where the problem arises for your components. You may need to review the implementation of your custom buttons and make sure they handle Streamlit’s re-rendering mechanism correctly. One possible approach is to use Streamlit’s st.session_state to store the button state and update it correctly during re-rendering

Thank you for your answer Oscar.

My problem is simple: I have an application with three buttons, two of which are custom. I want an implementation for my custom buttons with the same behavior as st.button. Tell me exactly how to do this without staying at a general level. Personally, I don’t know how to do it. I’ve spent time thinking about this problem, and I am at a dead end.

I have extensive experience in development. That doesn’t mean I’m always right about everything; I could be wrong, but I have my doubts: the behavior I’m observing now is not the one I observed in my tests a few weeks or months ago. I’ve uninstalled Streamlit (the most recent version) and reinstalled older versions to try to identify a regression or a change in behavior. The current behavior in version 1.38.0 is the same as in the older version 1.25.0 that I REINSTALLED.

I have a question: when I ran version 1.25.0 at the time it was released and when I run a version 1.25.0 installed recently, am I executing the same code? Some might say yes, of course, but is that really the case?

Obviously, I’m just a human being like anyone else who can be wrong or have been mistaken a few weeks ago…

That’s why I’m presenting a case in this application. Everyone can see the issue I’m exposing and the associated code. Personally, I don’t know how to solve the problem.

Prove me wrong using my example, give me the correct implementation, and I will say, yes, you are right.

For now, I remain very skeptical.

Thanks for sharing all the details. I get how frustrating it can be when things stop working the way they used to, especially with custom components. From what you’ve described, it sounds like the issue might be tied to how Streamlit handles state and re-renders for custom components.

Honestly, custom components are a bit of a double-edged sword. They give you tons of flexibility to build exactly what you need, but that freedom also comes with some challenges, especially when it comes to managing state and ensuring things behave consistently across re-renders. The trade-off is that while you can build powerful interactions with JavaScript and React, it can also get tricky to debug when things don’t line up perfectly between the front end and Python.

One other thing to consider: if even going back to earlier versions of Streamlit isn’t solving the problem, it’s possible another dependency (React, Ant Design, etc.) might have changed, which could be affecting the behavior of your custom component. Dependencies outside of Streamlit itself could be at play, so it’s worth checking for updates or breaking changes in those libraries too.

Here are a few thoughts that might help:

  1. React Component Reinstantiation: When you click the same custom button multiple times, and the result alternates between true and false, it’s likely because the component is being re-rendered, but the JavaScript logic isn’t firing consistently. You’ve got the counter in place, which is good, but it sounds like the JavaScript isn’t always getting triggered.
  2. State Management: You’re using st.session_state to keep track of the button states, but the behavior you’re seeing makes me think there could be a mismatch between the session state in Python and what’s happening on the front end. You might need a more explicit way to force an update or check that the state is in sync with the button presses.
  3. Component Lifecycle: Streamlit’s component lifecycle can sometimes be tricky, especially with custom JavaScript. Make sure your React component is properly resetting each time the button is clicked. React’s useEffect hook could be helpful here to watch for changes in the counter and force a refresh.
  4. Debugging: A quick way to figure out what’s happening might be to log the values of old and new from st.session_state to see if they’re updating as expected. Also, logging the state inside the JavaScript could give you a better idea of whether the click events are consistently registering.
  5. Potential Fix: As a workaround, you could try forcing a full re-render of the component when a button is clicked by incrementing a separate state value. This way, you make sure React always runs its re-render logic and the JavaScript behaves as expected.

Let me know if you want to dive deeper into the details or if anything specific is still unclear. Hope this helps point you in the right direction!

Hello Nico,

Many thanks for your detailed answer.

I’m pretty sure that the problem is not related to React and/or Ant Design because I have exactly the same behavior using Vue.js instead of React and Element plus instead of Ant Design.

Your first point “React Component Reinstantiation” is perfectly correct: the component is reinstantiated but javascript SEEMS not triggered. This is correct when the component is intantiated the first time(the Button has not been pushed) but this is incorrect when the reinstantiation is the consequence of a click with input parameters attached to the component updated.

Weeks ago, the behavior was not the same and my own code was different too. To have the correct behavior weeks ago, I had a “st.rerun” somewhere in the code. Now with that st.rerun, buttons are always returning false.

Something changed in the meantime, but I am unable to prove it by reinstalling an earlier version of Streamlit.

I will continue to investigate by tracing the counter values in JavaScript and the component responses in Python.

Nevertheless, thank you for your help.

Additional clarification: JavaScript is called on each “click,” but the counter value is the previous one. However, during the call, the counter parameter is initialized with the last value stored in st.session_state.

It’s as if the component reset takes into account the previous parameters of the component.