在 Plotly Dash 中,每个回调函数都应当是纯函数,它只根据输入参数计算并返回输出,自身不保存状态。这意味着一个按钮回调解析出来的数据,很难在下一个图表回调中直接复用。采用 dcc.Store 可以把中间结果以 JSON 形式暂存在浏览器端,由一个回调负责写入,另一个回调负责读取,从而让跨回调的数据传递变得清晰可控。

与隐藏的 html.Div 存数据相比,dcc.Store 的字段类型更明确,不会自动被转成字符串;与全局变量相比,它不依赖进程内状态,在多进程部署下行为一致。下面从存储类型、基础回调链、避坑要点和实战案例几个角度展开。
一、dcc.Store的存储类型与生命周期
dcc.Store 在布局中是不可见组件,核心属性只有 id、storage_type 和 data。storage_type 决定数据存在哪里以及能活多久。默认的 memory 模式把数据放在当前页面的内存里,用户刷新页面后数据就会消失;session 模式则保存在当前标签页的会话存储中,刷新后仍可读取,但关闭标签页后失效;local 模式使用浏览器的本地存储,即使关闭浏览器再打开也能恢复,适合保存用户偏好设置。
选择哪种模式要结合业务敏感度和数据量。临时计算结果、只服务于本次页面交互的中间值,用 memory 最合适,性能也好;需要跨刷新保持状态时用 session;需要持久化且数据不敏感时用 local。无论哪种模式,data 都必须是可 JSON 序列化对象,不能直接塞入 pandas DataFrame、日期对象或自定义类实例。
下面是一个简单的布局配置,它加入了一个内存型 Store,不会在页面上产生任何可见元素:
import dash
from dash import dcc, html
app = dash.Dash(__name__)
app.layout = html.Div([
dcc.Store(id='user-session', storage_type='session'),
dcc.Store(id='temp-result', storage_type='memory'),
html.Button('点击写入', id='write-btn', n_clicks=0),
html.Div(id='read-area')
])
这个布局中 user-session 用来保存刷新后仍要存在的数据,temp-result 用来保存临时结果。它们的 data 初始都是 None,直到回调函数首次返回有效数据。
二、基础写法:写入、读取与回调链
跨回调传递数据的最基本模式是「一写一读」。写入回调把 Output 指向 Store 的 data,读取回调把 Store 的 data 作为 Input。当写入回调执行完成后,data 发生变化,所有依赖该 Store 的回调会被自动触发。
from dash import dcc, html, Input, Output, State
from datetime import datetime
app.layout = html.Div([
dcc.Input(id='input-text', value='hello'),
html.Button('保存到Store', id='save-btn', n_clicks=0),
dcc.Store(id='text-store', storage_type='memory'),
html.Div(id='output-display')
])
@app.callback(
Output('text-store', 'data'),
Input('save-btn', 'n_clicks'),
State('input-text', 'value')
)
def save_to_store(n_clicks, value):
if n_clicks == 0:
raise dash.exceptions.PreventUpdate
return {
'text': value,
'saved_at': datetime.now().isoformat()
}
@app.callback(
Output('output-display', 'children'),
Input('text-store', 'data')
)
def display_from_store(data):
if data is None:
return '尚未保存'
return f"已保存内容:{data['text']},时间:{data['saved_at']}"
第一个回调在按钮点击后把输入框内容和时间戳写入 Store,第二个回调监听 Store 数据并渲染到页面。注意写入回调里使用 State('input-text', 'value') 而不是 Input,因为输入框的内容变化本身不应触发保存,只有按钮点击才应该触发写入。读取回调中使用 Input('text-store', 'data'),这样数据更新后显示区域会自动刷新。
这个模式可以继续扩展成多级回调链:回调 A 写入 Store,回调 B 读取后计算并写入另一个 Store,回调 C 再读取最终结果渲染。只要依赖关系不形成环路,Dash 会按拓扑顺序执行。复杂的链式处理建议拆分成多个小回调,每个回调只做一件事,便于调试和复用。
三、避免循环触发与数据合并的常见技巧
如果把同一个 Store 同时作为 Input 和 Output,Dash 会直接报错,因为这会产生明确的循环依赖。更常见的情况是:需要读取当前 Store 中的旧值,执行追加或更新后写回。这时应使用 State 读取旧值,用另一个组件的 Input 触发回调。这样写回不会再次触发当前回调,避免无限循环。
@app.callback(
Output('record-store', 'data'),
Input('add-btn', 'n_clicks'),
State('record-store', 'data'),
State('new-value', 'value')
)
def append_record(n_clicks, existing, new_value):
if n_clicks == 0:
raise dash.exceptions.PreventUpdate
records = existing or []
records.append({
'value': new_value,
'count': n_clicks
})
return records
这个回调通过 State('record-store', 'data') 拿到旧列表,追加新元素后写回同一个 Store。触发条件是按钮点击,不会因为 Store 本身变化而再次运行。还要注意 records = existing or [] 这种写法,因为 Store 初始 data 可能是 None,直接调用 append 会报错。
另一个容易忽略的问题是数据覆盖。多个写入回调如果共享同一个 Store,后执行的写入会覆盖前面结果。此时应把不同业务数据放入不同 Store,或者在单个写入回调中合并所有需要保存的字段。不要把 Store 当成全局数据库使用,数据量较大时频繁写入和序列化会明显增加页面响应延迟。
四、实战:上传文件解析后跨回调复用数据
一个典型场景是 CSV 文件上传:用户上传文件后,先解析内容并存入 Store,后续图表回调和统计回调都从这个 Store 读取,避免每个图表都重新解析一遍文件。下面代码将 dcc.Upload 与 dcc.Store 配合,实现「一次解析、多处使用」。
import base64
import io
import pandas as pd
from dash import dcc, html, Input, Output, State
app.layout = html.Div([
dcc.Upload(
id='upload-data',
children=html.Button('上传CSV'),
multiple=False
),
dcc.Store(id='parsed-store', storage_type='memory'),
dcc.Graph(id='line-plot'),
dcc.Graph(id='summary-graph')
])
def parse_csv(contents, filename):
content_type, content_string = contents.split(',')
decoded = base64.b64decode(content_string)
df = pd.read_csv(io.StringIO(decoded.decode('utf-8')))
return {
'records': df.to_dict('records'),
'filename': filename
}
@app.callback(
Output('parsed-store', 'data'),
Input('upload-data', 'contents'),
State('upload-data', 'filename')
)
def store_upload(contents, filename):
if contents is None:
raise dash.exceptions.PreventUpdate
return parse_csv(contents, filename)
@app.callback(
Output('line-plot', 'figure'),
Input('parsed-store', 'data')
)
def draw_line(data):
if data is None:
return {}
df = pd.DataFrame(data['records'])
return {
'data': [
{'x': df['date'], 'y': df['value'], 'type': 'line'}
],
'layout': {'title': data['filename']}
}
@app.callback(
Output('summary-graph', 'figure'),
Input('parsed-store', 'data')
)
def draw_summary(data):
if data is None:
return {}
df = pd.DataFrame(data['records'])
stats = df.groupby('category')['value'].sum().reset_index()
return {
'data': [
{'x': stats['category'], 'y': stats['value'], 'type': 'bar'}
],
'layout': {'title': '分类汇总'}
}
文件上传后,第一个回调把解析结果保存到 parsed-store。两个图表回调都把 parsed-store 的 data 作为 Input,数据一更新就同时刷新。由于两个图表的计算都依赖同一个解析结果,修改解析逻辑时只需要改 parse_csv 函数,不会影响图表渲染。
这种做法的优势在于减少重复计算和降低耦合。上传文件中的原始字节经过一次解析后变成结构化字典,后续逻辑不再依赖 dcc.Upload 的 contents 字符串。对于更大的数据集,可以先在回调里做筛选或聚合,只把必要字段写入 Store,避免浏览器端数据过大影响性能。浏览器存储适合中小规模数据,如果单次数据超过数 MB,建议改为服务端缓存或数据库。
总体来看,dcc.Store 是 Dash 中处理跨回调状态共享的基础组件。它把「写入-读取」模式标准化,配合 State 与 PreventUpdate 可以避免大多数循环更新问题。只要控制好数据量、合理拆分 Store、设计清晰的依赖链,就能在不引入外部状态管理库的情况下,构建出稳定且易于维护的交互式应用。
Plotly Dashdcc.Store跨回调数据传递修改时间:2026-09-17 20:36:33