Skip to content

Fix initial props reset back #3197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).

## [3.0.0-rc4] - 2025-03-04

## Fixed

- [#3197](https://github.com/plotly/dash/pull/3197) Fix initial props not updated in setProps causing the initial value of props to not be able to be set again.
- [#3183](https://github.com/plotly/dash/pull/3183) Fix external wrapper requiring id.
- [#3184](https://github.com/plotly/dash/pull/3184) Fix devtools dark mode button color issue and other ui fixes for the version checker.

## Changed

- [#3183](https://github.com/plotly/dash/pull/3183) Change ExternalWrapper props to component, componentPath.
- [#3197](https://github.com/plotly/dash/pull/3197) Improved layout path sum stringify of paths.

## [3.0.0-rc3] - 2025-02-21

## Added
Expand Down
4 changes: 2 additions & 2 deletions components/dash-core-components/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion components/dash-core-components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dash-core-components",
"version": "3.0.1",
"version": "3.0.2",
"description": "Core component suite for Dash",
"repository": {
"type": "git",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const Dropdown = props => {
search_value,
style,
value,
searchable,
} = props;
const [optionsCheck, setOptionsCheck] = useState(null);
const persistentOptions = useRef(null);
Expand Down Expand Up @@ -157,7 +158,7 @@ const Dropdown = props => {
options={sanitizedOptions}
value={value}
onChange={onChange}
onInputChange={onInputChange}
onInputChange={searchable ? onInputChange : undefined}
backspaceRemoves={clearable}
deleteRemoves={clearable}
inputProps={{autoComplete: 'off'}}
Expand Down
4 changes: 2 additions & 2 deletions dash/_dash_renderer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os

__version__ = "2.0.2"
__version__ = "2.0.3"

_available_react_versions = {"18.3.1", "18.2.0", "16.14.0"}
_available_reactdom_versions = {"18.3.1", "18.2.0", "16.14.0"}
Expand Down Expand Up @@ -64,7 +64,7 @@ def _set_react_version(v_react, v_reactdom=None):
{
"relative_package_path": "dash-renderer/build/dash_renderer.min.js",
"dev_package_path": "dash-renderer/build/dash_renderer.dev.js",
"external_url": "https://unpkg.com/[email protected].2"
"external_url": "https://unpkg.com/[email protected].3"
"/build/dash_renderer.min.js",
"namespace": "dash",
},
Expand Down
4 changes: 2 additions & 2 deletions dash/dash-renderer/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dash/dash-renderer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dash-renderer",
"version": "2.0.2",
"version": "2.0.3",
"description": "render dash components in react",
"main": "build/dash_renderer.min.js",
"scripts": {
Expand Down
7 changes: 4 additions & 3 deletions dash/dash-renderer/src/reducers/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import layout from './layout';
import paths from './paths';
import callbackJobs from './callbackJobs';
import loading from './loading';
import {stringifyPath} from '../wrapper/wrapping';

export const apiRequests = [
'dependenciesRequest',
Expand All @@ -36,9 +37,9 @@ function layoutHashes(state = {}, action) {
) {
// Let us compare the paths sums to get updates without triggering
// render on the parent containers.
const jsonPath = JSON.stringify(action.payload.itempath);
const prev = pathOr(0, [jsonPath], state);
return assoc(jsonPath, prev + 1, state);
const strPath = stringifyPath(action.payload.itempath);
const prev = pathOr(0, [strPath], state);
return assoc(strPath, prev + 1, state);
}
return state;
}
Expand Down
89 changes: 47 additions & 42 deletions dash/dash-renderer/src/wrapper/DashWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {DashConfig} from '../config';
import {notifyObservers, onError, updateProps} from '../actions';
import {getWatchedKeys, stringifyId} from '../actions/dependencies';
import {recordUiEdit} from '../persistence';
import {createElement, isDryComponent} from './wrapping';
import {createElement, getComponentLayout, isDryComponent} from './wrapping';
import Registry from '../registry';
import isSimpleComponent from '../isSimpleComponent';
import {
Expand Down Expand Up @@ -62,56 +62,61 @@ function DashWrapper({
const setProps = (newProps: UpdatePropsPayload) => {
const {id} = componentProps;
const {_dash_error, ...restProps} = newProps;
const oldProps = componentProps;
const changedProps = pickBy(
(val, key) => !equals(val, oldProps[key]),
restProps
);
if (_dash_error) {
dispatch(
onError({
type: 'frontEnd',
error: _dash_error
})

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
dispatch((dispatch, getState) => {
const currentState = getState();
const {graphs} = currentState;

const {props: oldProps} = getComponentLayout(
componentPath,
currentState
);
}
if (!isEmpty(changedProps)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
dispatch((dispatch, getState) => {
const {graphs} = getState();
// Identify the modified props that are required for callbacks
const watchedKeys = getWatchedKeys(
id,
keys(changedProps),
graphs
const changedProps = pickBy(
(val, key) => !equals(val, oldProps[key]),
restProps
);
if (_dash_error) {
dispatch(
onError({
type: 'frontEnd',
error: _dash_error
})
);
}

batch(() => {
// setProps here is triggered by the UI - record these changes
// for persistence
recordUiEdit(component, newProps, dispatch);
if (isEmpty(changedProps)) {
return;
}

// Only dispatch changes to Dash if a watched prop changed
if (watchedKeys.length) {
dispatch(
notifyObservers({
id,
props: pick(watchedKeys, changedProps)
})
);
}
// Identify the modified props that are required for callbacks
const watchedKeys = getWatchedKeys(id, keys(changedProps), graphs);

batch(() => {
// setProps here is triggered by the UI - record these changes
// for persistence
recordUiEdit(component, newProps, dispatch);

// Always update this component's props
// Only dispatch changes to Dash if a watched prop changed
if (watchedKeys.length) {
dispatch(
updateProps({
props: changedProps,
itempath: componentPath
notifyObservers({
id,
props: pick(watchedKeys, changedProps)
})
);
});
}

// Always update this component's props
dispatch(
updateProps({
props: changedProps,
itempath: componentPath
})
);
});
}
});
};

const createContainer = useCallback(
Expand Down
12 changes: 5 additions & 7 deletions dash/dash-renderer/src/wrapper/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import {path} from 'ramda';

import {DashLayoutPath, DashComponent, BaseDashProps} from '../types/component';
import {getComponentLayout, stringifyPath} from './wrapping';

type SelectDashProps = [DashComponent, BaseDashProps, number];

export const selectDashProps =
(componentPath: DashLayoutPath) =>
(state: any): SelectDashProps => {
const c = path(componentPath, state.layout) as DashComponent;
const c = getComponentLayout(componentPath, state);
// Layout hashes records the number of times a path has been updated.
// sum with the parents hash (match without the last ']') to get the real hash
// Then it can be easily compared without having to compare the props.
let jsonPath = JSON.stringify(componentPath);
jsonPath = jsonPath.substring(0, jsonPath.length - 1);
const strPath = stringifyPath(componentPath);

const h = Object.entries(state.layoutHashes).reduce(
(acc, [path, pathHash]) =>
jsonPath.startsWith(path.substring(0, path.length - 1))
(acc, [updatedPath, pathHash]) =>
strPath.startsWith(updatedPath)
? (pathHash as number) + acc
: acc,
0
Expand Down
14 changes: 13 additions & 1 deletion dash/dash-renderer/src/wrapper/wrapping.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import {mergeRight, type, has} from 'ramda';
import {mergeRight, path, type, has, join} from 'ramda';
import {DashComponent, DashLayoutPath} from '../types/component';

export function createElement(
element: any,
Expand Down Expand Up @@ -49,3 +50,14 @@ export function validateComponent(componentDefinition: any) {
);
}
}

export function stringifyPath(layoutPath: DashLayoutPath) {
return join(',', layoutPath);
}

export function getComponentLayout(
componentPath: DashLayoutPath,
state: any
): DashComponent {
return path(componentPath, state.layout) as DashComponent;
}
2 changes: 1 addition & 1 deletion dash/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "3.0.0rc3"
__version__ = "3.0.0rc4"
2 changes: 2 additions & 0 deletions tests/integration/renderer/test_persistence.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from multiprocessing import Value
import flaky
import pytest
import time

Expand Down Expand Up @@ -206,6 +207,7 @@ def toggle_table(n):
check_table_names(dash_duo, ["a", "b"])


@flaky.flaky(max_runs=3)
def test_rdps005_persisted_props(dash_duo):
app = Dash(__name__)
app.layout = html.Div(
Expand Down
34 changes: 34 additions & 0 deletions tests/integration/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,3 +472,37 @@ def my_route_f():
response = requests.post(url)
assert response.status_code == 200
assert response.text == "hello"


def test_inin031_initial_value_set_back(dash_duo):
# Test for regression on the initial value to be able to
# set back to initial after changing again.
app = Dash(__name__)

app.layout = html.Div(
[
dcc.Dropdown(
id="dropdown",
options=["Toronto", "Montréal", "Vancouver"],
value="Toronto",
searchable=False,
),
html.Div(id="output"),
]
)

@app.callback(Output("output", "children"), [Input("dropdown", "value")])
def callback(value):
return f"You have selected {value}"

dash_duo.start_server(app)

dash_duo.wait_for_text_to_equal("#output", "You have selected Toronto")

dash_duo.select_dcc_dropdown("#dropdown", "Vancouver")
dash_duo.wait_for_text_to_equal("#output", "You have selected Vancouver")

dash_duo.select_dcc_dropdown("#dropdown", "Toronto")
dash_duo.wait_for_text_to_equal("#output", "You have selected Toronto")

assert dash_duo.get_logs() == []