Right now the st_custom_slider
doesn’t feature any attributes that a Streamlit user would find useful, such as a minimum and maximum range.
How can we enable the user to pass arguments from Python and define some attributes of the slider, like its minimum and maximum range ?
If you look at the docs for the underlying baseui
Slider, you will notice that the min
and max
props can be passed to limit the range of the slider.
So let’s start by changing the content of the CustomSlider.tsx
to add fixed values to the given props:
import React, { useEffect, useState } from "react";
import { Streamlit, withStreamlitConnection } from "streamlit-component-lib";
import { Slider } from "baseui/slider";
const CustomSlider = () => {
const [value, setValue] = useState([10]);
useEffect(() => Streamlit.setFrameHeight());
return (
<Slider
value={value}
onChange={({ value }) => value && setValue(value)}
onFinalChange={({ value }) => console.log(value)}
min={10}
max={1000}
/>
);
};
export default withStreamlitConnection(CustomSlider);
This should properly set up the range of your slider. But we would prefer those values to be transferred by the user from the Streamlit script.
Let’s return to the __init__.py
file, where we defined the connection between Python and the frontend. The _component_func
—returned by declare_component
—is responsible for “accessing” the component. Any argument used to call the function is passed as props to the React component through a Javascript event.
Only JSON-seriazable data and Dataframes can be transferred from Python to the frontend component, so make sure that you pass Python native types or Pandas Dataframes.
Try it by passing a min
and max
variable in _component_func
and retrieve it from the React code, then edit your __init__.py
file accordingly:
import streamlit.components.v1 as components
_component_func = components.declare_component(
"custom_slider",
url="http://localhost:3001",
)
def st_custom_slider():
# Pass min and max from Python to the frontend component
component_value = _component_func(min=0, max=42)
return component_value
Print props of the CustomSlider
function in CustomSlider.tsx
and declare those props as your function’s arguments :
import React, { useEffect, useState } from "react";
import {
Streamlit,
withStreamlitConnection,
ComponentProps,
} from "streamlit-component-lib";
import { Slider } from "baseui/slider";
const CustomSlider = (props: ComponentProps) => {
console.log(props.args);
const [value, setValue] = useState([10]);
useEffect(() => Streamlit.setFrameHeight());
return (
<Slider
value={value}
onChange={({ value }) => value && setValue(value)}
onFinalChange={({ value }) => console.log(value)}
min={10}
max={1000}
/>
);
};
export default withStreamlitConnection(CustomSlider);
Reload your browser, open your JS console and have a look at your print logs.
So you get props.args = { min: 0, max: 42 }
. The props
dictionary has an args
key in which all arguments from _component_func
are being passed as a JS object.
You should see the props printed everytime you interact or render the component, which can result in a lot of printing. In more sophisticated components, you may need to throttle or debounce such calls to prevent unecessary rapid-fire function calls (which may break your browser).
Arguments passed from Python to React props is a specific behavior of the React template. The arguments are actually passed through a Javascript event forwarded by the Python call, and the withStreamlitConnection
class handles transferring those as props
of the CustomSlider
.
Conclude the exercise by passing a label
, min
and max
value from the app.py
file all the way down to the React component props. Since we are using Streamlit, we can use Streamlit widgets to interactively define the label
, min
, and max
arguments.
app.py
import streamlit as st
from streamlit_custom_slider import st_custom_slider
st.title("Testing Streamlit custom components")
# Add Streamlit widgets to define the parameters for the CustomSlider
label = st.sidebar.text_input('Label', 'Hello world')
min_value, max_value = st.sidebar.slider("Range slider", 0, 100, (0, 50))
# Pass the parameters inside the wrapper function
v = st_custom_slider(label=label, min_value=min_value, max_value=max_value)
st.write(v)
__init__.py
import streamlit.components.v1 as components
_component_func = components.declare_component(
"custom_slider",
url="http://localhost:3001",
)
# Add label, min and max as input arguments of the wrapped function
# Pass them to _component_func which will deliver them to the frontend part
def st_custom_slider(label: str, min_value: int, max_value: int):
component_value = _component_func(label=label, minValue=min_value, maxValue=max_value)
return component_value
CustomSlider.tsx
import React, { useEffect, useState } from "react";
import {
ComponentProps,
Streamlit,
withStreamlitConnection,
} from "streamlit-component-lib";
import { Slider } from "baseui/slider";
const CustomSlider = (props: ComponentProps) => {
/**
* Destructuring JSON objects is a good habit.
* This will look for label, minValue and maxValue keys
* to store them in separate variables.
*/
const { label, minValue, maxValue } = props.args;
const [value, setValue] = useState([10]);
useEffect(() => Streamlit.setFrameHeight());
// Add a label and pass min/max variables to the baseui Slider
return (
<>
<h3>{label}</h3>
<Slider
value={value}
onChange={({ value }) => value && setValue(value)}
onFinalChange={({ value }) => console.log(value)}
min={minValue}
max={maxValue}
/>
</>
);
};
export default withStreamlitConnection(CustomSlider);
There it is! Python arguments are propagated all the way to the underlying baseui
slider and its properties are modified accordingly.
Did you notice a problem on the demo ?
Everytime an input argument of st_custom_slider
on the user side changes, the CustomSlider
gets new props. Consequently, the component is considered a new object and is remounted…which means your older slider is dropped and a new slider is created. This behavior causes the component to “flicker” if you edit the parameters regularly and the state of your older slider isn’t preserved when the CustomSlider
is recreated!
We need a way to prevent the component from recreating itself every time its properties change.