├── .gitignore ├── LICENSE ├── README.md ├── example ├── main.py └── react_components │ ├── common.js │ └── my_component.js ├── pyproject.toml ├── src └── dash_local_react_components │ ├── __init__.py │ ├── _common.py │ ├── _config.py │ ├── _file_generator.py │ ├── _functions.py │ ├── _types.py │ └── _utils.py └── tests ├── __init__.py └── test_utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tomasz Rewak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dash-local-react-components 2 | 3 | A small library that allows for loading react components in Dash applications directly from local project files, without any need for a separate build process. 4 | 5 | # Setting up 6 | 7 | **1. Install the `dash-local-react-components` python package** 8 | 9 | ``` 10 | pip install dash-local-react-components 11 | ``` 12 | 13 | **2. Put `.js` files containing your react.js components into a subdirectory of your project** 14 | 15 | ``` 16 | my-project 17 | ├ main.py 18 | └ public 19 | ├ common.js 20 | └ my_component.js 21 | ``` 22 | 23 | **3. Load your component** 24 | 25 | ```python 26 | from dash import Dash 27 | from dash_local_react_components import load_react_component 28 | 29 | app = Dash() 30 | 31 | # if the component is exported from the my_component.js module as a default export 32 | MyComponent = load_react_component(app, 'public', 'my_component.js') 33 | 34 | # if the component is exported from the my_component.js module as a named export 35 | MyComponent = load_react_component(app, 'public', 'my_component.js', 'MyComponent') 36 | ``` 37 | 38 | **4. Add your component to the layout of your application** 39 | 40 | ```python 41 | app.layout = html.Div([ 42 | html.H1('My application'), 43 | MyComponent(id='my-component', text='Hello world') 44 | ]) 45 | ``` 46 | 47 | # Writing Dash react components 48 | 49 | Writing custom react.js components for Dash applications is similar to writing any other react.js components. Nevertheless, there are some differences that worth noting. 50 | 51 | An example of a Dash react component may look as follows: 52 | 53 | ```js 54 | import React from 'react' 55 | import { add } from './common.js'; 56 | 57 | export default function MyComponent(props) { 58 | const { setProps, count } = props; 59 | 60 | function onClick() { 61 | setProps({ count: add(count, 1) }); 62 | } 63 | 64 | return React.createElement('div', {}, [ 65 | React.createElement('div', {}, `count: ${count}`), 66 | React.createElement('button', { onClick }, 'Increment') 67 | ]); 68 | } 69 | 70 | MyComponent.defaultProps = { 71 | count: 0 72 | }; 73 | 74 | ``` 75 | 76 | First, let's address the elephant in the room. As the `dash-local-react-components` package removes the need for any build steps from the development process, it does not support `.jsx` files (which are not natively supported by web browsers). This means that one cannot use the native react notation like `
my text
`, but rather has to fall back to a little bit more involving syntax: `React.createElement('div', {}, 'my text')` (where the first statement gets compiled into the second during a regular build process). Functionally both expressions are identical, so one can still create fully fledged react components without using the .jsx format, it just requires some getting used to and is not as convenient. 77 | 78 | Apart form syntactic differences (imposed not necessarily by the Dash framework, but rather by this library), there are also some behavioral changes. Namely, each component used within a layout of a Dash application is provided with a special `setProps` property. It's a function that can be used to change values of other properties of the current component. It behaves similarly to the `setState` function of class-based components, but instead of modifying the state, it modifies the properties. You can use this function to manage the parts of the state of the component that should be exposed to python callbacks of your Dash application. 79 | 80 | # Motivation 81 | 82 | The goal of this library is to remove any additional configuration and build steps from the process of creating Dash applications with custom react.js components in order to shorten the development cycle of prototyping small visualizations. 83 | 84 | It is not recommended (until you fully know what you are doing) to use this library in bigger scale projects. The `dash-local-react-components` serves raw .js files in an un-minified and un-bundled way, which means that they require more resources in order to be download and executed. 85 | 86 | But if you are working on a small project and don't want to worry about npms, gulps, webpacks and grunts at this stage - this library might be something for you. Just run your `main.py` and enjoy your fully interactive application. 87 | 88 | # Future improvements 89 | 90 | The most inconvenient part of this library is the fact that it does not support .jsx files. It's a limitation of the current implementation, not the general design, though. It would be possible to compile .jsx files into .js files on the fly, when first requested. I did some investigation into it, but did not find any fully-python-based jsx transpilers, so I did not follow through - therefore it is not supported in the current version. But it might be a good thing to revisit in the future. -------------------------------------------------------------------------------- /example/main.py: -------------------------------------------------------------------------------- 1 | from dash import Dash, html, callback, Input, Output, State 2 | from dash_local_react_components import load_react_component 3 | 4 | app = Dash(__name__) 5 | 6 | MyComponent = load_react_component(app, 'react_components', 'my_component.js') 7 | 8 | app.layout = html.Div([ 9 | html.H1('My custom component test'), 10 | MyComponent(id='my-component', count=10), 11 | html.Button(id='my-button', children='save') 12 | ]) 13 | 14 | 15 | @callback( 16 | Output('my-button', 'children'), 17 | Input('my-button', 'n_clicks'), 18 | State('my-component', 'count'), 19 | prevent_initial_call=True 20 | ) 21 | def on_click(n_clicks, count): 22 | return f'saved {count}' 23 | 24 | 25 | app.run_server(debug=True) 26 | -------------------------------------------------------------------------------- /example/react_components/common.js: -------------------------------------------------------------------------------- 1 | export function add(a, b) { 2 | return a + b; 3 | } -------------------------------------------------------------------------------- /example/react_components/my_component.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { add } from './common.js'; 3 | 4 | export default function MyComponent(props) { 5 | const { setProps, count } = props; 6 | 7 | function onClick() { 8 | setProps({ count: add(count, 1) }); 9 | } 10 | 11 | return React.createElement('div', {}, [ 12 | React.createElement('div', {}, `count: ${count}`), 13 | React.createElement('button', { onClick }, 'Increment') 14 | ]); 15 | } 16 | 17 | MyComponent.defaultProps = { 18 | count: 0 19 | }; 20 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=65.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "dash-local-react-components" 7 | version = "1.4.0" 8 | authors = [ 9 | { name="Tomasz Rewak", email="tomasz.rewak@gmail.com" }, 10 | ] 11 | description = "Enables loading react components in Dash applications directly from local project files, without any need for a separate build process." 12 | readme = "README.md" 13 | requires-python = ">=3.7" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | ] 19 | dependencies = [ 20 | 'dash >= 2.6.0, < 3', 21 | ] 22 | 23 | [tool.setuptools.packages.find] 24 | where = ["src"] 25 | 26 | [tool.setuptools.package-data] 27 | "*" = ["*.js"] 28 | 29 | [project.urls] 30 | "Homepage" = "https://github.com/TomaszRewak/dash-local-react-components" 31 | "Bug Tracker" = "https://github.com/TomaszRewak/dash-local-react-components/issues" 32 | 33 | [tool.mypy] 34 | python_version = "3.10" 35 | warn_return_any = true 36 | 37 | [tool.pytest.ini_options] 38 | minversion = "6.0" 39 | testpaths = [ 40 | "tests" 41 | ] -------------------------------------------------------------------------------- /src/dash_local_react_components/__init__.py: -------------------------------------------------------------------------------- 1 | from dash_local_react_components._functions import load_react_component 2 | from dash_local_react_components._config import config 3 | -------------------------------------------------------------------------------- /src/dash_local_react_components/_common.py: -------------------------------------------------------------------------------- 1 | react_file_name = '__es_react__.js' 2 | import_file_name = '__local_react_components_import__.js' 3 | import_namespace = '__local_react_components__' 4 | -------------------------------------------------------------------------------- /src/dash_local_react_components/_config.py: -------------------------------------------------------------------------------- 1 | class Config: 2 | def __init__(self): 3 | self.import_files_root_path = '/' 4 | 5 | config = Config() -------------------------------------------------------------------------------- /src/dash_local_react_components/_file_generator.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Type 2 | from dash import Dash # type: ignore 3 | from dash.development.base_component import Component # type: ignore 4 | from dash_local_react_components._common import import_namespace 5 | from dash_local_react_components._types import ComponentKey 6 | from dash_local_react_components._config import config 7 | 8 | 9 | def _generate_component_import_file(public_path: str, file_path: str, export_name: str, component_name: str) -> str: 10 | relative_import_levels = config.import_files_root_path.count('/') - 1 11 | relative_public_path = '../' * relative_import_levels + public_path 12 | 13 | return f''' 14 | import {{{export_name} as {component_name}}} from "./{relative_public_path}/{file_path}"; 15 | {import_namespace}["{component_name}"] = {component_name} 16 | ''' 17 | 18 | 19 | def generate_import_file(app: Dash, components: Dict[ComponentKey, Type[Component]]) -> str: 20 | header = f''' 21 | const {import_namespace} = {{}}; 22 | ''' 23 | 24 | footer = f''' 25 | window.{import_namespace} = {import_namespace}; 26 | 27 | if (window.__start_dash_app__) 28 | window.__start_dash_app__(); 29 | ''' 30 | 31 | component_imports = [ 32 | _generate_component_import_file(public_path, file_path, export_name, component.unique_name) 33 | for (component_app, public_path, file_path, export_name), component in components.items() 34 | if component_app is app 35 | ] 36 | 37 | return header + '\n'.join(component_imports) + footer 38 | 39 | 40 | def generate_react_file(): 41 | return ''' 42 | React = window.React; 43 | export default React; 44 | ''' 45 | -------------------------------------------------------------------------------- /src/dash_local_react_components/_functions.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Set, Type 2 | from dash import Dash # type: ignore 3 | from dash.development.base_component import Component # type: ignore 4 | from urllib import parse 5 | from flask import send_from_directory 6 | from dash_local_react_components._config import config 7 | from dash_local_react_components._common import import_file_name, import_namespace, react_file_name 8 | from dash_local_react_components._file_generator import generate_import_file, generate_react_file 9 | from dash_local_react_components._types import AppKey, ComponentKey, LibraryKey 10 | from dash_local_react_components._utils import change_function_name 11 | 12 | _initialized_apps: Set[AppKey] = set() 13 | _initialized_libraries: Set[LibraryKey] = set() 14 | _initialized_components: Dict[ComponentKey, Type[Component]] = dict() 15 | 16 | 17 | def _initialize_app(app: Dash) -> None: 18 | import_file_path = f'{config.import_files_root_path}{import_file_name}' 19 | react_file_path = f'{config.import_files_root_path}{react_file_name}' 20 | 21 | app.renderer = f''' 22 | window.__start_dash_app__ = () => new DashRenderer(); 23 | 24 | if (window.{import_namespace}) 25 | window.__start_dash_app__(); 26 | ''' 27 | 28 | app.config.external_scripts += [f''' 29 | "> 30 | 31 |