Communicate Between React and Lightning¶
Audience: Anyone who wants to add a web user interface (UI) written in react to their app.
pre-requisites: Make sure you’ve already connected the React and Lightning app.
Difficulty level: intermediate.
Example code¶
To illustrate how to communicate between a React app and a lightning App, we’ll be using the example_app.py file which lightning init react-ui created:
# example_app.py
from pathlib import Path
import lightning as L
from lightning_app import frontend
class YourComponent(L.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(L.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 = L.LightningApp(HelloLitReact())
and the App.tsx file also created by lightning 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;
Update React –> Lightning app¶
To change the Lightning app from the React app, use updateLightningState.
In this example, when you press Start printing in the React UI, it toggles the react_ui.vars.should_print:
// 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;
By changing that variable in the Lightning app state, it sets react_ui.should_print to True, which enables the Lightning app to print:
# example_app.py
from pathlib import Path
import lightning as L
from lightning_app import frontend
class YourComponent(L.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(L.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 = L.LightningApp(HelloLitReact())
Update React <– Lightning app¶
To change the React app from the Lightning app, use the values from the lightningState.
In this example, when the react_ui.counter` increaes in the Lightning app:
# example_app.py
from pathlib import Path
import lightning as L
from lightning_app import frontend
class YourComponent(L.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(L.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 = L.LightningApp(HelloLitReact())
The React UI updates the text on the screen to reflect the count
// 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;