Repository: gradio-app/gradio
Pull request: Preserve value in reload mode
We had a regression where values of compnents were not preserved in reload mode, so when you edited the source file, values would be reset to empty. This is fixed. 

Previously, state was not preserved in reload mode either, a separate bug. Also fixed.

Fixes: #8855 
Fixes: https://github.com/gradio-app/gradio/issues/8641

Problem statement:
Reload mode should preserve the selected tab
Gradio's reload mode currently preserves some selected values, such as a the selected radio button. However, it doesn't preserve the selected tab, which makes it frustrating to use in applications with multiple tabs, i.e. if you are working on the content for the second tab, then you'll need to keep selecting it to preview your changes. 

Reload mode should preserve the selected tab as well.

Reload mode should preserve states
- [x] I have searched to see if a similar issue already exists.


**Is your feature request related to a problem? Please describe.**  
I have some components where input variables are specified, they're generated by a `gr.render` decorator though, so the actual values are stored in a state. The state does not persist reloads, and I need to input the values every time it reloads.

The same does not hold for normal textboxes – those persist just fine.

**Describe the solution you'd like**  
Upon hot reload, `gr.State` values should stay what they are. Perhaps a `key` argument should be added to `gr.State` to facilitate this.

Hints from issue discussion:
reload mode disapproves of our tabs, maybe it will start working when we redesign them.
Yeah, this issue I feel needs to be taken seriously, and it becomes important when I have a lot of tabs.

Then getting this issue may needs to be looked at in conjunction with #10282
I have a practice I'm using that can solve both #8641 and #10282.  This way will not only allow lazy loading of the tab page, but will also keep the page on the current page after refreshing it.

--------

When you click on `test123`, you can switch between different pages through the top tab bar. Just add many different tabs in `demo_dict`. 
<img width="1920" alt="截屏2025-01-10 14 39 37" src="https://github.com/user-attachments/assets/f8a83a1d-7966-4295-88cb-bf28409c76fd" />

code this here
```python
from fastapi import FastAPI
import gradio as gr
from demo.demo_test import demo_test


demo_dict = {
    "test123": demo_test,
}

def create_demo(demo_func):
    with gr.Blocks() as blocks:
        LINK_STYLE = 'color: #4a5568; text-decoration: none; '

        links = [f'<a style="{LINK_STYLE}" href="/">首页</a>'] + [
            f'<a style="{LINK_STYLE}" href="/{func.__name__}">{name}</a>'
            for name, func in demo_dict.items()
        ]

        html_content = f"""
        <style>
            .nav-container {{
                background: white;
                position: fixed;
                top: 0;
                left: 0;
                right: 0;
                box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
                z-index: 1000;
                width: fit-content;
                margin: auto;
                border-radius: 5px;
            }}

            .nav-list {{
                display: flex;
                justify-content: center;
                gap: 5px;
            }}

            ul a {{
                display: inline-block;
                padding: 5px 6px;
                font-weight: 400;
                white-space: nowrap;  /* 确保文字不换行 */
                transition: all 0.3s ease;
            }}

            a:hover {{
                background-color: #f0f0f0;
                border-radius: 3px;
            }}
        </style>
        <div class="nav-container">
            <ul class="nav-list" style="font-size: small; list-style: none;">
                {''.join(links)}
            </ul>
        </div>
        """

        gr.HTML(html_content)
        demo_func()

        return blocks


app = FastAPI()

@app.get("/")
def route_main():
    return RedirectResponse(url="/demo_test")

for demo_name, demo_func in demo_dict.items():
    path = f"/{demo_func.__name__}"
    app = gr.mount_gradio_app(app,  create_demo(demo_func), path=path, ssr_mode=True)
```

meanwhile demo/demo_test.py like this
```python
import gradio as gr

def demo_test():
    with gr.Blocks() as block:
        gr.Textbox("hhhhh", label="Name")
        gr.Button("Submit")
    return block

```
Ah yeah I think adding `key` to `gr.State` like we have for the components makes sense -- cc @aliabid94 

btw @irgolic can you share a small code snippet so that we can use to test?
Sure, here's a quick demo:

```python
import gradio as gr


with gr.Blocks() as demo:
    my_state = gr.State([])

    @gr.render(inputs=[my_state])
    def _(rows):
        for r in rows:
            gr.Textbox(r)

    button = gr.Button("+")
    button.click(lambda l: l + ["row"], my_state, my_state)


demo.launch()
```
Any updates on this? Anything I can do to help expedite?
Hi @irgolic - we're busy preparing a 5.0 major release. Contributions on this are welcome - we'd be happy to review any PRs :) 🙏 
Hi @freddyaboulton, good luck with the release!

I'd love to contribute, could you review my other outstanding PR first?

Commit message: add changeset

Patch to review:
diff --git a/.changeset/metal-tables-beg.md b/.changeset/metal-tables-beg.md
new file mode 100644
index 0000000000..e1bde23b54
--- /dev/null
+++ b/.changeset/metal-tables-beg.md
@@ -0,0 +1,9 @@
+---
+"@gradio/client": patch
+"@gradio/core": patch
+"@self/app": patch
+"@self/spa": patch
+"gradio": patch
+---
+
+fix:Preserve value in reload mode
diff --git a/client/js/src/client.ts b/client/js/src/client.ts
index fbc7fce3fa..f69a9bdc8a 100644
--- a/client/js/src/client.ts
+++ b/client/js/src/client.ts
@@ -272,9 +272,13 @@ export class Client {
 		app_reference: string,
 		options: ClientOptions = {
 			events: ["data"]
-		}
+		},
+		session_hash: string | null = null
 	): Promise<Client> {
 		const client = new this(app_reference, options); // this refers to the class itself, not the instance
+		if (session_hash) {
+			client.session_hash = session_hash;
+		}
 		await client.init();
 		return client;
 	}
diff --git a/gradio/state_holder.py b/gradio/state_holder.py
index 2d8c07be9e..475d4ca6f1 100644
--- a/gradio/state_holder.py
+++ b/gradio/state_holder.py
@@ -25,12 +25,6 @@ def set_blocks(self, blocks: Blocks):
         blocks.state_holder = self
         self.capacity = blocks.state_session_capacity
 
-    def reset(self, blocks: Blocks):
-        """Reset the state holder with new blocks. Used during reload mode."""
-        self.session_data = OrderedDict()
-        # Call set blocks again to set new ids
-        self.set_blocks(blocks)
-
     def __getitem__(self, session_id: str) -> SessionState:
         if session_id not in self.session_data:
             self.session_data[session_id] = SessionState(self.blocks)
diff --git a/gradio/utils.py b/gradio/utils.py
index 972a0ffbad..46931631b5 100644
--- a/gradio/utils.py
+++ b/gradio/utils.py
@@ -129,7 +129,7 @@ def swap_blocks(self, demo: Blocks):
         demo.has_launched = True
         demo.max_file_size = self.running_app.blocks.max_file_size
         demo.is_running = True
-        self.running_app.state_holder.reset(demo)
+        self.running_app.state_holder.set_blocks(demo)
         self.running_app.blocks = demo
 
 
@@ -361,44 +361,52 @@ def _serialize(a: Any) -> bytes:
 
 
 def reassign_keys(old_blocks: Blocks, new_blocks: Blocks):
-    from gradio.blocks import BlockContext
+    from gradio.blocks import BlockContext, Block
 
-    assigned_keys = [
-        block.key for block in new_blocks.children if block.key is not None
+    new_keys = [
+        block.key for block in new_blocks.blocks.values() if block.key is not None
     ]
 
     def reassign_context_keys(
-        old_context: BlockContext | None, new_context: BlockContext
+        old_block: Block | None, new_block: Block, top_level=False,
     ):
-        for i, new_block in enumerate(new_context.children):
-            if old_context and i < len(old_context.children):
-                old_block = old_context.children[i]
+        same_block_type = old_block.__class__ == new_block.__class__
+        if new_block.key is None:
+            if (
+                same_block_type
+                and old_block is not None
+                and old_block.key not in new_keys
+                and deep_equal(
+                    getattr(old_block, "value", None),
+                    getattr(new_block, "value", None),
+                )
+            ):
+                new_block.key = old_block.key
             else:
-                old_block = None
-            if new_block.key is None:
-                if (
-                    old_block.__class__ == new_block.__class__
-                    and old_block is not None
-                    and old_block.key not in assigned_keys
-                    and deep_equal(
-                        getattr(old_block, "value", None),
-                        getattr(new_block, "value", None),
-                    )
-                ):
-                    new_block.key = old_block.key
-                else:
-                    new_block.key = f"__{new_block._id}__"
-
-            if isinstance(new_block, BlockContext):
-                if (
-                    isinstance(old_block, BlockContext)
-                    and old_block.__class__ == new_block.__class__
-                ):
-                    reassign_context_keys(old_block, new_block)
+                new_block.key = f"__{new_block._id}__"
+        
+        if old_block and same_block_type and old_block.key is not None and old_block.key == new_block.key:
+            if not top_level:
+                new_blocks.default_config.blocks[old_block._id] = new_blocks.default_config.blocks[new_block._id]
+                del new_blocks.default_config.blocks[new_block._id]
+            for fn in new_blocks.default_config.fns.values():
+                for target_idx, target in enumerate(fn.targets):
+                    if target[0] == new_block._id:
+                        fn.targets[target_idx] = (old_block._id, target[1])
+
+            new_block._id = old_block._id
+
+
+        if isinstance(new_block, BlockContext) and isinstance(old_block, BlockContext) and same_block_type:
+            for i, new_block_child in enumerate(new_block.children):
+                if i < len(old_block.children):
+                    old_block_child = old_block.children[i]
                 else:
-                    reassign_context_keys(None, new_block)
+                    old_block_child = None
+                reassign_context_keys(old_block_child, new_block_child)
 
-    reassign_context_keys(old_blocks, new_blocks)
+    old_blocks.key = new_blocks.key = "__blocks__"
+    reassign_context_keys(old_blocks, new_blocks, top_level=True)
 
 
 def colab_check() -> bool:
diff --git a/js/app/src/routes/[...catchall]/+page.svelte b/js/app/src/routes/[...catchall]/+page.svelte
index dc73d45893..549b8a9270 100644
--- a/js/app/src/routes/[...catchall]/+page.svelte
+++ b/js/app/src/routes/[...catchall]/+page.svelte
@@ -310,11 +310,15 @@
 				});
 				stream.addEventListener("reload", async (event) => {
 					app.close();
-					app = await Client.connect(data.api_url, {
-						status_callback: handle_status,
-						with_null_state: true,
-						events: ["data", "log", "status", "render"]
-					});
+					app = await Client.connect(
+						data.api_url,
+						{
+							status_callback: handle_status,
+							with_null_state: true,
+							events: ["data", "log", "status", "render"]
+						},
+						app.session_hash
+					);
 
 					if (!app.config) {
 						throw new Error("Could not resolve app config");
diff --git a/js/core/src/init.ts b/js/core/src/init.ts
index 32800e7126..82273c66ab 100644
--- a/js/core/src/init.ts
+++ b/js/core/src/init.ts
@@ -107,6 +107,15 @@ export function create_components(initial_layout: ComponentMeta | undefined): {
 		flush();
 		app = _app;
 
+		if (instance_map) {
+			// re-render in reload mode
+			components.forEach((c) => {
+				if (c.props.value == null && c.id in instance_map) {
+					c.props.value = instance_map[c.id].props.value;
+				}
+			});
+		}
+
 		_components = components;
 		inputs = new Set();
 		outputs = new Set();
diff --git a/js/spa/src/Index.svelte b/js/spa/src/Index.svelte
index f8b4c7d7bb..618f7579db 100644
--- a/js/spa/src/Index.svelte
+++ b/js/spa/src/Index.svelte
@@ -391,11 +391,15 @@
 				});
 				stream.addEventListener("reload", async (event) => {
 					app.close();
-					app = await Client.connect(api_url, {
-						status_callback: handle_status,
-						with_null_state: true,
-						events: ["data", "log", "status", "render"]
-					});
+					app = await Client.connect(
+						api_url,
+						{
+							status_callback: handle_status,
+							with_null_state: true,
+							events: ["data", "log", "status", "render"]
+						},
+						app.session_hash
+					);
 
 					if (!app.config) {
 						throw new Error("Could not resolve app config");


Code review comments:
diff_hunk: @@ -391,11 +391,15 @@
 				});
 				stream.addEventListener("reload", async (event) => {
 					app.close();
-					app = await Client.connect(api_url, {
-						status_callback: handle_status,
-						with_null_state: true,
-						events: ["data", "log", "status", "render"]
-					});
+					app = await Client.connect(
+						api_url,
+						{
path: js/spa/src/Index.svelte
text: @user1:
Nit: but seems better to pass the session hash with the ClientOptions