• Docs >
  • Communicate Between React and Lightning
Shortcuts

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;