Communication between Lightning Components

Audience: Users that want to create interactive applications.

Level: Intermediate

Prerequisite: Read the Communication in Lightning Apps article.


Communication Between Components

When creating interactive Lightning Apps (App) with multiple components, you may need your components to share information with each other and rely on that information to control their execution, share progress in the UI, trigger a sequence of operations, etc.

To accomplish that, Lightning components can communicate using the App State. The App State is composed of all attributes defined within each component’s __init__ method e.g anything attached to the component with self.x = y.

All attributes of all LightningWork (Work) components are accessible in the LightningFlow (Flow) components in real-time.

By design, the Flows communicate to all Works within the application. However, Works can’t communicate with each other directly, they must use Flows as a proxy to communicate.

Once a Work is running, any updates to the Work’s state is automatically communicated to the Flow, as a delta (using DeepDiff). The state communication isn’t bi-directional, communication is only done from Work to Flow.

Internally, the App is alternatively collecting deltas sent from all the registered Works and/or UI, and running the root Flow run method of the App.


Communication from LightningWork to LightningFlow

LightningFlow (Flow) can access their children’s LightningWork (Work) state.

When a running Work attribute gets updated inside its method (separate process locally or remote machine), the app re-executes Flow’s run method once it receives the state update from the Work.

Here’s an example to better understand communication from Work to Flow.

The WorkCounter increments a counter until 1 million and the Flow prints the work counter.

As the Work is running its own process, its state changes are sent to the Flow which contains the latest value of the counter.

import lightning as L


class WorkCounter(L.LightningWork):
    def __init__(self):
        super().__init__(parallel=True)
        self.counter = 0

    def run(self):
        for _ in range(int(10e6)):
            self.counter += 1


class Flow(L.LightningFlow):
    def __init__(self):
        super().__init__()
        self.w = WorkCounter()

    def run(self):
        self.w.run()
        print(self.w.counter)


app = L.LightningApp(Flow())

A delta sent from the Work to the Flow looks like this:

{"values_changed": {"root['works']['w']['vars']['counter']": {"new_value": 425}}}

Here is the associated illustration:

Mechanism showing how delta are sent.

Here’s another example that is slightly different. Here we define a Flow and Work, where the Work increments a counter indefinitely and the Flow prints its state which contain the Work.

You can easily check the state of your entire app as follows:

from lightning.app import LightningWork, LightningFlow, LightningApp
from lightning.app.utilities.app_helpers import pretty_state


class Work(LightningWork):
    def __init__(self):
        super().__init__(cache_calls=False)
        # Attributes are registered automatically in the state.
        self.counter = 0

    def run(self):
        # Incrementing an attribute gets reflected in the `Flow` state.
        self.counter += 1


class Flow(LightningFlow):
    def __init__(self):
        super().__init__()
        self.w = Work()

    def run(self):
        if self.w.has_started:
            print(f"State: {pretty_state(self.state)} \n")
        self.w.run()


app = LightningApp(Flow())

Run the app with:

lightning run app docs/source/core_api/lightning_app/app.py

And here’s the output you get when running the App using the Lightning CLI:

INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view
State: {'works': {'w': {'vars': {'counter': 1}}}}
State: {'works': {'w': {'vars': {'counter': 2}}}}
State: {'works': {'w': {'vars': {'counter': 3}}}}
State: {'works': {'w': {'vars': {'counter': 3}}}}
State: {'works': {'w': {'vars': {'counter': 4}}}}
...

Communication from LightningFlow to LightningWork

Communication from the LightningFlow (Flow) to the LightningWork (Work) while running isn’t supported yet. If your application requires this feature, please open an issue on Github.

Here’s an example of what would happen if you try to have the Flow communicate with the Work:

import lightning as L
from time import sleep


class WorkCounter(L.LightningWork):
    def __init__(self):
        super().__init__(parallel=True)
        self.counter = 0

    def run(self):
        while True:
            sleep(1)
            print(f"Work {self.counter}")


class Flow(L.LightningFlow):
    def __init__(self):
        super().__init__()
        self.w = WorkCounter()

    def run(self):
        self.w.run()
        sleep(1)
        print(f"Flow {self.w.counter}")
        self.w.counter += 1


app = L.LightningApp(Flow())

As you can see, there is a divergence between the values within the Work and the Flow.

Flow 0
Flow 1
Flow 2
Flow 3
Work 0
Flow 4
Work 0
Flow 5
Work 0
Flow 6
Work 0
Flow 7
Work 0
Flow 8
Work 0
Flow 9
Work 0
Flow 10