How to avoid full reload of custom component?

I’m building a custom component that embeds a canvas element. I’m testing both the react-based and the vanilla templates.
The component should update the canvas when new data is passed to the component, but without reloading/rerendering from scratch.
I cannot implement this behavior because the component (the iframe content) is rerendered from scratch every time.

My excpetation was that only the render method was called when new arguments are given to it. How to keep the component “mounted” to retain its internal state?

1 Like

Try using st.fragment and see if it works.

1 Like

I will try it but I don’t think it will solve my problem. I don’t want to isolate the component from the app’s reruns, what I need is to avoid the unmount/remount of the DOM node.

Passing a key to the component has worked for me:


If the key is the default None, Streamlit autogenerates one. For custom components, this usually leads to them getting a new ID at every interaction and and therefore getting remounted.

Thanks @edsaac for the proposed solution, but it doesn’t work for me.

The arguments of my component contain a “data” parameter, which is a dataframe edited with an st.data_editor. I need to update my component with the value returned by st.data_editor without loosing the state of its internal canvas element.
If I set the key parameter the canvas is retained, as you say, but the component doesn’t receive the updated data.

In fact also the Streamlit ComponentWrapper doesn’t seem to receive updates.

To achieve this behavior, avoid letting React unmount and remount the canvas. Use the useRef hook to persist the canvas element and manipulate it directly without relying on React’s render cycle. Here’s a simplified approach:

  1. Use useRef to hold the canvas element:
    const canvasRef = useRef(null);
  2. Use useEffect to initialize the canvas only once:
useEffect(() => {
  const canvas = canvasRef.current;
  const context = canvas.getContext('2d');
  // Initialize your canvas here
}, []);
  1. Update the canvas manually when props change:
useEffect(() => {
  const canvas = canvasRef.current;
  const context = canvas.getContext('2d');
  // Update canvas based on new props
}, [props.data]); // Depend on the data that updates the canvas

By managing the updates yourself and not re-rendering the canvas element, the internal state remains intact.

Sure, that’s what I’m already doing. I have a useeffect which depends on the data prop, and I already use a ref for the canvas. It works just fine with a standard React app.

The problem that I’m facing is that, apparently, the whole custom component node is remounted when new data arrives, and it’s clearly visible also with a reactless component.

It can be easily reproduced with the custom component template. If you update the value of the name parameter you will see that the global javascript of the component is executed, not only the render function.

The issue seems to be with how the parent app is handling the component. If the custom component is remounting, ensure the parent doesn’t unmount it by:

  1. Checking if the key prop is changing on the component, as that forces a remount. Use a stable key.
  2. Verifying that the parent app isn’t re-rendering in a way that causes the component to unmount.

If using a framework like Stencil or Web Components, make sure to manage state internally and avoid relying on external re-renders. Feel free to ask me, if your problem still continues or I am missing an important part, in understanding your problem

This is a contrived portion of my app:

import streamlit as st
from midicanvas.streamlit_midicanvas import streamlit_midicanvas
import numpy as np
import pandas as pd
import plotly.express as px

st.set_page_config(layout="wide")

if "data" not in st.session_state:
    st.session_state["data"] = pd.DataFrame(
            np.random.randn(10, 20),    
            columns=('col %d' % i for i in range(20)))

canvas_container = st.container()

col1, col2 = st.columns(2)

with col1:
    st.markdown("#### Editor")
    edited_data = st.data_editor(st.session_state["data"], key="editor")

with col2:
    st.markdown("#### Plotly scatter")
    fig = px.scatter(edited_data)
    event_data = st.plotly_chart(fig, on_select="rerun")

with canvas_container:
   streamlit_midicanvas("giohappy", edited_data, key="canvas_component")

If I remvoe the key the React app is mounted on every app run. It can be seen in the following video, where the ComponentWrapper.componentDidMount is called on the first run, but also every time I modify the data with the Editor component.

component_update

If I set the key niether componentDidMount nor my useEffect is called. It’s like the component is frozen, it doesn’t react to the updated data param.

useffect_not_called

A similar effect can be reproduced with a reactless component. In that case the remount can be seen when the global js script is called on every run.

Do you have examples of custom components that accept updated data but retain their internal state? I couldn’t find anyone.
I hve inspected the st.data_edtorcomponent but it seems to use specific core setup code,.

I can use ChatGPT myself, no need to use a forum to get automated answers :slight_smile:

1 Like

The workaround that I’ve seen is to generate a key that is some hash from input data that you want to use to actually trigger a reload of the component. Here is an example of that implementation from the streamlit-folium component:

Another from the streamlit-keyup component:

Or an example with stpyvista:
variable_key

Code:
import pyvista as pv
import streamlit as st
from stpyvista import stpyvista

with st.sidebar:
    color = st.color_picker("Color", value="#ff0000")
    another_widget = st.slider("Another Widget", 0, 100)

sphere = pv.Sphere()
plotter = pv.Plotter()
plotter.add_mesh(sphere, color=color)
plotter.view_isometric()

cols = st.columns([1, 2])
with cols[0]:
    st.title("A variable key")
    st.write(f"The selected color is **`{color}`**")
    st.write(f"The slider value is **`{another_widget}`**")

with cols[1]:
    key = f"stpv_{color}"  # <-- my very basic hashing to generate a key
    stpyvista(plotter, key)

Thanks again @edsaac but I suspect I’m not explaining well my goal.

Your example with pyvista shows that whether I move the slider or change the color, the html inside the component’s iframe is fully reloaded.

What I’m trying to achieve is (following the pyvista example) being able to change the color of the sphere without reloading the component. If I rotate the sphere, I want its color to change but not the rotation (as an example).

So, using a static key should do the job, but the problem is that in this case the component doesn’t receive the “partial” update (the data parameter).