Synchronize dropdown values across React-hosted multiple iframed dash apps

Hi All,

I have built a react app, with 2 iframes, each iframe host a dash app.
Each dash app have dropdowns or filters.
I want to synchronize the dropdowns across the dash apps using postMessage API of the react.

Till now i am able to record the filter value of the originating dash app(dash app 1), send the message to react via postMessage and broadcast to all the other dash apps. Lastly the other dash apps are receiving the message, extracting the filter value and tries to change the filter of the dash app(dash app 2).

But only the inner Html seems to change not the filter UI of the dash.

This is the react App.js

import React, { useEffect } from 'react';

function App() {
  useEffect(() => {
    const handleMessage = (event) => {
      const allowedOrigins = ['http://localhost:8050', 'http://localhost:8051'];
      if (!allowedOrigins.includes(event.origin)) return;

      try {
        const data = event.data;

        if (data.type === 'filterChange') {

          // Broadcast to other iframes except the sender
          const iframes = document.getElementsByTagName('iframe');
          Array.from(iframes).forEach(iframe => {
            if (iframe.contentWindow !== event.source) {
              iframe.contentWindow.postMessage({
                type: 'filterChange',
                value: data.value,
                origin: event.origin
              }, '*');
            }
          });
        }
      } catch (error) {
        console.error('Error processing message:', error);
      }
    };

    window.addEventListener('message', handleMessage);

    return () => window.removeEventListener('message', handleMessage);
  }, []);

  return (
    <div>
      <h1>React App</h1>
      <div style={{ display: 'flex' }}>
        <iframe
          src="http://localhost:8050"
          width="600"
          height="400"
          title="Dash App 1"
          style={{ marginRight: '20px' }}
        ></iframe>
        <iframe
          src="http://localhost:8051"
          width="600"
          height="400"
          title="Dash App 2"
        ></iframe>
      </div>
    </div>
  );
}

export default App;

Same code for dash app 1 and dash app 2, with different ports at last line

from dash import Dash, dcc, html
from dash.dependencies import Input, Output, ClientsideFunction

app = Dash(__name__)

# Define the layout with the dropdown and message area
app.layout = html.Div([
    html.Div(id='heading', children='Dash App 1'),  # Add this to distinguish the apps
    html.Div(id='out-message', children=''),  # Placeholder for the out-message
    dcc.Dropdown(
        id='filter-dropdown',
        options=[
            {'label': 'Option 1', 'value': 'Option 1'},
            {'label': 'Option 2', 'value': 'Option 2'},
            {'label': 'Option 3', 'value': 'Option 3'}
        ],
        value='Option 1'  # Set initial value
    )
])

# Clientside callback to sync the dropdown value based on messages from React app
app.clientside_callback(
    ClientsideFunction(
        namespace='clientside',
        function_name='syncFilters'
    ),
    Output('filter-dropdown', 'value'),
    Input('filter-dropdown', 'value')
)

if __name__ == '__main__':
    app.run_server(debug=True, port=8050)  # Change the port here for Dash App 2

Clientside.js function
Put this code under assets/clientside.js

window.dash_clientside = Object.assign({}, window.dash_clientside, {
    clientside: {
        syncFilters: function(value) {
            // Send message to parent (React) when dropdown changes
            if (value) {
                window.parent.postMessage({
                    type: 'filterChange',
                    value: value
                }, '*');
            }

            // Listen for messages from parent (React app)
            window.addEventListener('message', function(event) {
                // Ensure the message is coming from a valid source (React app)
                if (event.origin !== 'http://localhost:3000') return;  // React app's origin

                if (event.data.type === 'filterChange') {
                    // Update the dropdown value in Dash
                    const dropdown = document.getElementById('filter-dropdown');
                    const outMessage = document.getElementById('out-message');

                    // Update the out-message div
                    outMessage.innerHTML = event.data.value;

                    // Now, update the dropdown value and trigger a change event
                    dropdown.value = event.data.value;

                    // Return the updated value for Dash callback
                    return event.data.value;
                }

                return window.dash_clientside.no_update;
            });

            // Return the current dropdown value to handle user input changes
            return value;
        }
    }
});

the text seems to change according to the selected filter in other dash app, but i dont understand why the filter value is not changing.

Please help me. This is important.

This is a great community, it has helped me a lot.
Thank you.

Hello @Abhi1214,

Welcome to the community!

The event listeners cannot interact with dash components directly, as they are not within context of a callback.

However, with dash_clientside.set_props you can have the event listener push updates to the dash eco system and thus update anything you desire.

2 Likes

Hi thank you for the reply,
It did worked but now it is listening for the updates indefinitely.


this is the screenshot of the console.log

Edit: It is actually creating a feedback loop now. If dash app 1 updates its own dropdown, a post messages to react host app is sent, then react host broadcast the message to other dash app other than dash app 1, now dash app 2 on receiving the message initiates a callback via dash_clientside.set_props(). Now what happens? Dash app 2 sends a seemingly new post message to react. So on.

1 Like

Maybe also send a timestamp of the last altered change? If the timestamp is the same as when the last message was sent from the origin point, then it doesn’t update the callback.

Actually, doesn’t the message automatically have the origin point attached? You can store that maybe in the window as a variable, and update then make sure to not broadcast the change back to that place.

Also, I’m not sure that you need the messages anymore. Assuming each iframe is a similar origin point, you should be able to access the frames window and call the set_props from that dash_clientside.