How does Streamlit component create a key session state variable?

Hi Streamlit community,

As English isn’t my native language, I didn’t managed to find a topic discussing about this already… I try to create a custom component with React/MUI/Typescript technologies. Everything is working well but I ran into a little issue of comprehension I guess : my custom component doesn’t initialize its default value in the session state variable called by its key. I hope you can help me understand what’s going on here.

Hereunder, I show you the result of a simple reproduction of a MUI Select component. I wrote a script that instantiates one custom component, then writes its output and finally writes the session state of the application.

init.py file :

import streamlit.components.v1 as components

_select_image = components.declare_component(
    "streamlit_select_image",
    url="http://localhost:3001",
)


def st_select_image(options, label="", key=None):
    image = _select_image(label=label, options=options, key=key, default=options[0])
    return image

Custom React/MUI component :

import {
  Streamlit,
  withStreamlitConnection,
} from "streamlit-component-lib"
import React, { ComponentProps, useEffect, useState } from "react"
import MenuItem from '@mui/material/MenuItem';
import Select, { SelectChangeEvent } from "@mui/material/Select"
import { Box, FormControl, InputLabel } from "@mui/material"

const SelectImage = (props: ComponentProps<any>) => {
  const options = props.args["options"];
  const [value, setValue] = useState(options[0]);
  useEffect(() => Streamlit.setFrameHeight(150));

  function handleChange(event: SelectChangeEvent) {
    Streamlit.setComponentValue(event.target.value as string);
    setValue(event.target.value);
  }

  return (
    <Box sx={{ minWidth: 50 , marginTop: 2}}>
      <FormControl fullWidth>
        <InputLabel id="image-select-label">{props.args["label"]}</InputLabel>
        <Select
          labelId="image-select-label"
          id="image-select"
          value={value}
          label="Image"
          onChange={handleChange}
        >
          {options.map((data: string) => (
            <MenuItem value={data}>{data}</MenuItem>
          ))}
        </Select>
      </FormControl>
    </Box>
  );
}

export default withStreamlitConnection(SelectImage)

Hereunder, you can see that the output of my custom component is as expected the first option on its list. However, the session state variable attached to its key is NULL.

Once I’ve selected another option on its list, it actualize the session state variable, as you can see on the following screenshot.

Am I doing something wrong at the initialization of the component ? Or is it impossible to initialize the session state variable connected to the instance of a custom component ?

I hope my question was clear, please, let me know if you want me to rephrase it.

Thank you for your help,
Happy New Year everyone !
Victor

Hi everyone,

I think it would be great to explain how I managed to make this work eventually.

The answer to my question was to use the abstract class StreamlitComponentBase to build my component as a class.

import {
  Streamlit, StreamlitComponentBase,
  withStreamlitConnection,
} from "streamlit-component-lib"
import React, { ComponentProps, useEffect, useState } from "react"
import MenuItem from '@mui/material/MenuItem';
import Select, { SelectChangeEvent } from "@mui/material/Select"
import { Box, FormControl, InputLabel } from "@mui/material"

class SelectImage extends StreamlitComponentBase {

  state = { back: [], options: this.props.args.options, images: this.props.args.images, value: this.props.args.default}

  componentDidMount() {
    const back: any[] = [];
    for (let i=0; i < this.state.options.length; i++) {
      back.push({option: this.state.options[i], image: this.state.images[i]});
    }
    this.setState((prev, state) => ({
      back: back,
    }), () => Streamlit.setComponentValue(this.state.value))
  }

  handleChange = (event: SelectChangeEvent) => {
    this.setState((prev, state) => ({
      value: event.target.value
    }),
      () => Streamlit.setComponentValue(this.state.value)
    )
  }

  render = () => {
    if (this.props.args["label"] !== "") {
      return (
        <Box sx={{ minWidth: 50 , marginTop: 2}}>
          <FormControl fullWidth>
            <InputLabel id="image-select-label">{this.props.args["label"]}</InputLabel>
            <Select
              labelId="image-select-label"
              id="image-select"
              value={this.state.value}
              label="Image"
              onChange={this.handleChange}
            >
              {this.state.back.map(({ image, option }) => (
                <MenuItem value={option}><img alt={option} src={image} height={"100px"} width={"100px"}/></MenuItem>
              ))}
            </Select>
          </FormControl>
        </Box>
      );
    } else {
      return (
        <Box sx={{ minWidth: 50 , marginTop: 2}}>
          <FormControl fullWidth>
            <Select
              labelId="image-select-label"
              id="image-select"
              value={this.state.value}
              onChange={this.handleChange}
            >
              {this.state.back.map(({ image, option }) => (
                <MenuItem value={option}><img alt={option} src={image} height={"100px"} width={"100px"}/></MenuItem>
              ))}
            </Select>
          </FormControl>
        </Box>
      );
    }
  }

}

export default withStreamlitConnection(SelectImage)

As you can see, I exchanged the useState function by a state class attribute. I used the componentDidMount to initialize the value of my component in the session state.

I rewrote the handleChange function as a class method sending the state value variable to Streamlit !

Eventually, when I instantiate with a for loop multiple custom component, it gives me the path corresponding to the first image as you can see below (compared to the None value before).

I hope this topic will help some people building Streamlit custom component, I intend to build some useful ones !

Happy new year everyone again,
Have a good day,
Victor

1 Like