-
-
Notifications
You must be signed in to change notification settings - Fork 143
Make Store component more robust #792
Changes from all commits
be2f01f
5b0b5de
18af043
c3e0493
2b2d6fa
d771fc7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,46 +1,7 @@ | ||
import {any, includes, isNil, type} from 'ramda'; | ||
import {equals, isNil} from 'ramda'; | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
|
||
/** | ||
* Deep equality check to know if the data has changed. | ||
* | ||
* @param {*} newData - New data to compare | ||
* @param {*} oldData - The old data to compare | ||
* @returns {boolean} The data has changed. | ||
*/ | ||
function dataChanged(newData, oldData) { | ||
const oldNull = isNil(oldData); | ||
const newNull = isNil(newData); | ||
if (oldNull || newNull) { | ||
return newData !== oldData; | ||
} | ||
const newType = type(newData); | ||
if (newType !== type(oldData)) { | ||
return true; | ||
} | ||
if (newType === 'Array') { | ||
if (newData.length !== oldData.length) { | ||
return true; | ||
} | ||
for (let i = 0; i < newData.length; i++) { | ||
if (dataChanged(newData[i], oldData[i])) { | ||
return true; | ||
} | ||
} | ||
} else if (includes(newType, ['String', 'Number', 'Boolean'])) { | ||
return oldData !== newData; | ||
} else if (newType === 'Object') { | ||
const oldEntries = Object.entries(oldData); | ||
const newEntries = Object.entries(newData); | ||
if (oldEntries.length !== newEntries.length) { | ||
return true; | ||
} | ||
return any(([k, v]) => dataChanged(v, oldData[k]))(newEntries); | ||
} | ||
return false; | ||
} | ||
|
||
/** | ||
* Abstraction for the memory storage_type to work the same way as local/session | ||
* | ||
|
@@ -88,7 +49,13 @@ class WebStore { | |
} | ||
|
||
getItem(key) { | ||
return JSON.parse(this._storage.getItem(key)); | ||
try { | ||
return JSON.parse(this._storage.getItem(key)); | ||
} catch (e) { | ||
// in case we somehow got a non-JSON value in storage, | ||
// just ignore it. | ||
return null; | ||
alexcjohnson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
setItem(key, value) { | ||
|
@@ -150,7 +117,7 @@ export default class Store extends React.Component { | |
} | ||
|
||
const old = this._backstore.getItem(id); | ||
if (isNil(old) && data) { | ||
if (isNil(old) && !isNil(data)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
// Initial data mount | ||
this._backstore.setItem(id, data); | ||
setProps({ | ||
|
@@ -159,7 +126,7 @@ export default class Store extends React.Component { | |
return; | ||
} | ||
|
||
if (dataChanged(old, data)) { | ||
if (!equals(old, data)) { | ||
setProps({ | ||
data: old, | ||
modified_timestamp: this._backstore.getModified(id), | ||
|
@@ -186,11 +153,19 @@ export default class Store extends React.Component { | |
} | ||
const old = this._backstore.getItem(id); | ||
// Only set the data if it's not the same data. | ||
if (dataChanged(data, old)) { | ||
this._backstore.setItem(id, data); | ||
setProps({ | ||
modified_timestamp: this._backstore.getModified(id), | ||
}); | ||
// If the new data is undefined, we got here by overwriting the entire | ||
// component with a new copy that has no `data` specified - so pull back | ||
// out the old value. | ||
// Note: this still allows you to set data to null | ||
if (!equals(data, old)) { | ||
if (data === undefined) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I bet this would all be even simpler if we moved to a functional component with If you actually remove it completely and bring it back later, it already worked, via the |
||
setProps({data: old}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Running this against a modified version of https://dash.plotly.com/dash-core-components/store, I expected to be able to rename the Turns out the callback triggered by https://github.com/plotly/dash-docs/blob/master/dash_docs/chapters/dash_core_components/Store/examples/store_clicks.py#L74 is not triggered on load, resulting in all field having value Possibly outside the scope of this PR, but a close neighbour. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Can you post the exact app you're using? The app seems to behave correctly when I try it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Running from the docs entry point Prior to changes,
Here's the modified code for
Running this against latest of Running There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. got it, thanks! Investigating... Here's a standalone version that shows the bug, just wrapping the layout in a content callback: import dash
import dash_html_components as html
import dash_core_components as dcc # noxx-exec
from dash.dependencies import Output, Input, State
from dash.exceptions import PreventUpdate
# This stylesheet makes the buttons and table pretty.
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(
__name__,
external_stylesheets=external_stylesheets,
suppress_callback_exceptions=True
)
layout = html.Div([
# The memory store reverts to the default on every page refresh
dcc.Store(id='memory'), # noxx-exec - stores are loaded from the beginning
# The local store will take the initial data
# only the first time the page is loaded
# and keep it until it is cleared.
dcc.Store(id='local', storage_type='local'), # noxx-exec
# Same as the local store but will lose the data
# when the browser/tab closes.
dcc.Store(id='session', storage_type='session'), # noxx-exec
html.Table([
html.Thead([
html.Tr(html.Th('Click to store in:', colSpan="3")),
html.Tr([
html.Th(html.Button('memory', id='memory-button')),
html.Th(html.Button('localStorage', id='local-button')),
html.Th(html.Button('sessionStorage', id='session-button'))
]),
html.Tr([
html.Th('Memory clicks'),
html.Th('Local clicks'),
html.Th('Session clicks')
])
]),
html.Tbody([
html.Tr([
html.Td(0, id='memory-clicks'),
html.Td(0, id='local-clicks'),
html.Td(0, id='session-clicks')
])
])
])
])
app.layout = html.Div(id="content")
@app.callback(Output("content", "children"), [Input("content", "style")])
def content(_):
return layout
# Create two callback for every store.
for store in ('memory', 'local', 'session'):
# add a click to the appropriate store.
@app.callback(Output(store, 'data'),
[Input('{}-button'.format(store), 'n_clicks')],
[State(store, 'data')])
def on_click(n_clicks, data):
if n_clicks is None:
# prevent the None callbacks is important with the store component.
# you don't want to update the store for nothing.
raise PreventUpdate
# Give a default data dict with 0 clicks if there's no data.
data = data or {'clicks': 0}
data['clicks'] = data['clicks'] + 1
return data
# output the stored clicks in the table cell.
@app.callback(Output('{}-clicks'.format(store), 'children'),
# Since we use the data prop in an output,
# we cannot get the initial data on load with the data prop.
# To counter this, you can use the modified_timestamp
# as Input and the data as State.
# This limitation is due to the initial None callbacks
# https://github.com/plotly/dash-renderer/pull/81
[Input(store, 'modified_timestamp')],
[State(store, 'data')])
def on_data(ts, data):
if ts is None:
raise PreventUpdate
data = data or {}
return data.get('clicks', 0)
if __name__ == '__main__':
app.run_server(debug=True) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ha, this is actually yet another renderer bug... PR coming in Dash as soon as I figure out how to write a test for it! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in plotly/dash#1224 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://github.com/plotly/dash-docs/pull/871 shows that between this PR and plotly/dash#1224 there are no issues anymore running the store examples unmodified within the docs. |
||
} else { | ||
this._backstore.setItem(id, data); | ||
setProps({ | ||
modified_timestamp: this._backstore.getModified(id), | ||
}); | ||
} | ||
} | ||
} | ||
|
||
|
Uh oh!
There was an error while loading. Please reload this page.