{"id":13411523,"url":"https://github.com/edelvalle/reactor","last_synced_at":"2026-01-17T06:18:58.108Z","repository":{"id":34652828,"uuid":"182271222","full_name":"edelvalle/reactor","owner":"edelvalle","description":"Phoenix LiveView but for Django","archived":false,"fork":false,"pushed_at":"2024-03-15T22:27:39.000Z","size":1166,"stargazers_count":612,"open_issues_count":17,"forks_count":28,"subscribers_count":15,"default_branch":"master","last_synced_at":"2024-04-15T08:14:15.128Z","etag":null,"topics":["django","django-channels","liveview","real-time"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/edelvalle.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-04-19T13:46:27.000Z","updated_at":"2024-05-15T23:47:51.796Z","dependencies_parsed_at":"2024-01-06T09:59:30.758Z","dependency_job_id":"a1edb687-53b6-4423-910a-18b18a8ce426","html_url":"https://github.com/edelvalle/reactor","commit_stats":{"total_commits":306,"total_committers":12,"mean_commits":25.5,"dds":0.261437908496732,"last_synced_commit":"6c038e03401b75d026f52811fe0857470bae6214"},"previous_names":[],"tags_count":61,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edelvalle%2Freactor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edelvalle%2Freactor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edelvalle%2Freactor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edelvalle%2Freactor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/edelvalle","download_url":"https://codeload.github.com/edelvalle/reactor/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243618635,"owners_count":20320269,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["django","django-channels","liveview","real-time"],"created_at":"2024-07-30T20:01:14.234Z","updated_at":"2026-01-17T06:18:58.098Z","avatar_url":"https://github.com/edelvalle.png","language":"Python","readme":"# Reactor, a LiveView library for Django\n\nReactor enables you to do something similar to Phoenix framework LiveView using Django Channels.\n\n![TODO MVC demo app](demo.gif)\n\n## What's in the box?\n\nThis is no replacement for VueJS or ReactJS, or any JavaScript but it will allow you use all the potential of Django to create interactive front-ends. This method has its drawbacks because if connection is lost to the server the components in the front-end go busted until connection is re-established. But also has some advantages, as everything is server side rendered the interface comes already with meaningful information in the first request response, you can use all the power of Django template and ORM directly in your component and update the interface in real-time by subscribing to events on the server. If connection is lost or a component crashes, the front-end will have enough information to rebuild their state in the last good known state and continue to operate with the connection is restored.\n\n## Installation and setup\n\nReactor requires Python \u003e=3.9.\n\nInstall reactor:\n\n```bash\npip install django-reactor\n```\n\nReactor makes use of `django-channels`, by default this one uses an InMemory channel layer which is not capable of a real broadcasting, so you might wanna use the Redis one, take a look here: [Channel Layers](https://channels.readthedocs.io/en/latest/topics/channel_layers.html)\n\nAdd `reactor` and `channels` to your `INSTALLED_APPS` before the Django applications so channels can override the `runserver` command.\n\n```python\nINSTALLED_APPS = [\n    'reactor',\n    'channels',\n    ...\n]\n\n...\n\nASGI_APPLICATION = 'project_name.asgi.application'\n```\n\nand modify your `project_name/asgi.py` file like:\n\n```python\nimport os\nos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project_name.settings')\n\nimport django\ndjango.setup()\n\nfrom channels.auth import AuthMiddlewareStack\nfrom channels.routing import ProtocolTypeRouter, URLRouter\nfrom django.core.asgi import get_asgi_application\nfrom reactor.urls import websocket_urlpatterns\n\napplication = ProtocolTypeRouter({\n    'http': get_asgi_application(),\n    'websocket': AuthMiddlewareStack(URLRouter(websocket_urlpatterns))\n})\n```\n\nNote: Reactor since version 2, autoloads any `live.py` file in your applications with the hope to find there Reactor Components so they get registered and can be instantiated.\n\nIn the templates where you want to use reactive components you have to load the reactor static files. So do something like this so the right JavaScript gets loaded:\n\n```html\n{% load reactor %}\n\u003c!doctype html\u003e\n\u003chtml\u003e\n    \u003chead\u003e\n        ... {% reactor_header %}\n    \u003c/head\u003e\n    ...\n\u003c/html\u003e\n```\n\nDon't worry if you put this as early as possible, the scripts are loaded using `\u003cscript defer\u003e` so they will be downloaded in parallel with the html, and when all is loaded they are executed.\n\n## Simple example of a counter\n\nIn your app create a template `x-counter.html`:\n\n```html\n{% load reactor %}\n\u003cdiv {% tag_header %}\u003e\n  {{ amount }}\n  \u003cbutton {% on 'click' 'inc' %}\u003e+\u003c/button\u003e\n  \u003cbutton {% on 'click' 'dec' %}\u003e-\u003c/button\u003e\n  \u003cbutton {% on 'click' 'set_to' amount=0 %}\u003ereset\u003c/button\u003e\n\u003c/div\u003e\n```\n\nAnatomy of a template: each component should be a single root tag and you need to add `{% tag_header %}` to that tag, so a set of attributes that describe the component state are added to that tag.\n\nEach component should have an `id` so the backend knows which instance is this one and a `state` attribute with the necessary information to recreate the full state of the component on first render and in case of re-connection to the back-end.\n\nRender things as usually, so you can use full Django template language, `trans`, `if`, `for` and so on. Just keep in mind that the instance of the component is referred as `this`.\n\nForwarding events to the back-end: Notice that for event binding in-line JavaScript is used on the event handler of the HTML elements. How does this work? When the increment button receives a click event `send(this, 'inc')` is called, `send` is a reactor function that will look for the parent custom component and will dispatch to it the `inc` message, or the `set_to` message and its parameters `{amount: 0}`. The custom element then will send this message to the back-end, where the state of the component will change and then will be re-rendered back to the front-end. In the front-end `morphdom` (just like in Phoenix LiveView) is used to apply the new HTML.\n\nNow let's write the behavior part of the component in `live.py`:\n\n```python\nfrom reactor.component import Component\n\n\nclass XCounter(Component):\n    _template_name = 'x-counter.html'\n\n    amount: int = 0\n\n    async def inc(self):\n        self.amount += 1\n\n    async def dec(self):\n        self.amount -= 1\n\n    async def set_to(self, amount: int):\n        self.amount = amount\n```\n\nLet's now render this counter, expose a normal view that renders HTML, like:\n\n```python\ndef index(request):\n    return render(request, 'index.html')\n```\n\nAnd the index template being:\n\n```html\n{% load reactor %}\n\u003c!doctype html\u003e\n\u003chtml\u003e\n    \u003chead\u003e\n        .... {% reactor_header %}\n    \u003c/head\u003e\n    \u003cbody\u003e\n        {% component 'XCounter' %}\n\n        \u003c!-- or passing an initial state --\u003e\n        {% component 'XCounter' amount=100 %}\n    \u003c/body\u003e\n\u003c/html\u003e\n```\n\nDon't forget to update your `urls.py` to call the index view.\n\n### Persisting the state of the Counter in the URL as a GET parameter\n\nAdd:\n\n```python\n...\n\nclass SearchList(Component):\n  query: str = \"\"\n\n  @classmethod\n  def new(cls, reactor: ReactorMeta, **kwargs):\n    # read the query parameter and initialize the object with that parameter\n    kwargs.setdefault(\"query\", reactor.params.get(\"query\", \"\"))\n    return cls(reactor=reactor, **kwargs)\n\n  async def filter_results(self, query: str):\n    self.query = query\n    # update the query string in the browser\n    self.reactor.params[\"query\"] = query\n...\n```\n\nThis will make that everytime that method gets called the query string on the browser will get updated to include \"query=blahblah\". Never replace the `self.reactor.params`, mutate it instead.\n\nHere is another example, suppose you have a list of items that can be expanded, like nodes in a tree view:\n\n```python\nclass Node(Component):\n    name: str\n    expanded: bool = False\n\n    @classmethod\n    def new(cls, reactor: ReactorMeta, id: str, **kwargs):\n        kwargs[\"expanded\"] = id in reactor.params.get(\"expanded.json\", [])\n        return cls(reactor=reactor, id=id, **kwargs)\n\n    async def toggle_expanded(self):\n        self.expanded = not self.expanded\n        expanded = self.reactor.params.setdefault(\"expanded.json\", [])\n        if self.expanded:\n            expanded.append(self.id)\n        elif self.id in expanded:\n            expanded.remove(self.id)\n```\n\nHere `expanded.json` is a list of the expanded nodes. Notice the `.json` this indicates that the value of this key should be encoded/decoded to/from JSON, so just put there JSON serializable stuff.\n\n## Settings:\n\nDefault settings of reactor are:\n\n```python\n\nfrom reactor.schemas import AutoBroadcast\n\nREACTOR = {\n    \"TRANSPILER_CACHE_SIZE\": 1024,\n    \"USE_HTML_DIFF\": True,\n    \"USE_HMIN\": False,\n    \"BOOST_PAGES\": False,\n    \"TRANSPILER_CACHE_NAME\": \"reactor:transpiler\",\n    \"AUTO_BROADCAST\": AutoBroadcast(\n        # model-a\n        model: bool = False\n        # model-a.1234\n        model_pk: bool = False\n        # model-b.9876.model-a-set\n        related: bool = False\n        # model-b.9876.model-a-set\n        # model-a.1234.model-b-set\n        m2m: bool = False\n        # this is a set of tuples of ('app_label', 'ModelName')\n        # to subscribe for the auto broadcast\n        senders: set[tuple[str, str]] = Field(default_factory=set)\n    ),\n}\n```\n\n-   `TRANSPILER_CACHE_SIZE`: this is the size of an LRU dict used to cache javascript event halder transpilations.\n-   `USE_HTML_DIFF`: when enabled uses `difflib` to create diffs to patch the front-end, reducing bandwidth. If disabled it sends the full HTML content every time.\n-   `REACTOR_USE_HMIN`: when enabled and django-hmin is installed will use it to minified the HTML of the components and save bandwidth.\n-   `AUTO_BROADCAST`: Controls which signals are sent to `Component.mutation` when a model is mutated.\n\n## Back-end APIs\n\n### Template tags and filters of `reactor` library\n\n-   `{% reactor_header %}`: that includes the necessary JavaScript to make this library work. ~10Kb of minified JS, compressed with gz or brotli.\n-   `{% component 'Component' param1=1 param2=2 %}`: Renders a component by its name and passing whatever parameters you put there to the `XComponent.new` method that constructs the component instance.\n-   `{% on 'click' 'event_handler' param1=1 param2=2 %}`: Binds an event handler with paramters to some event. Look at [Event binding in the front-end](#event-binding-in-the-front-end)\n-   `cond`: Allows simple conditional presence of a string: `{% cond {'hidden': is_hidden } %}`.\n-   `class`: Use it to handle conditional classes: `\u003cdiv {% class {'nav_bar': True, 'hidden': is_hidden} %}\u003e\u003c/div\u003e`.\n\n## Component live cycle\n\n### Initialization \u0026 Rendering\n\nThis happens when in a \"normal\" template you include a component.\n\n```html\n{% component 'Component' param1=1 param2=2 %}\n```\n\nThis passes those parameter there to `Component.new` that should return the component instance and then the component get's rendered in the template and is sent to the client.\n\n### Joins\n\nWhen the component arrives to the front-end it \"joins\" the backend. Sends it's serialized state to the backend which rebuilds the component and calls `Component.joined`.\n\nAfter that the component is rendered and the render is sent to the front-end. Why? Because could be that the client was online while some change in the backend happened and the component needs to be updated.\n\n### User events\n\nWhen a component or its parent has joined it can send user events to the client. Using the `on` template tag, this events are sent to the backend and then the componet is rendered again.\n\n### Subscriptions\n\nEvery time a component joins or responds to an event the `Componet._subscriptions` set is reviewed to check if the component subscribes or not to some channel.\n\n-   In case a mutation in a model occurs `Component.mutation(channel: str, action: reactor.auto_broadcast.Action, instance: Model)` will be called.\n-   In case you broadcast a message using `reactor.component.broadcast(channel, **kwargs)` this message will be sent to any component subscribed to `channel` using the method `Component.notification(channel, **kwargs)`.\n\n### Disconnection\n\nIf the component is destroyed using the `Component.destroy` or just desapears from the front-end it is removed from the backend. If the the websocket closes all components in that connection are removed from the backend and the state of those componets stay just in the front-end in the seralized form awaiting for the front-end to join again.\n\n#### Component API\n\nEach component is a Pydantic model so it can serialize itself. I would advice not to mess with the `__init__` method.\nInstead use the class method `new` to create the instance.\n\n##### Rendering\n\n-   `_template_name`: Contains the path of the template of the component.\n-   `_exclude_fields`: (default: `{\"user\", \"reactor\"}`) Which fields to exclude from state serialization during rendering\n\n#### Subscriptions\n\n-   `_subscriptions`: (default: `set()`) Defines which channels is this component subscribed to.\n-   `mutation(channel, action, instance)` Called when autobroadcast is enabled and a model you are subscribed to changes.\n-   `notification(channel, **kwargs)` Called when `reactor.component.broadcast(channel, **kwargs)` is used to send an arbitrary notification to components.\n\n#### Actions\n\n-   `destroy()`: Removes the component from the interface.\n-   `focus_on(selector: str)`: Makes the front-end look for that `selector` and run `.focus()` on it.\n-   `skip_render()`: Prevents the component from being rendered once.\n-   `send_render()`: Send a signal to request render the component ahead of time.\n-   `dom(_action: DomAction, id: str, component_or_template, **kwargs)`: Can append, prepend, insert befor or after certain HTMLElement ID in the dom, the component or template, rendered using the `kwargs`.\n-   `freeze()`: Prevents the component from being rendered again.\n-   `deffer(f, *args, **kwargs)`: Send a message to the current event to be executed after the current function is executed.\n-   `reactor.redirect_to(to, **kwargs)`: Changes the URL of the front-end and triggers a page load for that new URL\n-   `reactor.replace_to(to, **kwargs)`: Changes the current URL for another one.\n-   `reactor.push_to(to, **kwargs)`: Changs the URL of the front-end adding a new history entry but does not fetch the new URL from the backend.\n-   `reactor.send(_channel: str, _topic: str, **kwargs)`: Sends a message over a channel.\n\n## Front-end APIs\n\n-   `reactor.send(element, name, args)`: Sends a reactor user event to `element`, where `name` is the event handler and `args` is a JS object containing the implicit arguments of the call.\n\n### Event binding in the front-end\n\nLook at this:\n\n```html\n  \u003cbutton {% on \"click.prevent\" \"submit\" %}\u003eSubmit\u003c/button\u003e\n```\n\nSyntax: {% on \u003cevent-and-modifiers\u003e \u003cevent-handler\u003e [\u003cevent-handler-arguments-as-kwargs\u003e] %}\nThe format for event and modifiers is `@\u003cevent\u003e[.modifier1][.modifier2][.modifier2-argument1][.modifier2-argument2]`\n\nExamples:\n\n-   `{% on \"click.ctrl\" \"decrement\" %}\u003e`: Clicking with Ctrl pressed calls \"decrement\".\n-   `{% on \"click\" \"increment\" amount=1 %}\u003e`: Clicking calls \"increment\" passing `amount=1` as argument.\n\nMisc:\n\n-   `event`: is the name of the HTMLElement event: `click`, `blur`, `change`, `keypress`, `keyup`, `keydown`...\n-   `modifier`: can be concatenated after the event name and represent actions or conditions to be met before the event execution. This is very similar as [how VueJS does event binding](https://vuejs.org/v2/guide/events.html#Event-Modifiers):\n\n    Available modifiers are:\n\n    -   `inlinejs`: takes the next \"event handler\" argument as literal JS code.\n    -   `prevent`: calls `event.preventDefault()`\n    -   `stop`: calls `event.StopPropagation()`\n    -   `ctrl`, `alt`, `shift`, `meta`: continues processing the event if any of those keys is pressed\n    -   `debounce`: debounces the event, it needs a delay in milliseconds. Example: `keypress.debounce.100`.\n    -   `key.\u003ckeycode\u003e`: continues processing the event if the key with `keycode` is pressed\n    -   `enter`: alias for `key.enter`\n    -   `tab`: alias for `key.tab`\n    -   `delete`: alias for `key.delete`\n    -   `backspace`: alias for `key.backspace`\n    -   `space`: alias for `key. `\n    -   `up`: alias for `key.arrowup`\n    -   `down`: alias for `key.arrowdown`\n    -   `left`: alias for `key.arrowleft`\n    -   `right`: alias for `key.arrowright`\n\n#### Event arguments\n\nReactor sends the implicit arguments you pass on the `on` template tag, but also sends implicit arguments.\nThe implicit arguments are taken from the `form` the element handling the event is in or from the whole component otherwise.\n\nExamples:\n\nHere any event inside that component will have the implicit argument `x` being send to the backend.\n\n```html\n\u003cdiv {% tag-header %}\u003e\n  \u003cinput name=\"x\"/\u003e\n  \u003cbutton {% on \"click\" \"submit\" %}\u003eSend\u003c/button\u003e\n\u003c/div\u003e\n```\n\nHere any `submit_x` will send `x`, and `submit_y` will send just `y`.\n\n```html\n\u003cdiv {% tag-header %}\u003e\n  \u003cinput name=\"x\"/\u003e\n  \u003cbutton {% on \"click\" \"submit_x\" %}\u003eSend\u003c/button\u003e\n  \u003cform\u003e\n    \u003cinput name=\"y\"/\u003e\n    \u003cbutton {% on \"click.prevent\" \"submit_y\" %}\u003eSend\u003c/button\u003e\n  \u003c/form\u003e\n\u003c/div\u003e\n```\n\n### Event handlers in the back-end\n\nGiven:\n\n```html\n\u003cbutton {% on 'click 'inc' amount=2 %}\u003eIncrement\u003c/button\u003e\n```\n\nYou will need an event handler in that component in the back-end:\n\n```python\nasync def inc(self, amount: int):\n    ...\n```\n\nIt is good if you annotate the signature so the types are validated and converted if they have to be.\n\n## More complex components\n\nI made a TODO list app using models that signals from the model to the respective channels to update the interface when something gets created, modified or deleted.\n\nThis example contains nested components and some more complex interactions than a simple counter, the app is in the `/tests/` directory.\n\n## Development \u0026 Contributing\n\nClone the repo and create a virtualenv or any other contained environment, get inside the repo directory, build the development environment and the run tests.\n\n```bash\ngit clone git@github.com:edelvalle/reactor.git\ncd reactor\nmake install\nmake test\n```\n\nIf you want to run the included Django project used for testing do:\n\n```bash\nmake\ncd tests\npython manage.py runserver\n```\n\nEnjoy!\n","funding_links":[],"categories":["Front-end frameworks","Web Development"],"sub_categories":["More","Python"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedelvalle%2Freactor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fedelvalle%2Freactor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedelvalle%2Freactor/lists"}