Skip to content
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

fix: ui.text_field value not changing with use_state #498

Merged
merged 10 commits into from
Jun 3, 2024

Conversation

AkshatJawne
Copy link
Contributor

Resolves #372

Changes Implemented:

  • Added a useEffect that updates the local value state in the TextField whenever the propValue changes, which is the value that would be changed using the use_state

@AkshatJawne AkshatJawne requested a review from mofojed May 28, 2024 14:37
@AkshatJawne AkshatJawne self-assigned this May 28, 2024
Copy link
Member

@mofojed mofojed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works, but needs cleanup - use the usePrevious hook instead, and don't fallback to default_value (shouldn't need to).

I tested with a few different components:

# Controlled input with a reset button
@ui.component
def my_input():
    text, set_text = use_state("hello")
 
    def handle_stage_submit():
        set_text("goodbye")

    return [ui.flex(
        ui.text_field(value=text, on_change=set_text),
        ui.text(f"You typed {text}"),
        direction="column",
    ), ui.flex(ui.action_button("Submit", on_press=handle_stage_submit))]
 

mi = my_input()

# Controlled input, just setting the value
@ui.component
def my_input2():
    text, set_text = use_state("hello")

    return [ui.flex(
        ui.text_field(value=text, on_change=set_text),
        ui.text(f"You typed {text}"),
        direction="column",
    )]
 

mi2 = my_input2()

# Uncontrolled input, only setting the default value
@ui.component
def my_input3():
    text, set_text = use_state("hello")

    return [ui.flex(
        ui.text_field(default_value=text, on_change=set_text),
        ui.text(f"You typed {text}"),
        direction="column",
    )]
 

mi3 = my_input3()

plugins/ui/src/js/src/elements/spectrum/TextField.tsx Outdated Show resolved Hide resolved
plugins/ui/src/js/src/elements/spectrum/TextField.tsx Outdated Show resolved Hide resolved
@AkshatJawne AkshatJawne requested a review from mofojed May 29, 2024 17:32
Copy link
Member

@mofojed mofojed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, you do actually need the pending stuff as @mattrunyon called out.

Test with a widget like:

from deephaven import ui
from time import sleep


# Controlled input, just setting the value
@ui.component
def my_slow_input():
    text, set_text = use_state("hello")

    def sleep_set_text(new_text: str):
        sleep(2)
        set_text(new_text)

    return [ui.flex(
        ui.text_field(value=text, on_change=sleep_set_text),
        ui.text(f"You typed {text}"),
        direction="column",
    )]
 

msi = my_slow_input()

But you should wait for the result of the call to onChange that's passed in to TextField before resetting the pending state. So a couple of things around that:

  • We want to set pending state to true immediately in onChange (i.e. not after a debounce)
  • We don't want to reset that pending state to false until after we await/receive the response from the call to propOnChange (which will be after debounce, and after the server has received/responded to the request)
    • We'll need to update the type of onChange passed in as a prop to return Promise<void> instead of just void (by extending the DHCTextFieldProps type)

Then I think we should be good. This case would be much more of an issue in environments were the server is remote and there is some lag, so it's not as exposed if you're running localhost unless you call sleep to simulate it as in the example above.

@AkshatJawne
Copy link
Contributor Author

@mofojed @mattrunyon Attempted to implement code based on comments, have the following code, but after testing, it appears to break the functionality. Not sure if I am misunderstanding?

...
  const [value, setValue] = useState(propValue ?? defaultValue);
  const [pending, setPending] = useState(false);
  const prevPropValue = usePrevious(propValue);

  if (
    propValue !== prevPropValue &&
    propValue !== value &&
    propValue !== undefined &&
    !pending
  ) {
    setValue(propValue);
  }

  const debouncedOnChange = useDebouncedCallback(async (newValue: string) => {
    await propOnChange(newValue);
  }, VALUE_CHANGE_DEBOUNCE);

  const onChange = useCallback(
    (newValue: string) => {
      setPending(true);
      setValue(newValue);
      debouncedOnChange(newValue);
      setPending(false);
    },
    [debouncedOnChange]
  );
...

@AkshatJawne
Copy link
Contributor Author

BTW: I did extend the props as mentioned, just do not have it inlcuded in the pasted snippet. I suspect there is an issue with the setting of the pending state, but I believe it is in alignment with how @mofojed mentioned in his last comment

@mattrunyon
Copy link
Contributor

The debouncedOnChange is async, so you'd want to set isPending to false after the await in that function.

What's happening right now is you set pending, then fire the debounced function, and immediately set pending to false right after.

I think you could also use a ref instead of state here. Not sure if it will make much difference, but it would prevent a re-render after the debounced function returns since the pending state does not affect rendering. The server will push new props if the callback triggers a prop change on the server side

@AkshatJawne
Copy link
Contributor Author

Hmm interesting, I had tried that but it does seem to change the problem. The text in the "You typed" string still does not appear to change.

I will try using a ref instead of a state though, maybe that might fix the issue.

@mattrunyon
Copy link
Contributor

Another edge case that maybe we don't care about unless someone complains about it is you could have a pending onChange and a reset button. Clicking the reset button while the change is pending could result in the reset not working

@AkshatJawne AkshatJawne requested a review from mofojed May 29, 2024 21:29
@AkshatJawne AkshatJawne requested a review from mofojed May 30, 2024 15:16
@AkshatJawne AkshatJawne requested a review from mofojed May 30, 2024 21:23
@AkshatJawne AkshatJawne requested a review from mofojed May 31, 2024 13:24
mofojed
mofojed previously approved these changes Jun 2, 2024
@AkshatJawne
Copy link
Contributor Author

Need @mattrunyon 's approval to merge

plugins/ui/src/js/src/elements/spectrum/TextField.tsx Outdated Show resolved Hide resolved
Comment on lines +44 to +46
} catch (e) {
log.warn('Error returned from onChange', e);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any examples that can trigger this? I'm not sure if we want to swallow this or show it to the user (or if the existing error handling would show it to the user)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There could be cases where the onChange passed in could be faulty or possess an error, in that case, we can catch that here. @mofojed, were there was any specific cases you had in mind when suggesting this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be able to test with something like:

from deephaven import ui

def raise_error(v):
    raise ValueError("Boom")

tf = ui.text_field(on_change=raise_error)

Previously it would log an error in the python console, and an uncaught exception in the browser console. Now it's a warning in the browser console. It still logs in the python console as an error (which I think is correct).

We could potentially report an error on the TextField itself, by setting errorMessage and validationState, but that's out of scope for this ticket and also doesn't necessarily make sense. Either way would log that as a separate ticket.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok that's fair. I'm not sure if the component should blow up like if the error was raised outside of the change handler. Right now if this component were in a dashboard there would be no indication to the user (without log monitoring) that something went wrong.

Copy link
Contributor

@mattrunyon mattrunyon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we need this in other inputs, I would recommend creating a helper function or hook to handle this case (in a separate PR if adding to other input types). That way the logic is not duplicated

@AkshatJawne AkshatJawne merged commit 7d41072 into deephaven:main Jun 3, 2024
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ui.text_field value does not change with use_state
3 participants