Skip to content

Allow server-side components to send messages to client-side components #975

Description

@rmorshea

Current Situation

Presently, custom client-side components are able to send events back to the server. However, it is not possible to send events from server-side components to client-side ones.

Proposed Actions

Now that custom JS components have the ability to register callbacks with the client this should be technologically feasible. With that said, while users can listen in on particular message types, there is no concept of a message "target". We could achieve this by having message types of the form server-event:<the-target>, but this seems like a bit of a hack. Perhaps we can allow (require?) a nullable target field for this purpose.

Before diving into all those details though, we need to work out exactly what this interface should look like.

I can imagine having an interface similar to:

@component
def example():
    channel = use_channel()

    @use_effect
    async def delayed_message():
        import asyncio
        await asyncio.sleep(5)
        await channel.send({"my": "message"})
        response = await channel.receive()

    return custom_js_component({"channel_id": channel.id})

where custom_js_component would then subscribe to messages of the type channel-message and target channel.id.

Activity

  1. Archmonger commented on May 11, 2023

    @Archmonger
    Contributor
  2. changed the title [-]Allow Server-side components to send events to client-side components[/-] [+]Allow server-side components to send messages to client-side components[/+] on May 11, 2023
  3. Archmonger commented on May 11, 2023

    @Archmonger
    Contributor

    The name use_channel kind of implies that multiple channels and/or channel groups can be defined per component. Here's some other potential names

    • use_data_message
    • use_message
    • use_messenger
    • use_message_listener
    • use_message_signal
    • use_message_subscriber
    • use_signal_listener
    • use_consumer
    • use_data_channel
    • use_data_messenger
    • use_data_transport
    • use_data_dispatcher
    • use_communicator

    The design below would allow for an event-driven architecture when possible, inspired by channels.

    With any design we make, we're going to need to be careful about our asyncio cancellation policy for messengers. This will extend to us thinking deeper about async use_effect.

    from dataclasses import dataclass
    
    from reactpy import component, hooks
    
    
    @dataclass
    class DataMessenger:
        def __init__(self, *args, **kwargs):
            self.args = args or ()
            self.kwargs = kwargs or {}
    
        async def send(self, message):
            """This method typically isn't overridden."""
            ...
    
        async def recieve(self, message):
            """This method typically gets overriden with custom user behavior."""
            ...
    
        async def register(self, ... ):
            """This method typically isn't overridden. Used to register a message handler to a component."""
            ...
    
    
    def use_messenger(messenger: DataMessenger) -> DataMessenger:
        """This is a hook that allows users to communicate to client-side components.
        The given `messenger` will be registered to the current component.
        """
        MESSENGER_CONTEXT: list = hooks.use_context(...)
        
        # A given `Messenger` should persist throughout the entire lifecycle of a component
        if messenger not in MESSENGER_CONTEXT:  # Not a usable implementation, but you get the point
            messenger.register(...)
    
        # Update `args`/`kwargs` on each component re-render.
        else:
            messenger.args = MESSENGER_CONTEXT[0].args
            messenger.kwargs = MESSENGER_CONTEXT[0].kwargs
    
        # Give the user access to the registered messenger, if he wants to do some shenanigans within `use_effect`.
        return messenger
    
    
    @component
    def example():
        # If the user only wants to be event driven, then he won't need to save a `messenger = ...` variable
        messenger = use_messenger(DataMessenger(example_value=1))
    
        @hooks.use_effect
        async def send_message():
            # Using `messenger` methods here would rely on our half-baked async `use_effect`
            # I think this might be an argument to fleshing out async effects
            await messenger.send("Hello, World!")
  4. rmorshea commented on May 11, 2023

    @rmorshea
    CollaboratorAuthor

    The name use_channel kind of implies that multiple channels and/or channel groups can be defined per component

    I think this is true. One could imagine a server-side component that constructs several channels in order to communicate with different client-side components. use_messenger would be fine too though.

  5. Archmonger commented on May 11, 2023

    @Archmonger
    Contributor

    If we're allowing support for multiple communicators, imo that narrows it down to the following:

    • use_channel
    • use_data_channel
    • use_messenger
    • use_data_messenger
    • use_communicator
    • use_bridge

    I updated the example above to better outline how persistence of DataMessenger can be achieved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions