From b15ddec9e1d8640087b081b41ef1c8ed16dc63f3 Mon Sep 17 00:00:00 2001 From: raethlein Date: Wed, 4 Dec 2024 23:52:39 +0100 Subject: [PATCH 1/5] Use functional component instead of class-based --- .../my_component/frontend/src/MyComponent.tsx | 101 ++++++++---------- 1 file changed, 42 insertions(+), 59 deletions(-) diff --git a/template/my_component/frontend/src/MyComponent.tsx b/template/my_component/frontend/src/MyComponent.tsx index f68b04f1..2a372eb5 100644 --- a/template/my_component/frontend/src/MyComponent.tsx +++ b/template/my_component/frontend/src/MyComponent.tsx @@ -1,84 +1,67 @@ import { Streamlit, - StreamlitComponentBase, withStreamlitConnection, + ComponentProps, } from "streamlit-component-lib" -import React, { ReactNode } from "react" - -interface State { - numClicks: number - isFocused: boolean -} +import React, { useEffect, useState, ReactElement } from "react" /** * This is a React-based component template. The `render()` function is called * automatically when your component should be re-rendered. */ -class MyComponent extends StreamlitComponentBase { - public state = { numClicks: 0, isFocused: false } +function MyComponent({ args, disabled, theme }: ComponentProps): ReactElement { + const { name } = args - public render = (): ReactNode => { - // Arguments that are passed to the plugin in Python are accessible - // via `this.props.args`. Here, we access the "name" arg. - const name = this.props.args["name"] + const [isFocused, setIsFocused] = useState(false) + const [style, setStyle] = useState({}) + const [numClicks, setNumClicks] = useState(0) - // Streamlit sends us a theme object via props that we can use to ensure - // that our component has visuals that match the active theme in a - // streamlit app. - const { theme } = this.props - const style: React.CSSProperties = {} + useEffect(() => { + if (!theme) return - // Maintain compatibility with older versions of Streamlit that don't send - // a theme object. - if (theme) { - // Use the theme object to style our button border. Alternatively, the - // theme style is defined in CSS vars. - const borderStyling = `1px solid ${ - this.state.isFocused ? theme.primaryColor : "gray" - }` - style.border = borderStyling - style.outline = borderStyling - } + // Use the theme object to style our button border. Alternatively, the + // theme style is defined in CSS vars. + const borderStyling = `1px solid ${isFocused ? theme.primaryColor : "gray"}` + setStyle({ border: borderStyling, outline: borderStyling }) + }, [theme, isFocused]) - // Show a button and some text. - // When the button is clicked, we'll increment our "numClicks" state - // variable, and send its new value back to Streamlit, where it'll - // be available to the Python program. - return ( - - Hello, {name}!   - - - ) - } + useEffect(() => { + Streamlit.setComponentValue(numClicks) + }, [numClicks]) /** Click handler for our "Click Me!" button. */ - private onClicked = (): void => { - // Increment state.numClicks, and pass the new value back to - // Streamlit via `Streamlit.setComponentValue`. - this.setState( - prevState => ({ numClicks: prevState.numClicks + 1 }), - () => Streamlit.setComponentValue(this.state.numClicks) - ) + const onClicked = (): void => { + setNumClicks((prevNumClicks) => prevNumClicks + 1) } /** Focus handler for our "Click Me!" button. */ - private _onFocus = (): void => { - this.setState({ isFocused: true }) + const onFocus = (): void => { + setIsFocused(true) } /** Blur handler for our "Click Me!" button. */ - private _onBlur = (): void => { - this.setState({ isFocused: false }) + const onBlur = (): void => { + setIsFocused(false) } + + // Show a button and some text. + // When the button is clicked, we'll increment our "numClicks" state + // variable, and send its new value back to Streamlit, where it'll + // be available to the Python program. + return ( + + Hello, {name}!   + + + ) } // "withStreamlitConnection" is a wrapper function. It bootstraps the From 4ffdf681c9e10708b26abd30916098db6c864513 Mon Sep 17 00:00:00 2001 From: raethlein Date: Thu, 5 Dec 2024 00:10:12 +0100 Subject: [PATCH 2/5] Set component's frame height --- template/my_component/frontend/src/MyComponent.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/template/my_component/frontend/src/MyComponent.tsx b/template/my_component/frontend/src/MyComponent.tsx index 2a372eb5..92efbeae 100644 --- a/template/my_component/frontend/src/MyComponent.tsx +++ b/template/my_component/frontend/src/MyComponent.tsx @@ -29,6 +29,10 @@ function MyComponent({ args, disabled, theme }: ComponentProps): ReactElement { Streamlit.setComponentValue(numClicks) }, [numClicks]) + useEffect(() => { + Streamlit.setFrameHeight() + }, [style, numClicks]) + /** Click handler for our "Click Me!" button. */ const onClicked = (): void => { setNumClicks((prevNumClicks) => prevNumClicks + 1) From 25577b3f182b575268667428f6b0eddfeb246b7e Mon Sep 17 00:00:00 2001 From: raethlein Date: Thu, 5 Dec 2024 00:15:59 +0100 Subject: [PATCH 3/5] Apply feedback --- .../my_component/frontend/src/MyComponent.tsx | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/template/my_component/frontend/src/MyComponent.tsx b/template/my_component/frontend/src/MyComponent.tsx index 92efbeae..44a5f8f6 100644 --- a/template/my_component/frontend/src/MyComponent.tsx +++ b/template/my_component/frontend/src/MyComponent.tsx @@ -3,26 +3,25 @@ import { withStreamlitConnection, ComponentProps, } from "streamlit-component-lib" -import React, { useEffect, useState, ReactElement } from "react" +import React, { useCallback, useEffect, useMemo, useState, ReactElement } from "react" /** - * This is a React-based component template. The `render()` function is called - * automatically when your component should be re-rendered. + * This is a React-based component template. The passed props are coming from the + * Streamlit library. Your custom args can be accessed via the `args` props. */ function MyComponent({ args, disabled, theme }: ComponentProps): ReactElement { const { name } = args const [isFocused, setIsFocused] = useState(false) - const [style, setStyle] = useState({}) const [numClicks, setNumClicks] = useState(0) - useEffect(() => { - if (!theme) return + const style: React.CSSProperties = useMemo(() => { + if (!theme) return {} // Use the theme object to style our button border. Alternatively, the // theme style is defined in CSS vars. const borderStyling = `1px solid ${isFocused ? theme.primaryColor : "gray"}` - setStyle({ border: borderStyling, outline: borderStyling }) + return { border: borderStyling, outline: borderStyling } }, [theme, isFocused]) useEffect(() => { @@ -34,19 +33,19 @@ function MyComponent({ args, disabled, theme }: ComponentProps): ReactElement { }, [style, numClicks]) /** Click handler for our "Click Me!" button. */ - const onClicked = (): void => { + const onClicked = useCallback((): void => { setNumClicks((prevNumClicks) => prevNumClicks + 1) - } + }, [setNumClicks]) /** Focus handler for our "Click Me!" button. */ - const onFocus = (): void => { + const onFocus = useCallback((): void => { setIsFocused(true) - } + }, [setIsFocused]) /** Blur handler for our "Click Me!" button. */ - const onBlur = (): void => { + const onBlur = useCallback((): void => { setIsFocused(false) - } + }, [setIsFocused]) // Show a button and some text. // When the button is clicked, we'll increment our "numClicks" state From 6fe2db06b76c36ea27b39ab96e22160ab75cdb81 Mon Sep 17 00:00:00 2001 From: raethlein Date: Thu, 5 Dec 2024 00:24:50 +0100 Subject: [PATCH 4/5] Update cookiecutter template --- .../frontend-react/src/MyComponent.tsx | 114 ++++++++---------- 1 file changed, 50 insertions(+), 64 deletions(-) diff --git a/cookiecutter/{{ cookiecutter.package_name }}/{{ cookiecutter.import_name }}/frontend-react/src/MyComponent.tsx b/cookiecutter/{{ cookiecutter.package_name }}/{{ cookiecutter.import_name }}/frontend-react/src/MyComponent.tsx index f68b04f1..44a5f8f6 100644 --- a/cookiecutter/{{ cookiecutter.package_name }}/{{ cookiecutter.import_name }}/frontend-react/src/MyComponent.tsx +++ b/cookiecutter/{{ cookiecutter.package_name }}/{{ cookiecutter.import_name }}/frontend-react/src/MyComponent.tsx @@ -1,84 +1,70 @@ import { Streamlit, - StreamlitComponentBase, withStreamlitConnection, + ComponentProps, } from "streamlit-component-lib" -import React, { ReactNode } from "react" - -interface State { - numClicks: number - isFocused: boolean -} +import React, { useCallback, useEffect, useMemo, useState, ReactElement } from "react" /** - * This is a React-based component template. The `render()` function is called - * automatically when your component should be re-rendered. + * This is a React-based component template. The passed props are coming from the + * Streamlit library. Your custom args can be accessed via the `args` props. */ -class MyComponent extends StreamlitComponentBase { - public state = { numClicks: 0, isFocused: false } +function MyComponent({ args, disabled, theme }: ComponentProps): ReactElement { + const { name } = args - public render = (): ReactNode => { - // Arguments that are passed to the plugin in Python are accessible - // via `this.props.args`. Here, we access the "name" arg. - const name = this.props.args["name"] + const [isFocused, setIsFocused] = useState(false) + const [numClicks, setNumClicks] = useState(0) - // Streamlit sends us a theme object via props that we can use to ensure - // that our component has visuals that match the active theme in a - // streamlit app. - const { theme } = this.props - const style: React.CSSProperties = {} + const style: React.CSSProperties = useMemo(() => { + if (!theme) return {} - // Maintain compatibility with older versions of Streamlit that don't send - // a theme object. - if (theme) { - // Use the theme object to style our button border. Alternatively, the - // theme style is defined in CSS vars. - const borderStyling = `1px solid ${ - this.state.isFocused ? theme.primaryColor : "gray" - }` - style.border = borderStyling - style.outline = borderStyling - } + // Use the theme object to style our button border. Alternatively, the + // theme style is defined in CSS vars. + const borderStyling = `1px solid ${isFocused ? theme.primaryColor : "gray"}` + return { border: borderStyling, outline: borderStyling } + }, [theme, isFocused]) - // Show a button and some text. - // When the button is clicked, we'll increment our "numClicks" state - // variable, and send its new value back to Streamlit, where it'll - // be available to the Python program. - return ( - - Hello, {name}!   - - - ) - } + useEffect(() => { + Streamlit.setComponentValue(numClicks) + }, [numClicks]) + + useEffect(() => { + Streamlit.setFrameHeight() + }, [style, numClicks]) /** Click handler for our "Click Me!" button. */ - private onClicked = (): void => { - // Increment state.numClicks, and pass the new value back to - // Streamlit via `Streamlit.setComponentValue`. - this.setState( - prevState => ({ numClicks: prevState.numClicks + 1 }), - () => Streamlit.setComponentValue(this.state.numClicks) - ) - } + const onClicked = useCallback((): void => { + setNumClicks((prevNumClicks) => prevNumClicks + 1) + }, [setNumClicks]) /** Focus handler for our "Click Me!" button. */ - private _onFocus = (): void => { - this.setState({ isFocused: true }) - } + const onFocus = useCallback((): void => { + setIsFocused(true) + }, [setIsFocused]) /** Blur handler for our "Click Me!" button. */ - private _onBlur = (): void => { - this.setState({ isFocused: false }) - } + const onBlur = useCallback((): void => { + setIsFocused(false) + }, [setIsFocused]) + + // Show a button and some text. + // When the button is clicked, we'll increment our "numClicks" state + // variable, and send its new value back to Streamlit, where it'll + // be available to the Python program. + return ( + + Hello, {name}!   + + + ) } // "withStreamlitConnection" is a wrapper function. It bootstraps the From 0755480b527ef171d40d469d2439f3025492afcb Mon Sep 17 00:00:00 2001 From: raethlein Date: Thu, 5 Dec 2024 00:35:10 +0100 Subject: [PATCH 5/5] Add comment and remove unnecessary deps --- .../frontend-react/src/MyComponent.tsx | 10 ++++++---- template/my_component/frontend/src/MyComponent.tsx | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/cookiecutter/{{ cookiecutter.package_name }}/{{ cookiecutter.import_name }}/frontend-react/src/MyComponent.tsx b/cookiecutter/{{ cookiecutter.package_name }}/{{ cookiecutter.import_name }}/frontend-react/src/MyComponent.tsx index 44a5f8f6..4f73995e 100644 --- a/cookiecutter/{{ cookiecutter.package_name }}/{{ cookiecutter.import_name }}/frontend-react/src/MyComponent.tsx +++ b/cookiecutter/{{ cookiecutter.package_name }}/{{ cookiecutter.import_name }}/frontend-react/src/MyComponent.tsx @@ -28,24 +28,26 @@ function MyComponent({ args, disabled, theme }: ComponentProps): ReactElement { Streamlit.setComponentValue(numClicks) }, [numClicks]) + // setFrameHeight should be called on first render and evertime the size might change (e.g. due to a DOM update). + // Adding the style and theme here since they might effect the visual size of the component. useEffect(() => { Streamlit.setFrameHeight() - }, [style, numClicks]) + }, [style, theme]) /** Click handler for our "Click Me!" button. */ const onClicked = useCallback((): void => { setNumClicks((prevNumClicks) => prevNumClicks + 1) - }, [setNumClicks]) + }, []) /** Focus handler for our "Click Me!" button. */ const onFocus = useCallback((): void => { setIsFocused(true) - }, [setIsFocused]) + }, []) /** Blur handler for our "Click Me!" button. */ const onBlur = useCallback((): void => { setIsFocused(false) - }, [setIsFocused]) + }, []) // Show a button and some text. // When the button is clicked, we'll increment our "numClicks" state diff --git a/template/my_component/frontend/src/MyComponent.tsx b/template/my_component/frontend/src/MyComponent.tsx index 44a5f8f6..4f73995e 100644 --- a/template/my_component/frontend/src/MyComponent.tsx +++ b/template/my_component/frontend/src/MyComponent.tsx @@ -28,24 +28,26 @@ function MyComponent({ args, disabled, theme }: ComponentProps): ReactElement { Streamlit.setComponentValue(numClicks) }, [numClicks]) + // setFrameHeight should be called on first render and evertime the size might change (e.g. due to a DOM update). + // Adding the style and theme here since they might effect the visual size of the component. useEffect(() => { Streamlit.setFrameHeight() - }, [style, numClicks]) + }, [style, theme]) /** Click handler for our "Click Me!" button. */ const onClicked = useCallback((): void => { setNumClicks((prevNumClicks) => prevNumClicks + 1) - }, [setNumClicks]) + }, []) /** Focus handler for our "Click Me!" button. */ const onFocus = useCallback((): void => { setIsFocused(true) - }, [setIsFocused]) + }, []) /** Blur handler for our "Click Me!" button. */ const onBlur = useCallback((): void => { setIsFocused(false) - }, [setIsFocused]) + }, []) // Show a button and some text. // When the button is clicked, we'll increment our "numClicks" state