Connect React to a Lightning app¶
Audience: Users who already have a react app and want to connect it to a Lightning app.
pre-requisites: Make sure you already have a react app you want to connect.
Difficulty level: intermediate.
Example code¶
To illustrate how to connect a React app and a lightning App, we’ll be using the example_app.py file which lightning_app init react-ui created:
# example_app.py
from pathlib import Path
from lightning.app import LightningApp, LightningFlow, frontend
class YourComponent(LightningFlow):
def __init__(self):
super().__init__()
self.message_to_print = "Hello World!"
self.should_print = False
def configure_layout(self):
return frontend.StaticWebFrontend(Path(__file__).parent / "ui/dist")
class HelloLitReact(LightningFlow):
def __init__(self):
super().__init__()
self.counter = 0
self.react_ui = YourComponent()
def run(self):
if self.react_ui.should_print:
print(f"{self.counter}: {self.react_ui.message_to_print}")
self.counter += 1
def configure_layout(self):
return [{"name": "React UI", "content": self.react_ui}]
app = LightningApp(HelloLitReact())
and the App.tsx file also created by lightning_app init react-ui:
// App.tsx
import { Button } from "@mui/material";
import { TextField } from "@mui/material";
import Box from "@mui/material/Box";
import { ChangeEvent } from "react";
import cloneDeep from "lodash/cloneDeep";
import "./App.css";
import { useLightningState } from "./hooks/useLightningState";
function App() {
const { lightningState, updateLightningState } = useLightningState();
const counter = lightningState?.vars.counter;
const handleClick = async () => {
if (lightningState) {
const newLightningState = cloneDeep(lightningState);
newLightningState.flows.react_ui.vars.should_print = !newLightningState.flows.react_ui.vars.should_print;
updateLightningState(newLightningState);
}
};
const handleTextField = async (event: ChangeEvent<HTMLInputElement>) => {
if (lightningState) {
const newLightningState = cloneDeep(lightningState);
newLightningState.flows.react_ui.vars.message_to_print = event.target.value;
updateLightningState(newLightningState);
}
};
return (
<div className="App">
<div className="wrapper">
<div>
<Button variant="text" onClick={() => handleClick()}>
<h2>
{lightningState?.["flows"]?.["react_ui"]?.["vars"]?.["should_print"] ? "Stop printing" : "Start Printing"}
</h2>
</Button>
</div>
<Box
component="form"
sx={{
"& .MuiTextField-root": { m: 1, width: "25ch" },
}}
noValidate
autoComplete="off"
>
<div>
<TextField
defaultValue="Hello World!"
onChange={handleTextField}
helperText="Message to be printed in your terminal"
/>
</div>
<div>
<h2>Total number of prints in your terminal: {counter}</h2>
</div>
</Box>
</div>
</div>
);
}
export default App;
Connect the component to the react UI¶
The first step is to connect the dist folder of the react app using StaticWebFrontend:
# example_app.py
from pathlib import Path
from lightning.app import LightningApp, LightningFlow, frontend
class YourComponent(LightningFlow):
def __init__(self):
super().__init__()
self.message_to_print = "Hello World!"
self.should_print = False
def configure_layout(self):
return frontend.StaticWebFrontend(Path(__file__).parent / "ui/dist")
class HelloLitReact(LightningFlow):
def __init__(self):
super().__init__()
self.counter = 0
self.react_ui = YourComponent()
def run(self):
if self.react_ui.should_print:
print(f"{self.counter}: {self.react_ui.message_to_print}")
self.counter += 1
def configure_layout(self):
return [{"name": "React UI", "content": self.react_ui}]
app = LightningApp(HelloLitReact())
the dist folder must contain an index.html file which is generated by the compilating command yarn build which we’ll explore later.
Connect component to the root flow¶
Next, connect your component to the root flow. Display the react app on the tab of your choice using configure_layout:
# example_app.py
from pathlib import Path
from lightning.app import LightningApp, LightningFlow, frontend
class YourComponent(LightningFlow):
def __init__(self):
super().__init__()
self.message_to_print = "Hello World!"
self.should_print = False
def configure_layout(self):
return frontend.StaticWebFrontend(Path(__file__).parent / "ui/dist")
class HelloLitReact(LightningFlow):
def __init__(self):
super().__init__()
self.counter = 0
self.react_ui = YourComponent()
def run(self):
if self.react_ui.should_print:
print(f"{self.counter}: {self.react_ui.message_to_print}")
self.counter += 1
def configure_layout(self):
return [{"name": "React UI", "content": self.react_ui}]
app = LightningApp(HelloLitReact())
Connect React and Lightning state¶
At this point, the React app will render in the Lightning app. Test it out!
lightning_app run app example_app.py
However, to make powerful React+Lightning apps, you must also connect the Lightning App state to the react app. These lines enable two-way communication between the react app and the Lightning app.
// App.tsx
import { Button } from "@mui/material";
import { TextField } from "@mui/material";
import Box from "@mui/material/Box";
import { ChangeEvent } from "react";
import cloneDeep from "lodash/cloneDeep";
import "./App.css";
import { useLightningState } from "./hooks/useLightningState";
function App() {
const { lightningState, updateLightningState } = useLightningState();
const counter = lightningState?.vars.counter;
const handleClick = async () => {
if (lightningState) {
const newLightningState = cloneDeep(lightningState);
newLightningState.flows.react_ui.vars.should_print = !newLightningState.flows.react_ui.vars.should_print;
updateLightningState(newLightningState);
}
};
const handleTextField = async (event: ChangeEvent<HTMLInputElement>) => {
if (lightningState) {
const newLightningState = cloneDeep(lightningState);
newLightningState.flows.react_ui.vars.message_to_print = event.target.value;
updateLightningState(newLightningState);
}
};
return (
<div className="App">
<div className="wrapper">
<div>
<Button variant="text" onClick={() => handleClick()}>
<h2>
{lightningState?.["flows"]?.["react_ui"]?.["vars"]?.["should_print"] ? "Stop printing" : "Start Printing"}
</h2>
</Button>
</div>
<Box
component="form"
sx={{
"& .MuiTextField-root": { m: 1, width: "25ch" },
}}
noValidate
autoComplete="off"
>
<div>
<TextField
defaultValue="Hello World!"
onChange={handleTextField}
helperText="Message to be printed in your terminal"
/>
</div>
<div>
<h2>Total number of prints in your terminal: {counter}</h2>
</div>
</Box>
</div>
</div>
);
}
export default App;
Component vs App¶
Notice that in this guide, we connected a single react app to a single component.
# example_app.py
from pathlib import Path
from lightning.app import LightningApp, LightningFlow, frontend
class YourComponent(LightningFlow):
def __init__(self):
super().__init__()
self.message_to_print = "Hello World!"
self.should_print = False
def configure_layout(self):
return frontend.StaticWebFrontend(Path(__file__).parent / "ui/dist")
class HelloLitReact(LightningFlow):
def __init__(self):
super().__init__()
self.counter = 0
self.react_ui = YourComponent()
def run(self):
if self.react_ui.should_print:
print(f"{self.counter}: {self.react_ui.message_to_print}")
self.counter += 1
def configure_layout(self):
return [{"name": "React UI", "content": self.react_ui}]
app = LightningApp(HelloLitReact())
You can use this single react app for the FULL Lightning app, or you can specify a React app for EACH component.
import lightning as L
class ComponentA(L.LightningFlow):
def configure_layout(self):
return L.app.frontend.StaticWebFrontend(Path(__file__).parent / "react_app_1/dist")
class ComponentB(L.LightningFlow):
def configure_layout(self):
return L.app.frontend.StaticWebFrontend(Path(__file__).parent / "react_app_2/dist")
class HelloLitReact(L.LightningFlow):
def __init__(self):
super().__init__()
self.react_app_1 = ComponentA()
self.react_app_2 = ComponentB()
def configure_layout(self):
tab_1 = {"name": "App 1", "content": self.react_app_1}
tab_2 = {"name": "App 2", "content": self.react_app_2}
return tab_1, tab_2
app = L.LightningApp(HelloLitReact())
This is a powerful idea that allows each Lightning component to have a self-contained web UI.