Create a component with React + TypeScript
In this tutorial, you'll use the official component template to generate a React-based custom component. You'll learn how React integrates with Streamlit's component lifecycle, how to manage the React root, and how to extend the template with React hooks and JSX.
Prerequisites
- The following packages must be installed in your Python environment:
- Node.js 24 or later must be installed. This includes npm, the package manager for JavaScript.
- Familiarity with React basics (components, hooks, JSX) is recommended.
- Familiarity with inline custom components is recommended.
Summary
The template generates a working "Hello, World!" component with a click counter built using React. You'll walk through the generated code, then extend it to render a dynamic list of items from Python data.
Here's a look at what you'll build:
Generate the project
-
Navigate to the directory where you want to create your project and run the cookiecutter generator. The generator will create a new subdirectory for your project.
Terminal -
Follow the interactive prompts. When asked for the framework, select React + Typescript:
TerminalThis creates a
my-react-counter/directory with the following structure:Notice the React template has two frontend source files instead of one:
index.tsxhandles integration with Streamlit's lifecycle, andMyComponent.tsxcontains the React component. This is a convention but not a requirement. You can have a single source file or arbitrarily many source files.
Run the template
You need two terminals running in parallel for development. The following steps use uv run to run commands inside the project's virtual environment. If a .venv doesn't exist yet, uv run creates one automatically.
-
In the first terminal, navigate to the frontend directory, install dependencies, and start the dev build watcher:
Terminal -
In a second terminal, navigate to the project root and run the example app:
Terminal -
View your running app.
You should see a "Hello, World!" heading with a "Click Me!" button. Clicking the button increments a counter that's sent back to Python. An
st.text_inputlets you specify a name which is passed to a second instance of the component.
Understand the generated code
Now that the component is running, walk through each file to understand how it works.
-
Open
my_react_counter/__init__.py:PythonThis file does two things:
-
Registers the component with
st.components.v2.component(). The first argument is a qualified name ("<package-name>.<component-name>") where<package-name>matches thenamefield in the project-levelpyproject.tomland<component-name>matches thenamefield in the component-levelpyproject.toml. The other two arguments point to the frontend assets:jsis a glob pattern that matches the JavaScript bundle produced by Vite.htmlprovides the root<div>that React mounts into. -
Defines a wrapper function (
my_react_counter) that provides a clean API. The wrapper calls the raw component withdata,default, and callback parameters. This pattern is optional but recommended. For more about these parameters, see Component mounting.
-
-
Open
my_react_counter/frontend/src/index.tsx:TypeScriptThis file bridges Streamlit's component lifecycle and React. Because Streamlit calls your
FrontendRendererfunction on every re-render (wheneverdatachanges), the pattern is different from a typical React app:- React root management: You can't create a new React root each time Streamlit calls your function because that would destroy React state on every update. Instead, the
WeakMapstores one root per component instance, keyed byparentElement. On the first call, it creates the root. On subsequent calls, it re-renders into the existing root. This also means multiple instances of the same component in an app each get their own independent React root with their own state. - Module-level scope: Code outside
MyComponentRoot, like theWeakMapdeclaration, runs once when the module loads and is shared across all component instances. If you need one-time global setup. like initializing a third-party library, put it at the module level so it's done once rather than repeated per instance or per re-render. - Passing props:
MyComponentRootextractsdataandsetStateValuefrom Streamlit's args and passes them as React props toMyComponent. This is where you decide which Streamlit args your React component needs. - Cleanup: The returned function unmounts the React root when Streamlit removes the component from the page.
- React root management: You can't create a new React root each time Streamlit calls your function because that would destroy React state on every update. Instead, the
-
Open
my_react_counter/frontend/src/MyComponent.tsx:TypeScriptThis is a standard React functional component:
- Type-safe props:
MyComponentPropsis constructed fromFrontendRendererArgsusing TypeScript'sPickutility type. This ensures thesetStateValueprop is correctly typed for the component's state shape. - React state management: Local UI state (like
isFocused) is managed with React'suseStatehook. This state is purely for the frontend and doesn't need to go back to Python. - Communicating with Python: When the button is clicked,
setStateValue("num_clicks", newNumClicks)sends the count back to Streamlit. This triggers a Python rerun, just like in non-React components. - Streamlit theming: The component uses CSS custom properties like
var(--st-primary-color)directly in inline styles. These properties are provided by Streamlit's theme system and work inside the component's shadow DOM.
- Type-safe props:
Modify the component
Now extend the template to render a dynamic list of items from Python data. This showcases something React does well: declaratively rendering lists with state.
-
In
my_react_counter/frontend/src/MyComponent.tsx, make the following changes to add list rendering and item selection:star Tip
The copy button on the diff code blocks only copy the lines in the final result, not the deleted lines.
TypeScript -
In
my_react_counter/frontend/src/index.tsx, make the following changes to pass the new props:TypeScriptTypeScript -
In
my_react_counter/__init__.py, make the following changes to pass items and handle the new callbacks:PythonThe wrapper now accepts
itemsand anon_item_clickedcallback (defaulting tolambda: None). Inside,on_num_clicks_changeandon_selected_item_changeuse inline lambdas since nothing needs to happen for those events.on_item_clicked_changepasses through the caller's callback so the app can react when an item is clicked. -
To exercise the new list feature, replace the contents of
example.pywith the following:Python -
If
npm run devis still running, the frontend rebuilds automatically. Save your files, refresh your Streamlit app, and view the updated component with a clickable list.
Build for production
When you're ready to share your component, create a production build.
-
Stop the
npm run devwatcher and thestreamlit runprocess by pressingCtrl+Cin each terminal. -
In either terminal, navigate to the frontend directory and build the frontend:
Terminal -
Navigate to the project root and build the Python wheel:
TerminalThis creates a
.whlfile in thedist/directory that you can distribute or upload to PyPI. For publishing instructions, see Publish a Component.
What's next?
- Learn more about the project structure in Package-based components.
- Understand State vs trigger values for interactive components.
- Explore Theming and styling to use Streamlit's CSS custom properties.
- Try the Pure TypeScript tutorial if you want a lighter-weight approach without React.
Still have questions?
Our forums are full of helpful information and Streamlit experts.
