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.