Repository: reflex-dev/reflex
Pull request: [Fix 477] Use jinja2 for templating
### All Submissions:

- [x] Have you followed the guidelines stated in [CONTRIBUTING.md](https://github.com/pynecone-io/pynecone/blob/main/CONTRIBUTING.md) file?
- [x] Have you checked to ensure there aren't any other open [Pull Requests](https://github.com/pynecone-io/pynecone/pulls ) for the desired changed?

<!-- You can erase any parts of this template not applicable to your Pull Request. -->

### Type of change

Please delete options that are not relevant.

- [x] Breaking change (fix or feature that would cause existing functionality to not work as expected)


### New Feature Submission:

- [x] Does your submission pass the tests? 
- [x] Have you linted your code locally prior to submission?

### Changes To Core Features:

- [x] Have you added an explanation of what your changes do and why you'd like us to include them?
- [x] Have you written new tests for your core changes, as applicable?
- [x] Have you successfully ran tests with your changes locally?

### Description
Added jinja2 for templating.
Fix #477 .

#### Basic concept
Pass dictionary data from Component class to template file through jinja2.
I modified type of return value, that comes from render function in Component, from string type to dict.
And created template file with ".jinja" extentions to show data.

#### template fles
Most of js.jinja file inherited from "base_page.js.jinja" file.
"base_page.js.jinja2" has basic stracture of js file.

"pages/utils.js.jinja2" file has functions to create JSX with indenting.
Components are nested, so that I show them recursively and add indents compared with their parent's indents.
(Like, children components have 2 white space indents, grandchildren have 2 white space indents more than the children and so on.)
Condition and iterable tag are created in function.

#### template.py
I add the Environment class at the top of the file.
It has some global function "json_dumps" and "react_setter" refering pynecone project functions, 
and some global context of constant value.
These are used in template files.

#### Component class
I modified render function of Component class.
It returns dictionary data now.
To get JSX tag data, I use dict function to Tag class.
I think we don't get string of Tag class , so `__str__` function of Tag class is removed.

#### React Imports
Update import logic in "utils.py" file.
At `compile_import_statement` function, just return default and rest import from library.
When import in js file, we use like below
```js
import default from "library"
import {rest_1, test_2} from "library"
```
At  `compile_imports` function, we get list of dictionary for template engine.

#### Unit Testing
Update unit testing related above.


Problem statement:
Use Jinja for templating
Currently all the JS templating is done manually in Python and f-string formatting. It would be nice to use something like Jinja that was designed for this very task. As the templates get bigger, this will help us stay more understandable and maintainable.

Hints from issue discussion:
Waiting for a follow up PR to fix the build, then we will merge this

Commit message: fix test fails of python 3.8

Patch to review:
diff --git a/poetry.lock b/poetry.lock
index 2012042a11d..6839da93192 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry and should not be changed by hand.
 
 [[package]]
 name = "anyio"
@@ -482,6 +482,84 @@ files = [
     {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
 ]
 
+[[package]]
+name = "jinja2"
+version = "3.1.2"
+description = "A very fast and expressive template engine."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
+    {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
+[[package]]
+name = "markupsafe"
+version = "2.1.2"
+description = "Safely add untrusted strings to HTML/XML markup."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"},
+    {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
+]
+
 [[package]]
 name = "mypy-extensions"
 version = "1.0.0"
@@ -1258,7 +1336,7 @@ files = [
 ]
 
 [package.dependencies]
-greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and platform_machine == \"aarch64\" or python_version >= \"3\" and platform_machine == \"ppc64le\" or python_version >= \"3\" and platform_machine == \"x86_64\" or python_version >= \"3\" and platform_machine == \"amd64\" or python_version >= \"3\" and platform_machine == \"AMD64\" or python_version >= \"3\" and platform_machine == \"win32\" or python_version >= \"3\" and platform_machine == \"WIN32\""}
+greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"}
 importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
 
 [package.extras]
@@ -1619,4 +1697,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.7"
-content-hash = "8c9f764a830657316f774fb679895c89d6874460582f4dbf8b2edbd4f534b262"
+content-hash = "43e53d9fff649b6a939ec953411b8932e512600b5af3edc0cbbbbb6c5576168b"
diff --git a/pynecone/compiler/compiler.py b/pynecone/compiler/compiler.py
index a35f4dab18b..037cce5d284 100644
--- a/pynecone/compiler/compiler.py
+++ b/pynecone/compiler/compiler.py
@@ -1,7 +1,6 @@
 """Compiler for the pynecone apps."""
 from __future__ import annotations
 
-import json
 from functools import wraps
 from typing import Callable, List, Set, Tuple, Type
 
@@ -10,7 +9,7 @@
 from pynecone.components.component import Component, CustomComponent
 from pynecone.state import State
 from pynecone.style import Style
-from pynecone.utils import imports, path_ops
+from pynecone.utils import imports
 
 # Imports to be included in every Pynecone app.
 DEFAULT_IMPORTS: imports.ImportDict = {
@@ -37,7 +36,7 @@ def _compile_document_root(root: Component) -> str:
     Returns:
         The compiled document root.
     """
-    return templates.DOCUMENT_ROOT(
+    return templates.DOCUMENT_ROOT.render(
         imports=utils.compile_imports(root.get_imports()),
         document=root.render(),
     )
@@ -52,7 +51,7 @@ def _compile_theme(theme: dict) -> str:
     Returns:
         The compiled theme.
     """
-    return templates.THEME(theme=json.dumps(theme))
+    return templates.THEME.render(theme=theme)
 
 
 def _compile_page(component: Component, state: Type[State]) -> str:
@@ -67,17 +66,20 @@ def _compile_page(component: Component, state: Type[State]) -> str:
     """
     # Merge the default imports with the app-specific imports.
     imports = utils.merge_imports(DEFAULT_IMPORTS, component.get_imports())
+    imports = utils.compile_imports(imports)
 
     # Compile the code to render the component.
-    return templates.PAGE(
-        imports=utils.compile_imports(imports),
-        custom_code=path_ops.join(component.get_custom_code()),
-        constants=utils.compile_constants(),
-        state=utils.compile_state(state),
-        events=utils.compile_events(state),
-        effects=utils.compile_effects(state),
-        hooks=path_ops.join(component.get_hooks()),
+    return templates.PAGE.render(
+        imports=imports,
+        custom_codes=component.get_custom_code(),
+        endpoints={
+            constant.name: constant.get_url() for constant in constants.Endpoint
+        },
+        initial_state=utils.compile_state(state),
+        state_name=state.get_name(),
+        hooks=component.get_hooks(),
         render=component.render(),
+        transports=constants.Transports.POLLING_WEBSOCKET.get_transports(),
     )
 
 
@@ -94,18 +96,18 @@ def _compile_components(components: Set[CustomComponent]) -> str:
         "react": {"memo"},
         f"/{constants.STATE_PATH}": {"E", "isTrue"},
     }
-    component_defs = []
+    component_renders = []
 
     # Compile each component.
     for component in components:
-        component_def, component_imports = utils.compile_custom_component(component)
-        component_defs.append(component_def)
+        component_render, component_imports = utils.compile_custom_component(component)
+        component_renders.append(component_render)
         imports = utils.merge_imports(imports, component_imports)
 
     # Compile the components page.
-    return templates.COMPONENTS(
+    return templates.COMPONENTS.render(
         imports=utils.compile_imports(imports),
-        components=path_ops.join(component_defs),
+        components=component_renders,
     )
 
 
diff --git a/pynecone/compiler/templates.py b/pynecone/compiler/templates.py
index a5c24e7ce34..c97bea9a59a 100644
--- a/pynecone/compiler/templates.py
+++ b/pynecone/compiler/templates.py
@@ -1,163 +1,75 @@
 """Templates to use in the pynecone compiler."""
 
-from typing import Optional, Set
+from jinja2 import Environment, FileSystemLoader, Template
 
 from pynecone import constants
 from pynecone.utils import path_ops
-
-# Template for the Pynecone config file.
-PCCONFIG = f"""import pynecone as pc
-
-class {{config_name}}(pc.Config):
-    pass
-
-config = {{config_name}}(
-    app_name="{{app_name}}",
-    db_url="{constants.DB_URL}",
-    env=pc.Env.DEV,
-)
-"""
-
-# Javascript formatting.
-CONST = "const {name} = {value}".format
-PROP = "{object}.{property}".format
-IMPORT_LIB = 'import "{lib}"'.format
-IMPORT_FIELDS = 'import {default}{others} from "{lib}"'.format
-
-
-def format_import(lib: str, default: str = "", rest: Optional[Set[str]] = None) -> str:
-    """Format an import statement.
+from pynecone.utils.format import json_dumps
+
+
+class PyneconeJinjaEnvironment(Environment):
+    """The template class for jinja environment."""
+
+    def __init__(self) -> None:
+        """Set default environment."""
+        extensions = ["jinja2.ext.debug"]
+        super().__init__(
+            extensions=extensions,
+            trim_blocks=True,
+            lstrip_blocks=True,
+        )
+        self.filters["json_dumps"] = json_dumps
+        self.filters["react_setter"] = lambda state: f"set{state.capitalize()}"
+        self.loader = FileSystemLoader(constants.JINJA_TEMPLATE_DIR)
+        self.globals["const"] = {
+            "socket": constants.SOCKET,
+            "result": constants.RESULT,
+            "router": constants.ROUTER,
+            "event_endpoint": constants.Endpoint.EVENT.name,
+            "events": constants.EVENTS,
+            "state": constants.STATE,
+            "processing": constants.PROCESSING,
+            "initial_result": {
+                constants.STATE: None,
+                constants.EVENTS: [],
+                constants.PROCESSING: False,
+            },
+            "color_mode": constants.COLOR_MODE,
+            "toggle_color_mode": constants.TOGGLE_COLOR_MODE,
+            "use_color_mode": constants.USE_COLOR_MODE,
+        }
+
+
+def get_template(name: str) -> Template:
+    """Get render function that work with a template.
 
     Args:
-        lib: The library to import from.
-        default: The default field to import.
-        rest: The set of fields to import from the library.
+        name: The template name. "/" is used as the path separator.
 
     Returns:
-        The compiled import statement.
+        A render function.
     """
-    # Handle the case of direct imports with no libraries.
-    if not lib:
-        assert not default, "No default field allowed for empty library."
-        assert rest is not None and len(rest) > 0, "No fields to import."
-        return path_ops.join([IMPORT_LIB(lib=lib) for lib in sorted(rest)])
-
-    # Handle importing from a library.
-    rest = rest or set()
-    if len(default) == 0 and len(rest) == 0:
-        # Handle the case of importing a library with no fields.
-        return IMPORT_LIB(lib=lib)
-    # Handle importing specific fields from a library.
-    others = f'{{{", ".join(sorted(rest))}}}' if len(rest) > 0 else ""
-    if default != "" and len(rest) > 0:
-        default += ", "
-    return IMPORT_FIELDS(default=default, others=others, lib=lib)
+    return PyneconeJinjaEnvironment().get_template(name=name)
 
 
+# Template for the Pynecone config file.
+PCCONFIG = get_template("app/pcconfig.py.jinja2")
+
 # Code to render a NextJS Document root.
-DOCUMENT_ROOT = path_ops.join(
-    [
-        "{imports}",
-        "export default function Document() {{",
-        "return (",
-        "{document}",
-        ")",
-        "}}",
-    ]
-).format
+DOCUMENT_ROOT = get_template("web/pages/_document.js.jinja2")
 
 # Template for the theme file.
-THEME = "export default {theme}".format
+THEME = get_template("web/utils/theme.js.jinja2")
 
 # Code to render a single NextJS page.
-PAGE = path_ops.join(
-    [
-        "{imports}",
-        "{custom_code}",
-        "{constants}",
-        "export default function Component() {{",
-        "{state}",
-        "{events}",
-        "{effects}",
-        "{hooks}",
-        "return (",
-        "{render}",
-        ")",
-        "}}",
-    ]
-).format
-
-# Code to render a single exported custom component.
-COMPONENT = path_ops.join(
-    [
-        "export const {name} = memo(({{{props}}}) => (",
-        "{render}",
-        "))",
-    ]
-).format
+PAGE = get_template("web/pages/index.js.jinja2")
 
 # Code to render the custom components page.
-COMPONENTS = path_ops.join(
-    [
-        "{imports}",
-        "{components}",
-    ]
-).format
-
-
-# React state declarations.
-USE_STATE = CONST(
-    name="[{state}, {set_state}]", value="useState({initial_state})"
-).format
-
-
-def format_state_setter(state: str) -> str:
-    """Format a state setter.
-
-    Args:
-        state: The name of the state variable.
-
-    Returns:
-        The compiled state setter.
-    """
-    return f"set{state[0].upper() + state[1:]}"
-
-
-def format_state(
-    state: str,
-    initial_state: str,
-) -> str:
-    """Format a state declaration.
-
-    Args:
-        state: The name of the state variable.
-        initial_state: The initial state of the state variable.
-
-    Returns:
-        The compiled state declaration.
-    """
-    set_state = format_state_setter(state)
-    return USE_STATE(state=state, set_state=set_state, initial_state=initial_state)
+COMPONENTS = get_template("web/pages/custom_component.js.jinja2")
 
+# Sitemap config file.
+SITEMAP_CONFIG = "module.exports = {config}".format
 
-# Events.
-EVENT_ENDPOINT = constants.Endpoint.EVENT.name
-EVENT_FN = path_ops.join(
-    [
-        "const Event = events => {set_state}({{",
-        "  ...{state},",
-        "  events: [...{state}.events, ...events],",
-        "}})",
-    ]
-).format
-UPLOAD_FN = path_ops.join(
-    [
-        "const File = files => {set_state}({{",
-        "  ...{state},",
-        "  files,",
-        "}})",
-    ]
-).format
 FULL_CONTROL = path_ops.join(
     [
         "{{setState(prev => ({{",
@@ -167,52 +79,3 @@ def format_state(
         ")}}",
     ]
 ).format
-
-# Effects.
-ROUTER = constants.ROUTER
-RESULT = constants.RESULT
-PROCESSING = constants.PROCESSING
-SOCKET = constants.SOCKET
-STATE = constants.STATE
-EVENTS = constants.EVENTS
-SET_RESULT = format_state_setter(RESULT)
-READY = f"const {{ isReady }} = {ROUTER};"
-USE_EFFECT = path_ops.join(
-    [
-        "useEffect(() => {{",
-        "  if(!isReady) {{",
-        "    return;",
-        "  }}",
-        f"  if (!{SOCKET}.current) {{{{",
-        f"    connect({SOCKET}, {{state}}, {{set_state}}, {RESULT}, {SET_RESULT}, {ROUTER}, {EVENT_ENDPOINT}, {{transports}})",
-        "  }}",
-        "  const update = async () => {{",
-        f"    if ({RESULT}.{STATE} != null) {{{{",
-        f"      {{set_state}}({{{{",
-        f"        ...{RESULT}.{STATE},",
-        f"        events: [...{{state}}.{EVENTS}, ...{RESULT}.{EVENTS}],",
-        "      }})",
-        f"      {SET_RESULT}({{{{",
-        f"        {STATE}: null,",
-        f"        {EVENTS}: [],",
-        f"        {PROCESSING}: false,",
-        "      }})",
-        "    }}",
-        f"    await updateState({{state}}, {{set_state}}, {RESULT}, {SET_RESULT}, {ROUTER}, {SOCKET}.current)",
-        "  }}",
-        "  update()",
-        "}})",
-    ]
-).format
-
-# Routing
-ROUTER = f"const {constants.ROUTER} = useRouter()"
-
-# Sockets.
-SOCKET = "const socket = useRef(null)"
-
-# Color toggle
-COLORTOGGLE = f"const {{ {constants.COLOR_MODE}, {constants.TOGGLE_COLOR_MODE} }} = {constants.USE_COLOR_MODE}()"
-
-# Sitemap config file.
-SITEMAP_CONFIG = "module.exports = {config}".format
diff --git a/pynecone/compiler/utils.py b/pynecone/compiler/utils.py
index d6b80f3a7ce..21898717b34 100644
--- a/pynecone/compiler/utils.py
+++ b/pynecone/compiler/utils.py
@@ -1,11 +1,9 @@
 """Common utility functions used in the compiler."""
 
-import json
 import os
 from typing import Dict, List, Optional, Set, Tuple, Type
 
 from pynecone import constants
-from pynecone.compiler import templates
 from pynecone.components.base import (
     Body,
     ColorModeScript,
@@ -30,7 +28,7 @@
 merge_imports = imports.merge_imports
 
 
-def compile_import_statement(lib: str, fields: Set[str]) -> str:
+def compile_import_statement(lib: str, fields: Set[str]) -> Tuple[str, Set[str]]:
     """Compile an import statement.
 
     Args:
@@ -38,7 +36,9 @@ def compile_import_statement(lib: str, fields: Set[str]) -> str:
         fields: The set of fields to import from the library.
 
     Returns:
-        The compiled import statement.
+        The libraries for default and rest.
+        default: default library. When install "import def from library".
+        rest: rest of libraries. When install "import {rest1, rest2} from library"
     """
     # Check for default imports.
     defaults = {
@@ -51,58 +51,59 @@ def compile_import_statement(lib: str, fields: Set[str]) -> str:
     # Get the default import, and the specific imports.
     default = next(iter(defaults), "")
     rest = fields - defaults
-    return templates.format_import(lib=lib, default=default, rest=rest)
 
+    return default, rest
 
-def compile_imports(imports: imports.ImportDict) -> str:
+
+def compile_imports(imports: imports.ImportDict) -> List[dict]:
     """Compile an import dict.
 
     Args:
         imports: The import dict to compile.
 
     Returns:
-        The compiled import dict.
+        The list of import dict.
     """
-    return path_ops.join(
-        [compile_import_statement(lib, fields) for lib, fields in imports.items()]
-    )
-
+    import_dicts = []
+    for lib, fields in imports.items():
+        default, rest = compile_import_statement(lib, fields)
+        if not lib:
+            assert not default, "No default field allowed for empty library."
+            assert rest is not None and len(rest) > 0, "No fields to import."
+            for module in sorted(rest):
+                import_dicts.append(get_import_dict(module))
+            continue
 
-def compile_constant_declaration(name: str, value: str) -> str:
-    """Compile a constant declaration.
-
-    Args:
-        name: The name of the constant.
-        value: The value of the constant.
+        import_dicts.append(get_import_dict(lib, default, rest))
+    return import_dicts
 
-    Returns:
-        The compiled constant declaration.
-    """
-    return templates.CONST(name=name, value=json.dumps(value))
 
+def get_import_dict(lib: str, default: str = "", rest: Optional[set] = None) -> dict:
+    """Get dictionary for import template.
 
-def compile_constants() -> str:
-    """Compile all the necessary constants.
+    Args:
+        lib: The importing react library.
+        default: The default module to import.
+        rest: The rest module to import.
 
     Returns:
-        A string of all the compiled constants.
+        A dictionary for import template.
     """
-    return path_ops.join(
-        [
-            compile_constant_declaration(name=endpoint.name, value=endpoint.get_url())
-            for endpoint in constants.Endpoint
-        ]
-    )
+    return {
+        "lib": lib,
+        "default": default,
+        "rest": rest if rest else set(),
+    }
 
 
-def compile_state(state: Type[State]) -> str:
+def compile_state(state: Type[State]) -> dict:
     """Compile the state of the app.
 
     Args:
         state: The app state object.
 
     Returns:
-        A string of the compiled state.
+        A dictionary of the compiled state.
     """
     initial_state = state().dict()
     initial_state.update(
@@ -111,77 +112,12 @@ def compile_state(state: Type[State]) -> str:
             "files": [],
         }
     )
-    initial_state = format.format_state(initial_state)
-    synced_state = templates.format_state(
-        state=state.get_name(), initial_state=json.dumps(initial_state)
-    )
-    initial_result = {
-        constants.STATE: None,
-        constants.EVENTS: [],
-        constants.PROCESSING: False,
-    }
-    result = templates.format_state(
-        state="result",
-        initial_state=json.dumps(initial_result),
-    )
-    router = templates.ROUTER
-    socket = templates.SOCKET
-    ready = templates.READY
-    color_toggle = templates.COLORTOGGLE
-    return path_ops.join([synced_state, result, router, socket, ready, color_toggle])
-
-
-def compile_events(state: Type[State]) -> str:
-    """Compile all the events for a given component.
-
-    Args:
-        state: The state class for the component.
-
-    Returns:
-        A string of the compiled events for the component.
-    """
-    state_name = state.get_name()
-    state_setter = templates.format_state_setter(state_name)
-    return path_ops.join(
-        [
-            templates.EVENT_FN(state=state_name, set_state=state_setter),
-            templates.UPLOAD_FN(state=state_name, set_state=state_setter),
-        ]
-    )
-
-
-def compile_effects(state: Type[State]) -> str:
-    """Compile all the effects for a given component.
-
-    Args:
-        state: The state class for the component.
-
-    Returns:
-        A string of the compiled effects for the component.
-    """
-    state_name = state.get_name()
-    set_state = templates.format_state_setter(state_name)
-    transports = constants.Transports.POLLING_WEBSOCKET.get_transports()
-    return templates.USE_EFFECT(
-        state=state_name, set_state=set_state, transports=transports
-    )
-
-
-def compile_render(component: Component) -> str:
-    """Compile the component's render method.
-
-    Args:
-        component: The component to compile the render method for.
-
-    Returns:
-        A string of the compiled render method.
-    """
-    return component.render()
+    return format.format_state(initial_state)
 
 
 def compile_custom_component(
     component: CustomComponent,
-) -> Tuple[str, imports.ImportDict]:
+) -> Tuple[dict, imports.ImportDict]:
     """Compile a custom component.
 
     Args:
@@ -201,15 +137,15 @@ def compile_custom_component(
     }
 
     # Concatenate the props.
-    props = ", ".join([prop.name for prop in component.get_prop_vars()])
+    props = [prop.name for prop in component.get_prop_vars()]
 
     # Compile the component.
     return (
-        templates.COMPONENT(
-            name=component.tag,
-            props=props,
-            render=render,
-        ),
+        {
+            "name": component.tag,
+            "props": props,
+            "render": render.render(),
+        },
         imports,
     )
 
diff --git a/pynecone/components/base/meta.py b/pynecone/components/base/meta.py
index 99d0c91672b..bb4b7542af4 100644
--- a/pynecone/components/base/meta.py
+++ b/pynecone/components/base/meta.py
@@ -11,18 +11,17 @@ class Title(Component):
 
     tag = "title"
 
-    def render(self) -> str:
+    def render(self) -> dict:
         """Render the title component.
 
         Returns:
             The rendered title component.
         """
-        tag = self._render()
         # Make sure the title is a single string.
         assert len(self.children) == 1 and isinstance(
             self.children[0], Bare
         ), "Title must be a single string."
-        return str(tag.set(contents=str(self.children[0].contents)))
+        return super().render()
 
 
 class Meta(Component):
diff --git a/pynecone/components/component.py b/pynecone/components/component.py
index b5c56b35033..2e2d5d2fa25 100644
--- a/pynecone/components/component.py
+++ b/pynecone/components/component.py
@@ -21,7 +21,7 @@
     get_handler_args,
 )
 from pynecone.style import Style
-from pynecone.utils import format, imports, path_ops, types
+from pynecone.utils import format, imports, types
 from pynecone.var import BaseVar, Var
 
 
@@ -290,7 +290,7 @@ def __repr__(self) -> str:
         Returns:
             The code to render the component.
         """
-        return self.render()
+        return format.json_dumps(self.render())
 
     def __str__(self) -> str:
         """Represent the component in React.
@@ -298,7 +298,7 @@ def __str__(self) -> str:
         Returns:
             The code to render the component.
         """
-        return self.render()
+        return format.json_dumps(self.render())
 
     def _render(self) -> Tag:
         """Define how to render the component in React.
@@ -393,14 +393,14 @@ def add_style(self, style: ComponentStyle) -> Component:
             child.add_style(style)
         return self
 
-    def render(self) -> str:
+    def render(self) -> dict:
         """Render the component.
 
         Returns:
-            The code to render the component.
+            The dictionary for template of component.
         """
         tag = self._render()
-        return str(
+        return dict(
             tag.add_props(
                 **self.event_triggers,
                 key=self.key,
@@ -408,10 +408,10 @@ def render(self) -> str:
                 id=self.id,
                 class_name=self.class_name,
             ).set(
-                contents=path_ops.join(
-                    [str(tag.contents)] + [child.render() for child in self.children]
-                ).strip(),
-            )
+                children=[child.render() for child in self.children],
+                contents=str(tag.contents),
+                props=tag.format_props(),
+            ),
         )
 
     def _get_custom_code(self) -> Optional[str]:
diff --git a/pynecone/components/layout/cond.py b/pynecone/components/layout/cond.py
index 913bce8be19..794f3a8036e 100644
--- a/pynecone/components/layout/cond.py
+++ b/pynecone/components/layout/cond.py
@@ -24,7 +24,7 @@ class Cond(Component):
 
     @classmethod
     def create(
-        cls, cond: Var, comp1: Component, comp2: Optional[Component] = None
+        cls, cond: Var, comp1: Component, comp2: Optional[Component]
     ) -> Component:
         """Create a conditional component.
 
@@ -37,8 +37,10 @@ def create(
             The conditional component.
         """
         # Wrap everything in fragments.
-        comp1 = Fragment.create(comp1)
-        comp2 = Fragment.create(comp2) if comp2 else Fragment.create()
+        if comp1.__class__.__name__ != "Fragment":
+            comp1 = Fragment.create(comp1)
+        if comp2 is None or comp2.__class__.__name__ != "Fragment":
+            comp2 = Fragment.create(comp2) if comp2 else Fragment.create()
         return Fragment.create(
             cls(
                 cond=cond,
@@ -55,6 +57,26 @@ def _render(self) -> Tag:
             false_value=self.comp2.render(),
         )
 
+    def render(self) -> dict:
+        """Render the component.
+
+        Returns:
+            The dictionary for template of component.
+        """
+        tag = self._render()
+        return dict(
+            tag.add_props(
+                **self.event_triggers,
+                key=self.key,
+                sx=self.style,
+                id=self.id,
+                class_name=self.class_name,
+            ).set(
+                props=tag.format_props(),
+            ),
+            cond_state=f"isTrue({self.cond.full_name})",
+        )
+
 
 def cond(condition: Any, c1: Any, c2: Any = None):
     """Create a conditional component or Prop.
diff --git a/pynecone/components/layout/foreach.py b/pynecone/components/layout/foreach.py
index 33a8c37cc0a..8380bb3d5cd 100644
--- a/pynecone/components/layout/foreach.py
+++ b/pynecone/components/layout/foreach.py
@@ -4,8 +4,8 @@
 from typing import Any, Callable, List
 
 from pynecone.components.component import Component
-from pynecone.components.tags import IterTag, Tag
-from pynecone.var import BaseVar, Var
+from pynecone.components.tags import IterTag
+from pynecone.var import BaseVar, Var, get_unique_variable_name
 
 
 class Foreach(Component):
@@ -49,5 +49,38 @@ def create(cls, iterable: Var[List], render_fn: Callable, **props) -> Foreach:
             **props,
         )
 
-    def _render(self) -> Tag:
+    def _render(self) -> IterTag:
         return IterTag(iterable=self.iterable, render_fn=self.render_fn)
+
+    def render(self):
+        """Render the component.
+
+        Returns:
+            The dictionary for template of component.
+        """
+        tag = self._render()
+        try:
+            type_ = self.iterable.type_.__args__[0]
+        except Exception:
+            type_ = Any
+        arg = BaseVar(
+            name=get_unique_variable_name(),
+            type_=type_,
+        )
+        index_arg = tag.get_index_var_arg()
+        component = tag.render_component(self.render_fn, arg)
+        return dict(
+            tag.add_props(
+                **self.event_triggers,
+                key=self.key,
+                sx=self.style,
+                id=self.id,
+                class_name=self.class_name,
+            ).set(
+                children=[component.render()],
+                props=tag.format_props(),
+            ),
+            iterable_state=tag.iterable.full_name,
+            arg_name=arg.name,
+            arg_index=index_arg,
+        )
diff --git a/pynecone/components/tags/cond_tag.py b/pynecone/components/tags/cond_tag.py
index f6c6cc8886c..9dd1d6206fe 100644
--- a/pynecone/components/tags/cond_tag.py
+++ b/pynecone/components/tags/cond_tag.py
@@ -1,9 +1,8 @@
 """Tag to conditionally render components."""
 
-from typing import Any
+from typing import Any, Optional
 
 from pynecone.components.tags.tag import Tag
-from pynecone.utils import format
 from pynecone.var import Var
 
 
@@ -14,20 +13,7 @@ class CondTag(Tag):
     cond: Var[Any]
 
     # The code to render if the condition is true.
-    true_value: str
+    true_value: dict
 
     # The code to render if the condition is false.
-    false_value: str
-
-    def __str__(self) -> str:
-        """Render the tag as a React string.
-
-        Returns:
-            The React code to render the tag.
-        """
-        assert self.cond is not None, "The condition must be set."
-        return format.format_cond(
-            cond=self.cond.full_name,
-            true_value=self.true_value,
-            false_value=self.false_value,
-        )
+    false_value: Optional[dict]
diff --git a/pynecone/components/tags/iter_tag.py b/pynecone/components/tags/iter_tag.py
index 51572d9ceac..cac7bfd4c9a 100644
--- a/pynecone/components/tags/iter_tag.py
+++ b/pynecone/components/tags/iter_tag.py
@@ -2,11 +2,10 @@
 from __future__ import annotations
 
 import inspect
-from typing import TYPE_CHECKING, Any, Callable, List
+from typing import TYPE_CHECKING, Callable, List
 
 from pynecone.components.tags.tag import Tag
-from pynecone.utils import format
-from pynecone.var import BaseVar, Var, get_unique_variable_name
+from pynecone.var import Var
 
 if TYPE_CHECKING:
     from pynecone.components.component import Component
@@ -83,24 +82,3 @@ def render_component(render_fn: Callable, arg: Var) -> Component:
             component.key = index
 
         return component
-
-    def __str__(self) -> str:
-        """Render the tag as a React string.
-
-        Returns:
-            The React code to render the tag.
-        """
-        try:
-            type_ = self.iterable.type_.__args__[0]
-        except Exception:
-            type_ = Any
-        arg = BaseVar(
-            name=get_unique_variable_name(),
-            type_=type_,
-        )
-        index_arg = self.get_index_var_arg()
-        component = self.render_component(self.render_fn, arg)
-        return format.wrap(
-            f"{self.iterable.full_name}.map(({arg.name}, {index_arg}) => {component})",
-            "{",
-        )
diff --git a/pynecone/components/tags/tag.py b/pynecone/components/tags/tag.py
index f23e975eed7..5b9fd3f78a7 100644
--- a/pynecone/components/tags/tag.py
+++ b/pynecone/components/tags/tag.py
@@ -3,9 +3,8 @@
 from __future__ import annotations
 
 import json
-import os
 import re
-from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Tuple, Union
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union
 
 from plotly.graph_objects import Figure
 from plotly.io import to_json
@@ -37,6 +36,9 @@ class Tag(Base):
     # Special props that aren't key value pairs.
     special_props: Set[Var] = set()
 
+    # The children components.
+    children: List[Any] = []
+
     def __init__(self, *args, **kwargs):
         """Initialize the tag.
 
@@ -117,55 +119,22 @@ def format_prop(
         assert isinstance(prop, str), "The prop must be a string."
         return format.wrap(prop, "{", check_first=False)
 
-    def format_props(self) -> str:
+    def format_props(self) -> list:
         """Format the tag's props.
 
         Returns:
-            The formatted props.
+            The formatted props list.
         """
         # If there are no props, return an empty string.
         if len(self.props) == 0:
-            return ""
+            return []
 
         # Format all the props.
-        return os.linesep.join(
+        return [
             f"{name}={self.format_prop(prop)}"
             for name, prop in sorted(self.props.items())
             if prop is not None
-        )
-
-    def __str__(self) -> str:
-        """Render the tag as a React string.
-
-        Returns:
-            The React code to render the tag.
-        """
-        # Get the tag props.
-        props_str = self.format_props()
-
-        # Add the special props.
-        props_str += " ".join([str(prop) for prop in self.special_props])
-
-        # Add a space if there are props.
-        if len(props_str) > 0:
-            props_str = " " + props_str
-
-        if len(self.contents) == 0:
-            # If there is no inner content, we don't need a closing tag.
-            tag_str = format.wrap(f"{self.name}{props_str}/", "<")
-        else:
-            if self.args is not None:
-                # If there are args, wrap the tag in a function call.
-                args_str = ", ".join(self.args)
-                contents = f"{{({{{args_str}}}) => ({self.contents})}}"
-            else:
-                contents = self.contents
-            # Otherwise wrap it in opening and closing tags.
-            open = format.wrap(f"{self.name}{props_str}", "<")
-            close = format.wrap(f"/{self.name}", "<")
-            tag_str = format.wrap(contents, open, close)
-
-        return tag_str
+        ] + [str(prop) for prop in self.special_props]
 
     def add_props(self, **kwargs: Optional[Any]) -> Tag:
         """Add props to the tag.
diff --git a/pynecone/constants.py b/pynecone/constants.py
index f85336d63c8..7c2604dc5a6 100644
--- a/pynecone/constants.py
+++ b/pynecone/constants.py
@@ -33,6 +33,8 @@
 WEB_TEMPLATE_DIR = os.path.join(TEMPLATE_DIR, "web")
 # The assets subdirectory of the template directory.
 ASSETS_TEMPLATE_DIR = os.path.join(TEMPLATE_DIR, APP_ASSETS_DIR)
+# The jinja template directory.
+JINJA_TEMPLATE_DIR = os.path.join(ROOT_DIR, MODULE_NAME, "templates")
 
 # The frontend directories in a project.
 # The web folder where the NextJS app is compiled to.
diff --git a/pynecone/el/element.py b/pynecone/el/element.py
index 849a6030525..1d0aa5f2523 100644
--- a/pynecone/el/element.py
+++ b/pynecone/el/element.py
@@ -1,7 +1,6 @@
 """Base class definition for raw HTML elements."""
 
 from pynecone.components.component import Component
-from pynecone.utils import path_ops
 
 
 class Element(Component):
@@ -12,14 +11,14 @@ class Element(Component):
     prop.
     """
 
-    def render(self) -> str:
+    def render(self) -> dict:
         """Render the element.
 
         Returns:
             The code to render the element.
         """
         tag = self._render()
-        return str(
+        return dict(
             tag.add_props(
                 **self.event_triggers,
                 key=self.key,
@@ -27,9 +26,8 @@ def render(self) -> str:
                 style=self.style,
                 class_name=self.class_name,
             ).set(
-                contents=path_ops.join(
-                    [str(tag.contents)] + [child.render() for child in self.children]
-                ).strip(),
+                contents=str(tag.contents),
+                children=[child.render() for child in self.children],
             )
         )
 
diff --git a/pynecone/templates/app/pcconfig.py.jinja2 b/pynecone/templates/app/pcconfig.py.jinja2
new file mode 100644
index 00000000000..da215ed107d
--- /dev/null
+++ b/pynecone/templates/app/pcconfig.py.jinja2
@@ -0,0 +1,10 @@
+import pynecone as pc
+
+class {{ config_name }}(pc.Config):
+    pass
+
+config = {{ config_name }}(
+    app_name="{{ app_name }}",
+    db_url="{{ db_url }}",
+    env=pc.Env.DEV,
+)
diff --git a/pynecone/templates/web/pages/_document.js.jinja2 b/pynecone/templates/web/pages/_document.js.jinja2
new file mode 100644
index 00000000000..f17c4206990
--- /dev/null
+++ b/pynecone/templates/web/pages/_document.js.jinja2
@@ -0,0 +1,9 @@
+{% extends "web/pages/base_page.js.jinja2" %}
+
+{% block export %}
+export default function Document() {
+  return (
+    {{utils.render(document, indent_width=4)}}
+  )
+}
+{% endblock %}
diff --git a/pynecone/templates/web/pages/base_page.js.jinja2 b/pynecone/templates/web/pages/base_page.js.jinja2
new file mode 100644
index 00000000000..0f3868aa449
--- /dev/null
+++ b/pynecone/templates/web/pages/base_page.js.jinja2
@@ -0,0 +1,13 @@
+{% import 'web/pages/utils.js.jinja2' as utils %}
+
+{%- block imports_libs %}
+{% for module in imports%}
+  {{- utils.get_import(module) }}
+{% endfor %}
+{% endblock %}
+
+{% block declaration %}
+{% endblock %}
+
+{% block export %}
+{% endblock %}
diff --git a/pynecone/templates/web/pages/custom_component.js.jinja2 b/pynecone/templates/web/pages/custom_component.js.jinja2
new file mode 100644
index 00000000000..89d5d7abb32
--- /dev/null
+++ b/pynecone/templates/web/pages/custom_component.js.jinja2
@@ -0,0 +1,10 @@
+{% extends "web/pages/base_page.js.jinja2" %}
+
+{% block export %}
+{% for component in components %}
+
+export const {{component.name}} = memo(({ {{-component.props|join(", ")-}} }) => (
+  {{utils.render(component.render)}}
+))
+{% endfor %}
+{% endblock %}
diff --git a/pynecone/templates/web/pages/index.js.jinja2 b/pynecone/templates/web/pages/index.js.jinja2
new file mode 100644
index 00000000000..819f7c673ad
--- /dev/null
+++ b/pynecone/templates/web/pages/index.js.jinja2
@@ -0,0 +1,66 @@
+{% extends "web/pages/base_page.js.jinja2" %}
+
+{% block declaration %}
+{% for custom_code in custom_codes %}
+{{custom_code}}
+{% endfor %}
+
+{% for name, url in endpoints.items() %}
+const {{name}} = {{url|json_dumps}}
+{% endfor %}
+{% endblock %}
+
+{% block export %}
+export default function Component() {
+  const [{{state_name}}, {{state_name|react_setter}}] = useState({{initial_state|json_dumps}})
+  const [{{const.result}}, {{const.result|react_setter}}] = useState({{const.initial_result|json_dumps}})
+  const {{const.router}} = useRouter()
+  const {{const.socket}} = useRef(null)
+  const { isReady } = {{const.router}}
+  const { {{const.color_mode}}, {{const.toggle_color_mode}} } = {{const.use_color_mode}}()
+
+  const Event = events => {{state_name|react_setter}}({
+    ...{{state_name}},
+    events: [...{{state_name}}.events, ...events],
+  })
+
+  const File = files => {{state_name|react_setter}}({
+    ...{{state_name}},
+    files,
+  })
+
+  useEffect(()=>{
+    if(!isReady) {
+      return;
+    }
+    if (!{{const.socket}}.current) {
+      connect({{const.socket}}, {{state_name}}, {{state_name|react_setter}}, {{const.result}}, {{const.result|react_setter}}, {{const.router}}, {{const.event_endpoint}}, {{transports}})
+    }
+    const update = async () => {
+      if ({{const.result}}.{{const.state}} != null){
+        {{state_name|react_setter}}({
+          ...{{const.result}}.{{const.state}},
+          events: [...{{state_name}}.{{const.events}}, ...{{const.result}}.{{const.events}}],
+        })
+
+        {{const.result|react_setter}}({
+          {{const.state}}: null,
+          {{const.events}}: [],
+          {{const.processing}}: false,
+        })
+      }
+
+      await updateState({{state_name}}, {{state_name|react_setter}}, {{const.result}}, {{const.result|react_setter}}, {{const.router}}, {{const.socket}}.current)
+      }
+      update()
+  })
+
+  {% for hook in hooks %}
+  {{ hook }}
+  {% endfor %}
+
+  return (
+    {{utils.render(render, indent_width=4)}}
+  )
+}
+{% endblock %}
diff --git a/pynecone/templates/web/pages/utils.js.jinja2 b/pynecone/templates/web/pages/utils.js.jinja2
new file mode 100644
index 00000000000..7ec0b01f91f
--- /dev/null
+++ b/pynecone/templates/web/pages/utils.js.jinja2
@@ -0,0 +1,110 @@
+{# Renderting components recursively. #}
+{# Args: #}
+{#     component: component dictionary #}
+{#     indent_width: indent width #}
+{% macro render(component, indent_width=2) %}
+{% filter indent(width=indent_width) %}
+  {%- if component is not mapping %}
+    {{- component }}
+  {%- elif component.iterable %}
+    {{- render_iterable_tag(component) }}
+  {%- elif component.cond %}
+    {{- render_condition_tag(component) }}
+  {%- elif component.children|length %}
+    {{- render_tag(component) }}
+  {%- else %}
+    {{- render_self_close_tag(component) }}
+  {%- endif %}
+{% endfilter %}
+{% endmacro %}
+
+{# Renderting self close tag. #}
+{# Args: #}
+{#     component: component dictionary #}
+{% macro render_self_close_tag(component) %}
+{%- if component.name|length %}
+<{{ component.name }} {{- render_props(component.props) }}/>
+{%- else %}
+  {{- component.contents }}
+{%- endif %}
+{% endmacro %}
+
+{# Renderting close tag with args and props. #}
+{# Args: #}
+{#     component: component dictionary #}
+{% macro render_tag(component) %}
+<{{component.name}} {{- render_props(component.props) }}>
+{%- if component.args is not none -%}
+  {{- render_arg_content(component) }}
+{%- else -%}
+  {{ component.contents }}
+  {% for child in component.children %}
+  {{ render(child) }}
+  {% endfor %}
+{%- endif -%}
+</{{component.name}}>
+{%- endmacro %}
+
+
+{# Renderting condition component. #}
+{# Args: #}
+{#     component: component dictionary #}
+{% macro render_condition_tag(component) %}
+{ {{- component.cond_state }} ? (
+  {{ render(component.true_value) }}
+) : (
+  {{ render(component.false_value) }}
+)}
+{%- endmacro %}
+
+
+{# Renderting iterable component. #}
+{# Args: #}
+{#     component: component dictionary #}
+{% macro render_iterable_tag(component) %}
+{ {{- component.iterable_state }}.map(({{ component.arg_name }}, {{ component.arg_index }}) => (
+  {% for child in component.children %}
+  {{ render(child) }}
+  {% endfor %}
+))}
+{%- endmacro %}
+
+
+{# Renderting props of a component. #}
+{# Args: #}
+{#     component: component dictionary #}
+{% macro render_props(props) %}
+{% if props|length %} {{ props|join(" ") }}{% endif %}
+{% endmacro %}
+
+
+{# Renderting content with args. #}
+{# Args: #}
+{#     component: component dictionary #}
+{% macro render_arg_content(component) %}
+{% filter indent(width=2) %}
+{# no string below for a line break #}
+
+{({ {{component.args|join(", ")}} }) => (
+  {% for child in component.children %}
+  {{ render(child) }}
+  {% endfor %}
+)}
+{% endfilter %}
+{% endmacro %}
+
+
+{# Get react libraries import . #}
+{# Args: #}
+{#     module: react module dictionary #}
+{% macro get_import(module)%}
+{%- if module.default|length and module.rest|length -%}
+  import {{module.default}}, { {{module.rest|sort|join(", ")}} } from "{{module.lib}}"
+{%- elif module.default|length -%}
+  import {{module.default}} from "{{module.lib}}"
+{%- elif module.rest|length -%}
+  import { {{module.rest|sort|join(", ")}} } from "{{module.lib}}"
+{%- else -%}
+  import "{{module.lib}}"
+{%- endif -%}
+{% endmacro %}
diff --git a/pynecone/templates/web/utils/theme.js.jinja2 b/pynecone/templates/web/utils/theme.js.jinja2
new file mode 100644
index 00000000000..74f861c5af9
--- /dev/null
+++ b/pynecone/templates/web/utils/theme.js.jinja2
@@ -0,0 +1 @@
+export default {{ theme|json_dumps }}
diff --git a/pynecone/utils/format.py b/pynecone/utils/format.py
index 957f8356c97..2be1497adac 100644
--- a/pynecone/utils/format.py
+++ b/pynecone/utils/format.py
@@ -304,11 +304,9 @@ def format_upload_event(event_spec: EventSpec) -> str:
     Returns:
         The compiled event.
     """
-    from pynecone.compiler import templates
-
     state, name = get_event_handler_parts(event_spec.handler)
     parent_state = state.split(".")[0]
-    return f'uploadFiles({parent_state}, {templates.RESULT}, {templates.SET_RESULT}, {parent_state}.files, "{state}.{name}",UPLOAD)'
+    return f'uploadFiles({parent_state}, {constants.RESULT}, set{constants.RESULT.capitalize()}, {parent_state}.files, "{state}.{name}",UPLOAD)'
 
 
 def format_full_control_event(event_chain: EventChain) -> str:
diff --git a/pynecone/utils/prerequisites.py b/pynecone/utils/prerequisites.py
index 1122a613c47..27ee352ca2f 100644
--- a/pynecone/utils/prerequisites.py
+++ b/pynecone/utils/prerequisites.py
@@ -150,7 +150,7 @@ def create_config(app_name: str):
 
     config_name = f"{re.sub(r'[^a-zA-Z]', '', app_name).capitalize()}Config"
     with open(constants.CONFIG_FILE, "w") as f:
-        f.write(templates.PCCONFIG.format(app_name=app_name, config_name=config_name))
+        f.write(templates.PCCONFIG.render(app_name=app_name, config_name=config_name))
 
 
 def create_web_directory(root: Path) -> str:
diff --git a/pyproject.toml b/pyproject.toml
index bc47033694f..bd5ba23ab19 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -40,6 +40,7 @@ websockets = "^10.4"
 cloudpickle = "^2.2.1"
 python-multipart = "^0.0.5"
 watchdog = "^2.3.1"
+jinja2 = "^3.1.2"
 
 [tool.poetry.group.dev.dependencies]
 pytest = "^7.1.2"
diff --git a/tests/compiler/test_compiler.py b/tests/compiler/test_compiler.py
index 31c2dae497a..ddc5d6bcbdb 100644
--- a/tests/compiler/test_compiler.py
+++ b/tests/compiler/test_compiler.py
@@ -1,4 +1,4 @@
-from typing import Set
+from typing import List, Set
 
 import pytest
 
@@ -7,72 +7,92 @@
 
 
 @pytest.mark.parametrize(
-    "lib,fields,output",
+    "lib,fields,test_default,test_rest",
     [
-        ("axios", {"axios"}, 'import axios from "axios"'),
-        ("axios", {"foo", "bar"}, 'import {bar, foo} from "axios"'),
-        ("axios", {"axios", "foo", "bar"}, 'import axios, {bar, foo} from "axios"'),
+        ("axios", {"axios"}, "axios", set()),
+        ("axios", {"foo", "bar"}, "", {"foo", "bar"}),
+        ("axios", {"axios", "foo", "bar"}, "axios", {"foo", "bar"}),
     ],
 )
-def test_compile_import_statement(lib: str, fields: Set[str], output: str):
+def test_compile_import_statement(
+    lib: str, fields: Set[str], test_default: str, test_rest: str
+):
     """Test the compile_import_statement function.
 
     Args:
         lib: The library name.
         fields: The fields to import.
-        output: The expected output.
+        test_default: The expected output of default library.
+        test_rest: The expected output rest libraries.
     """
-    assert utils.compile_import_statement(lib, fields) == output
+    default, rest = utils.compile_import_statement(lib, fields)
+    assert default == test_default
+    assert rest == test_rest
 
 
 @pytest.mark.parametrize(
-    "import_dict,output",
+    "import_dict,test_dicts",
     [
-        ({}, ""),
-        ({"axios": {"axios"}}, 'import axios from "axios"'),
-        ({"axios": {"foo", "bar"}}, 'import {bar, foo} from "axios"'),
+        ({}, []),
+        ({"axios": {"axios"}}, [{"lib": "axios", "default": "axios", "rest": set()}]),
+        (
+            {"axios": {"foo", "bar"}},
+            [{"lib": "axios", "default": "", "rest": {"foo", "bar"}}],
+        ),
         (
             {"axios": {"axios", "foo", "bar"}, "react": {"react"}},
-            'import axios, {bar, foo} from "axios"\nimport react from "react"',
+            [
+                {"lib": "axios", "default": "axios", "rest": {"foo", "bar"}},
+                {"lib": "react", "default": "react", "rest": set()},
+            ],
+        ),
+        (
+            {"": {"lib1.js", "lib2.js"}},
+            [
+                {"lib": "lib1.js", "default": "", "rest": set()},
+                {"lib": "lib2.js", "default": "", "rest": set()},
+            ],
         ),
-        ({"": {"lib1.js", "lib2.js"}}, 'import "lib1.js"\nimport "lib2.js"'),
         (
             {"": {"lib1.js", "lib2.js"}, "axios": {"axios"}},
-            'import "lib1.js"\nimport "lib2.js"\nimport axios from "axios"',
+            [
+                {"lib": "lib1.js", "default": "", "rest": set()},
+                {"lib": "lib2.js", "default": "", "rest": set()},
+                {"lib": "axios", "default": "axios", "rest": set()},
+            ],
         ),
     ],
 )
-def test_compile_imports(
-    import_dict: imports.ImportDict, output: str, windows_platform: bool
-):
+def test_compile_imports(import_dict: imports.ImportDict, test_dicts: List[dict]):
     """Test the compile_imports function.
 
     Args:
         import_dict: The import dictionary.
-        output: The expected output.
-        windows_platform: whether system is windows.
+        test_dicts: The expected output.
     """
-    assert utils.compile_imports(import_dict) == (
-        output.replace("\n", "\r\n") if windows_platform else output
-    )
+    imports = utils.compile_imports(import_dict)
+    for import_dict, test_dict in zip(imports, test_dicts):
+        assert import_dict["lib"] == test_dict["lib"]
+        assert import_dict["default"] == test_dict["default"]
+        assert import_dict["rest"] == test_dict["rest"]
 
 
-@pytest.mark.parametrize(
-    "name,value,output",
-    [
-        ("foo", "bar", 'const foo = "bar"'),
-        ("num", 1, "const num = 1"),
-        ("check", False, "const check = false"),
-        ("arr", [1, 2, 3], "const arr = [1, 2, 3]"),
-        ("obj", {"foo": "bar"}, 'const obj = {"foo": "bar"}'),
-    ],
-)
-def test_compile_constant_declaration(name: str, value: str, output: str):
-    """Test the compile_constant_declaration function.
+# @pytest.mark.parametrize(
+#     "name,value,output",
+#     [
+#         ("foo", "bar", 'const foo = "bar"'),
+#         ("num", 1, "const num = 1"),
+#         ("check", False, "const check = false"),
+#         ("arr", [1, 2, 3], "const arr = [1, 2, 3]"),
+#         ("obj", {"foo": "bar"}, 'const obj = {"foo": "bar"}'),
+#     ],
+# )
+# def test_compile_constant_declaration(name: str, value: str, output: str):
+#     """Test the compile_constant_declaration function.
 
-    Args:
-        name: The name of the constant.
-        value: The value of the constant.
-        output: The expected output.
-    """
-    assert utils.compile_constant_declaration(name, value) == output
+#     Args:
+#         name: The name of the constant.
+#         value: The value of the constant.
+#         output: The expected output.
+#     """
+#     assert utils.compile_constant_declaration(name, value) == output
diff --git a/tests/components/base/test_bare.py b/tests/components/base/test_bare.py
index 04d061044bf..de0c68f257d 100644
--- a/tests/components/base/test_bare.py
+++ b/tests/components/base/test_bare.py
@@ -19,5 +19,5 @@ def test_fstrings(contents, expected):
         contents: The contents of the component.
         expected: The expected output.
     """
-    comp = Bare.create(contents)
-    assert str(comp) == expected
+    comp = Bare.create(contents).render()
+    assert comp["contents"] == expected
diff --git a/tests/components/datadisplay/test_datatable.py b/tests/components/datadisplay/test_datatable.py
index f733cc63c58..f6e06f8ab8b 100644
--- a/tests/components/datadisplay/test_datatable.py
+++ b/tests/components/datadisplay/test_datatable.py
@@ -1,5 +1,3 @@
-import os
-
 import pandas as pd
 import pytest
 
@@ -37,11 +35,12 @@ def test_validate_data_table(data_table_state: pc.Var, expected):
         props["columns"] = data_table_state.columns
     data_table_component = data_table(**props)
 
-    assert (
-        str(data_table_component)
-        == f"<DataTableGrid columns={{{expected}.columns}}{os.linesep}data={{"
-        f"{expected}.data}}/>"
-    )
+    data_table_dict = data_table_component.render()
+
+    assert data_table_dict["props"] == [
+        f"columns={{{expected}.columns}}",
+        f"data={{{expected}.data}}",
+    ]
 
 
 @pytest.mark.parametrize(
diff --git a/tests/components/forms/test_uploads.py b/tests/components/forms/test_uploads.py
index 025af06c4b7..c71f571363d 100644
--- a/tests/components/forms/test_uploads.py
+++ b/tests/components/forms/test_uploads.py
@@ -1,5 +1,3 @@
-import os
-
 import pytest
 
 import pynecone as pc
@@ -49,14 +47,36 @@ def test_upload_component_render(upload_component):
     Args:
         upload_component: component fixture
     """
+    uplaod = upload_component.render()
+
+    # upload
+    assert uplaod["name"] == "ReactDropzone"
+    assert uplaod["props"] == [
+        "multiple={true}",
+        "onDrop={e => File(e)}",
+    ]
+    assert uplaod["args"] == ("getRootProps", "getInputProps")
+
+    # box inside of upload
+    [box] = uplaod["children"]
+    assert box["name"] == "Box"
+    assert box["props"] == [
+        'sx={{"border": "1px dotted black"}}',
+        "{...getRootProps()}",
+    ]
+
+    # input, button and text inside of box
+    [input, button, text] = box["children"]
+    assert input["name"] == "Input"
+    assert input["props"] == ['type="file"', "{...getInputProps()}"]
+
+    assert button["name"] == "Button"
+    assert button["children"][0]["contents"] == "{`select file`}"
+
+    assert text["name"] == "Text"
     assert (
-        str(upload_component) == f"<ReactDropzone multiple={{true}}{os.linesep}"
-        "onDrop={e => File(e)}>{({getRootProps, getInputProps}) => (<Box "
-        'sx={{"border": "1px dotted black"}}{...getRootProps()}><Input '
-        f'type="file"{{...getInputProps()}}/>{os.linesep}'
-        f"<Button>{{`select file`}}</Button>{os.linesep}"
-        "<Text>{`Drag and drop files here or click to select "
-        "files`}</Text></Box>)}</ReactDropzone>"
+        text["children"][0]["contents"]
+        == "{`Drag and drop files here or click to select files`}"
     )
 
 
@@ -66,14 +86,11 @@ def test_upload_component_with_props_render(upload_component_with_props):
     Args:
         upload_component_with_props: component fixture
     """
-    assert (
-        str(upload_component_with_props) == f"<ReactDropzone maxFiles={{2}}{os.linesep}"
-        f"multiple={{true}}{os.linesep}"
-        f"noDrag={{true}}{os.linesep}"
-        "onDrop={e => File(e)}>{({getRootProps, getInputProps}) => (<Box "
-        'sx={{"border": "1px dotted black"}}{...getRootProps()}><Input '
-        f'type="file"{{...getInputProps()}}/>{os.linesep}'
-        f"<Button>{{`select file`}}</Button>{os.linesep}"
-        "<Text>{`Drag and drop files here or click to select "
-        "files`}</Text></Box>)}</ReactDropzone>"
-    )
+    uplaod = upload_component_with_props.render()
+
+    assert uplaod["props"] == [
+        "maxFiles={2}",
+        "multiple={true}",
+        "noDrag={true}",
+        "onDrop={e => File(e)}",
+    ]
diff --git a/tests/components/layout/test_cond.py b/tests/components/layout/test_cond.py
index a16b6ab027f..ac0a568d9bf 100644
--- a/tests/components/layout/test_cond.py
+++ b/tests/components/layout/test_cond.py
@@ -38,12 +38,27 @@ def test_validate_cond(cond_state: pc.Var):
         Text.create("cond is True"),
         Text.create("cond is False"),
     )
+    cond_dict = cond_component.render() if type(cond_component) == Fragment else {}
+    assert cond_dict["name"] == "Fragment"
 
-    assert str(cond_component) == (
-        "<Fragment>{isTrue(cond_state.value) ? "
-        "<Fragment><Text>{`cond is True`}</Text></Fragment> : "
-        "<Fragment><Text>{`cond is False`}</Text></Fragment>}</Fragment>"
-    )
+    [condition] = cond_dict["children"]
+    assert condition["cond_state"] == "isTrue(cond_state.value)"
+
+    # true value
+    true_value = condition["true_value"]
+    assert true_value["name"] == "Fragment"
+
+    [true_value_text] = true_value["children"]
+    assert true_value_text["name"] == "Text"
+    assert true_value_text["children"][0]["contents"] == "{`cond is True`}"
+
+    # false value
+    false_value = condition["false_value"]
+    assert false_value["name"] == "Fragment"
+
+    [false_value_text] = false_value["children"]
+    assert false_value_text["name"] == "Text"
+    assert false_value_text["children"][0]["contents"] == "{`cond is False`}"
 
 
 @pytest.mark.parametrize(
diff --git a/tests/components/test_tag.py b/tests/components/test_tag.py
index ee0bda1e6d3..805651cd6c6 100644
--- a/tests/components/test_tag.py
+++ b/tests/components/test_tag.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict
+from typing import Any, Dict, List
 
 import pytest
 
@@ -71,25 +71,24 @@ def test_format_prop(prop: Var, formatted: str):
 
 
 @pytest.mark.parametrize(
-    "props,formatted",
+    "props,test_props",
     [
-        ({}, ""),
-        ({"key": 1}, "key={1}"),
-        ({"key": "value"}, 'key="value"'),
-        ({"key": True, "key2": "value2"}, 'key={true}\nkey2="value2"'),
+        ({}, []),
+        ({"key": 1}, ["key={1}"]),
+        ({"key": "value"}, ['key="value"']),
+        ({"key": True, "key2": "value2"}, ["key={true}", 'key2="value2"']),
     ],
 )
-def test_format_props(props: Dict[str, Var], formatted: str, windows_platform: bool):
+def test_format_props(props: Dict[str, Var], test_props: List):
     """Test that the formatted props are correct.
 
     Args:
         props: The props to test.
-        formatted: The expected formatted props.
-        windows_platform: Whether the system is windows.
+        test_props: The expected props.
     """
-    assert Tag(props=props).format_props() == (
-        formatted.replace("\n", "\r\n") if windows_platform else formatted
-    )
+    tag_props = Tag(props=props).format_props()
+    for i, tag_prop in enumerate(tag_props):
+        assert tag_prop == test_props[i]
 
 
 @pytest.mark.parametrize(
@@ -126,13 +125,20 @@ def test_add_props():
 @pytest.mark.parametrize(
     "tag,expected",
     [
-        (Tag(), "</>"),
-        (Tag(name="br"), "<br/>"),
-        (Tag(contents="hello"), "<>hello</>"),
-        (Tag(name="h1", contents="hello"), "<h1>hello</h1>"),
+        (Tag(), {"name": "", "contents": "", "props": {}}),
+        (Tag(name="br"), {"name": "br", "contents": "", "props": {}}),
+        (Tag(contents="hello"), {"name": "", "contents": "hello", "props": {}}),
+        (
+            Tag(name="h1", contents="hello"),
+            {"name": "h1", "contents": "hello", "props": {}},
+        ),
         (
             Tag(name="box", props={"color": "red", "textAlign": "center"}),
-            '<box color="red"\ntextAlign="center"/>',
+            {
+                "name": "box",
+                "contents": "",
+                "props": {"color": "red", "textAlign": "center"},
+            },
         ),
         (
             Tag(
@@ -140,30 +146,44 @@ def test_add_props():
                 props={"color": "red", "textAlign": "center"},
                 contents="text",
             ),
-            '<box color="red"\ntextAlign="center">text</box>',
+            {
+                "name": "box",
+                "contents": "text",
+                "props": {"color": "red", "textAlign": "center"},
+            },
         ),
     ],
 )
-def test_format_tag(tag: Tag, expected: str, windows_platform: bool):
-    """Test that the formatted tag is correct.
+def test_format_tag(tag: Tag, expected: dict):
+    """Test that the tag dict is correct.
 
     Args:
         tag: The tag to test.
-        expected: The expected formatted tag.
-        windows_platform: Whether the system is windows.
+        expected: The expected tag dictionary.
     """
-    expected = expected.replace("\n", "\r\n") if windows_platform else expected
-    assert str(tag) == expected
+    tag_dict = dict(tag)
+    assert tag_dict["name"] == expected["name"]
+    assert tag_dict["contents"] == expected["contents"]
+    assert tag_dict["props"] == expected["props"]
 
 
 def test_format_cond_tag():
-    """Test that the formatted cond tag is correct."""
+    """Test that the cond tag dict is correct."""
     tag = CondTag(
-        true_value=str(Tag(name="h1", contents="True content")),
-        false_value=str(Tag(name="h2", contents="False content")),
+        true_value=dict(Tag(name="h1", contents="True content")),
+        false_value=dict(Tag(name="h2", contents="False content")),
         cond=BaseVar(name="logged_in", type_=bool),
     )
-    assert (
-        str(tag)
-        == "{isTrue(logged_in) ? <h1>True content</h1> : <h2>False content</h2>}"
+    tag_dict = dict(tag)
+    cond, true_value, false_value = (
+        tag_dict["cond"],
+        tag_dict["true_value"],
+        tag_dict["false_value"],
     )
+    assert cond == "logged_in"
+
+    assert true_value["name"] == "h1"
+    assert true_value["contents"] == "True content"
+
+    assert false_value["name"] == "h2"
+    assert false_value["contents"] == "False content"
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 423941b4ae0..7441eb9335a 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -334,7 +334,7 @@ def test_create_config(app_name, expected_config_name, mocker):
     mocker.patch("builtins.open")
     tmpl_mock = mocker.patch("pynecone.compiler.templates.PCCONFIG")
     prerequisites.create_config(app_name)
-    tmpl_mock.format.assert_called_with(
+    tmpl_mock.render.assert_called_with(
         app_name=app_name, config_name=expected_config_name
     )
 


Code review comments:
diff_hunk: @@ -117,55 +119,22 @@ def format_prop(
         assert isinstance(prop, str), "The prop must be a string."
         return format.wrap(prop, "{", check_first=False)
 
-    def format_props(self) -> str:
+    def format_props(self) -> list:
path: pynecone/components/tags/tag.py
text: @user1:
we should use `typing.List` to support python3.7

diff_hunk: @@ -126,44 +125,65 @@ def test_add_props():
 @pytest.mark.parametrize(
     "tag,expected",
     [
-        (Tag(), "</>"),
-        (Tag(name="br"), "<br/>"),
-        (Tag(contents="hello"), "<>hello</>"),
-        (Tag(name="h1", contents="hello"), "<h1>hello</h1>"),
+        (Tag(), {"name": "", "contents": "", "props": {}}),
+        (Tag(name="br"), {"name": "br", "contents": "", "props": {}}),
+        (Tag(contents="hello"), {"name": "", "contents": "hello", "props": {}}),
+        (
+            Tag(name="h1", contents="hello"),
+            {"name": "h1", "contents": "hello", "props": {}},
+        ),
         (
             Tag(name="box", props={"color": "red", "textAlign": "center"}),
-            '<box color="red"\ntextAlign="center"/>',
+            {
+                "name": "box",
+                "contents": "",
+                "props": {"color": "red", "textAlign": "center"},
+            },
         ),
         (
             Tag(
                 name="box",
                 props={"color": "red", "textAlign": "center"},
                 contents="text",
             ),
-            '<box color="red"\ntextAlign="center">text</box>',
+            {
+                "name": "box",
+                "contents": "text",
+                "props": {"color": "red", "textAlign": "center"},
+            },
         ),
     ],
 )
-def test_format_tag(tag: Tag, expected: str, windows_platform: bool):
-    """Test that the formatted tag is correct.
+def test_format_tag(tag: Tag, expected: dict):
path: tests/components/test_tag.py
text: @user1:
use typing.Dict