Closed kavimuru closed 1 year ago
@mananjadhav, @slafortune, @fedirjh, @roryabraham Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!
@mananjadhav, @slafortune, @fedirjh, @roryabraham Huh... This is 4 days overdue. Who can take care of this?
@mananjadhav, @slafortune, @fedirjh, @roryabraham Now this issue is 8 days overdue. Are you sure this should be a Daily? Feel free to change it!
I think we're blocked here because the original solution doesn't work for the offline behavior. @fedirjh if we don't have an alternative, then we should it open it back for other proposals. The debounce
solution was already recommended earlier and we decided not to opt for it.
@mananjadhav Or throttle
! I summed up in my proposal why debounce
is not the best choice 🙃
But aside of that, no matter if we stick to debounce
or throttle
, I'll remind that...
The only catch is that the throttling window has to be adjusted to take the before-mentioned performance situation into consideration.
That window would probably need to be wider that 1000 ms. Or even wider than 2000 ms. Please correct me if I'm wrong, but that's what was my conclusion when I tested this approach.
@rushatgabhane
When the debounce time is 250ms -> all button clicks that happen within 250ms will trigger a single onPress callback only.
please correct if I'm wrong.
Well, conceptually you're right, but in practice what you're saying is wrong in the context of this issue. It works like that in general, but in React Native with blocking calls, in practice, if your debounce time is 250 ms, two clicks within 50 ms can still be both processed, if we block for more than 250 ms. This is probably caused by the way interactions are queued, if I were to guess.
And as far as I can tell, we block the thread for much more than 250 ms when creating a workspace.
So like I said before, we can either adjust the debouncing/throttling window (which looks bad, but reflects another problem, not really introducing a new one) or optimize the code so it doesn't block for so long. But I don't have any good ideas for the second part, and the scope might be huge.
@roryabraham @mananjadhav what do you think the best path forward is for this issue?
@slafortune I think we're open for better proposals here.
@cubuspl42 I wonder why would throttle work if debounce fails?
@slafortune Also I am not sure why it says Internal
. I think we should apply External
label and also Help Wanted
@mananjadhav Which aspect of "failing" are you talking about?
throttle
has a better characteristic than debounce
for this use casedebounce
and throttle
, and any time-dependent solution for what it's worth, need to adjust their thresholds to take the performance issue into accountI'm not changing my position, just to be clear, I already mentioned that it's a separate thing:
But aside of that, no matter if we stick to debounce or throttle, I'll remind that...
To be honest, I think that the performance issue is a totally separate thing, and I would still apply throttle
here. I'm not sure if there's any process for performance issues in this project; I know that the user isn't directly affected, but blocking the JS thread for thousands of milliseconds isn't perfect
Actually, I have an idea how to improve the situation by combining throttle
and delay
. It won't be perfect, but I think that it can work in practice! I'll update my proposal in a few moments.
Edit: The idea looks promising, but I'm struggling to run Expensify in release build for testing performance, which is crucial in case of this issue.
Edit 2: Managed to build in release mode, so far my observations are still promising! Stay tuned.
When a button that triggers navigation is pressed (in particular, the "New workspace" button), there is an opportunity to press it more times before the navigation actually takes place. In the case of the "New workspace" button, this window is very wide (up to thousands of milliseconds), because the JavaScript thread gets blocked in the same task the button is pressed. Likely, this is the main reason why the problem was ever reported. Still, if we actually double-press some navigation-triggering buttons really fast, similar behavior can be observed without performance.
I can see two root causes:
A part of the perfect solution would be fixing the performance problem, but I wasn't able to point a single place responsible for blocking the JS thread. That would make it hard to double-press the button, but not impossible.
Inspired by the overall discussion and conclusions from my previous proposal revisions, I propose to add a property disableOnInteractions
to our Button
class, which disables the buttons while interactions (navigation) takes place:
// Slightly change the semantics of isDisabled:
/** Indicates whether the button should be disabled because of some external logic */
isDisabled: PropTypes.bool,
// Add this new property:
/** Indicates whether the button should be disabled when interactions take place */
disableOnInteractions: PropTypes.bool,
The crucial changes to the Button
implementation:
class Button extends Component {
constructor(props) {
super(props);
this.state = {
isDisabledOnInteractions: false,
};
this.renderContent = this.renderContent.bind(this);
}
// ...
// New helper getter:
get isEffectivelyDisabled() {
return this.props.isDisabled || this.state.isDisabledOnInteractions;
}
// ...
render() {
return (
<Pressable
onPress={(e) => {
// ...
if (this.props.disableOnInteractions) {
this.setState({
isDisabledOnInteractions: true,
});
this.props.onPress(e);
// Use runAfterInteractions to disable the button temporarily:
InteractionManager.runAfterInteractions(() => {
this.setState({
isDisabledOnInteractions: false,
});
});
} else {
this.props.onPress(e);
}
}}
// ...
// Use isEffectivelyDisabled here and in other places that
// previously referred to `props.isDisabled`:
disabled={this.props.isLoading || this.isEffectivelyDisabled}
// ...
>
{/* ... */}
</Pressable>
);
}
}
Previously, I explored the possibility to work around the performance problem of the "New workspace" button itself, but still left a narrow window for fast double presses. That solution is available in the edit history.
Triggered auto assignment to @dylanexpensify (External
), see https://stackoverflow.com/c/expensify/questions/8582 for more details.
Current assignee @mananjadhav is eligible for the External assigner, not assigning anyone new.
Current assignee @roryabraham is eligible for the External assigner, not assigning anyone new.
Pressing New Workspace
multiple times subsequently creates multiple workspaces.
Since navigation to the workspace page happens after the workspace creation completes, it leaves an opportunity window for creating additional workspaces by repeatedly pressing the button. This is a result of the small period of processing time the creation of a new workspace requires, and the lack of a condition that prevents it.
My initial thought was using state to store an isButtonPressed
boolean, and then conditioning on it in the onPress
handler, but after reading through the thread I learned of this issue:
If you use state in react components to disable the button status, you can't avoid triple button click events. Because react state is only changed when the component is re-rendered. But the event listeners can be called several times before re-rendering! So you can't avoid the issue using react state.
So as an alternative, what if we used an IIFE and a closure to replace the handler with an empty function after it's called the first time? Called directly it initializes the handler, but the function it creates is callable only once, and no-ops on successive calls.
// WorkspacesListPage.js
....
setPressHandler() {
this.handlePress = (() => {
let onceCallable = () => {
Policy.createWorkspace();
onceCallable = f => f;
};
return () => onceCallable();
})();
}
We would call setPressHandler
in the constructor (or useEffect) to initialize the handler before the component is mounted, and again in componentDidUpdate
(or useEffect with a dependency, or a navigation listener) to reset it, since reversing through the navigation stack doesn't remount the component.
The Button
for creating the workspace would use onPress={this.handlePress}
which is only functionable once. This effectively prevents multiple presses of the button from creating multiple workspaces without relying on state.
that is clever
Clicking on "New workspace" button multiple times quickly, creates that number of workspaces.
Policy.createWorkspace
is not protected with a loading flag.
We can create a state variable to track function execution state, and only allow the execution if the previous function is fully executed. We might need to promisify Policy.createWorkspace
, but it is working without that too.
isCreatingWorkspace
createWorkspace
and pass this to Button
onPress
prop. createWorkspace() {
// this check should be here, react guarantee to flush `setState` inside browser event boundary
if (this.state.isCreatingWorkspace) {
return;
}
this.setState({isCreatingWorkspace: true});
Policy.createWorkspace();
this.setState({isCreatingWorkspace: true});
}
Source: https://stackoverflow.com/questions/35315872/reactjs-prevent-multiple-times-button-press
None
I am sorry. I made a mistake while testing my proposal, and thought It's working, but I still believe that will work after we promisify Policy.createWorkspace
.
Clicking New workspace
button 'n' times calls create workspace 'n' times on Workspaces list page
Before navigating to new workspace page, button onPress
event can still be triggered when click it multiple times quickly, even after creating workspace.
This happens because navigation takes time and it gives enough chance for user to click button.
We already prevented multiple IOU requests in https://github.com/Expensify/App/pull/13733.
We can use same approach here.
So define local flag variable (class member), something like this.isCreatingWorkspace
and call Policy.createWorkspace()
only when this flag is false.
In detail:
https://github.com/Expensify/App/blob/1d3f0695c79c0bad8d6cf7402536025ce1272a54/src/pages/workspace/WorkspacesListPage.js#L199
onPress
event callback, if this.isCreatingWorkspace
= true, early returnthis.isCreatingWorkspace
= false, update this value to true and trigger Policy.createWorkspace();
withNavigationFocus
and update flag to false on componentDidupdate
under this condition - this.props.isFocused && !prevProps.isFocused
Clicking New workspace button ‘n’ number of times creates ‘n’ number of workspaces immediately
There are 2 problems:
The button to create work space, it's not disabled during creating work space time https://github.com/Expensify/App/blob/f327eb0c3c73d13c1820ea62a3ee1a94cc023801/src/pages/workspace/WorkspacesListPage.js#L199
Even if we disable before and enable after calling Policy.createWorkspace
, when the screen is navigating to work space that just created, there are small amount of transition time that user can press the button
https://github.com/Expensify/App/blob/f327eb0c3c73d13c1820ea62a3ee1a94cc023801/src/libs/actions/Policy.js#L950-L956
To solve each problem, I suggest that:
Add an addition isCreatingWorkspace
state to component WorkspacesListPage
, then disable button if isCreating state is true. It will help improve better UX where creating work space take a lot of time.
To deal with transition affect, we can use InteractionManager.runAfterInteractions, which is using in several places in our codebase
More details, we should update those LOC by:
<Button
isDisabled={this.state.isCreatingWorkspace}
success
text={this.props.translate('workspace.new.newWorkspace')}
onPress={() => {
this.setState({isCreatingWorkspace: true});
Policy.createWorkspace();
InteractionManager.runAfterInteractions(() => {
this.setState({isCreatingWorkspace: false});
});
}}
/>
If my proposal above is not strong enough, we can define an additional flag isRequestCreatingWorkspace
in the action lib Policy here. We will turn on/turn off this flag when enter the method or when the method is finished
@mananjadhav, @slafortune, @fedirjh, @roryabraham Whoops! This issue is 2 days overdue. Let's get this updated quick!
do we want callstack's help on this? @roryabraham @mananjadhav
@mananjadhav, @slafortune, @fedirjh, @roryabraham Still overdue 6 days?! Let's take care of this!
@mananjadhav, @slafortune, @fedirjh, @roryabraham Now this issue is 8 days overdue. Are you sure this should be a Daily? Feel free to change it!
@roryabraham @mananjadhav what do you think the best path forward is for this issue?
can we review my proposal here https://github.com/Expensify/App/issues/14572#issuecomment-1490165910 ? I think it's align with what we did in other places
@mananjadhav, @slafortune, @roryabraham Whoops! This issue is 2 days overdue. Let's get this updated quick!
Is this issue open for new proposals?
@mananjadhav Any updates here, it would be great to consolidate this I will also post in slack for more help since this is a big issue.
📣 @s77rt You have been assigned to this job by @mountiny! Please apply to this job in Upwork and leave a comment on the Github issue letting us know when we can expect a PR to be ready for review 🧑💻 Keep in mind: Code of Conduct | Contributing 📖
What are we trying to do here?
Create a generic solution that any page with a button can use? Or only fix this issue for workspace similar to how we fixed it for IOU?
Create a generic solution that any page with a button can use? Or only fix this issue for workspace similar to how we fixed it for IOU
Any issue with similar problem. We experience it here with Send money too https://github.com/Expensify/App/issues/17167
@s77rt can you please propose update issue body to make sure it addresses any button with this problem? thank you!
Cool, thanks!
for proposal https://github.com/Expensify/App/issues/14572#issuecomment-1484123677 @situchan any thoughts on how we can create a generic solution that will work for all buttons ?
Fast clicking any button 'n' times execute 'n' times
cc @mountiny
Not to be able to fast click any button, after the first click the button should be disabled or have no effect until the initial click effects are done. i.e. Clicking ‘New Workspace’ button multiple times should just create a single workspace.
The issue is reproducible with all buttons e.g. the IOU's I'll settle up elsewhere
button as reported on https://github.com/Expensify/App/issues/17167.
Clicking ‘New workspace’ button ‘n’ number of times creates ‘n’ number of workspaces
Thanks! Updated everything, lets go through the proposals @s77rt 🙇
Maybe we can somehow build on top of this proposal of mine from three months ago? It has a generic solution that will solve this bug in multiple places.
@priyeshshah11 sure! But IIRC, we had some issues with using state because there can be a duplicate button click in the time it takes for the state to update
you wanna update your proposal to take that into account?
Also i think it should be fine to use hooks now too
We can't use state. User can click number of times in android while state change, especially on heavy device.
I don't think there's general solution to fix all buttons throughout the app. Should be case by case. So if the page that has button is functional component, use ref hook (ref.value) or useSharedValue, if class component, use stateless member variable (this). class example: https://github.com/Expensify/App/pull/13733/files#diff-d7b964ad00daabd50674375196edbc54b915728ee08b5e5f86adbc79d358238bR461-R465 function example: https://github.com/Expensify/App/pull/17326/files#diff-f265a5ae9cea30af21ecbd34b52aa47636dd70e948f45c95246a41fc62398580R312-R315
Well there will be no class components soon so we should focus on fixing this with functional components in mind so we dont waste efforts.
Pressing sending button multiple times sending money multiple times on the next screen (https://github.com/Expensify/App/issues/17167)
The button does not have any disabling logic to prevent multiple clicks during execution
We need simply prevent user of clicking multiple times on the button - in other words make onPress non actionable Possible and elegant solution (to avoid any not needed setState) - using ref. In which we will put - true when the button was clicked first time And have a condition check - if true already in ref - do nothing. Example (from https://github.com/Expensify/App/issues/17167):
onConfirm={(selectedParticipants) => { if (buttonRef.current) { return; } buttonRef.current = true; createTransaction(selectedParticipants); ReportScrollManager.scrollToBottom(); }} It can be done as either global or local solution - depends on how we would like to fix it
@narefyev91 Thanks for the proposal. Can you please elaborate how we can make this a global solution?
@situchan I like idea of just setting a boolean variable in the onPress
callback but the challenge is how to reset it. Did you explore some options here?
@priyeshshah11 I would really prefer if we can find a solution without the use of promises, I feel it's being over used in App a bit. The reason we can't go with a promise here is that we will have to make every function that is called on the onPress
callback return a promise (and every function that is called within) e.g. check the SettlementButton
on MoneyRequestConfirmationList
, we will have to make confirm, onSendMoney, onConfirm, up to the whatever comes before in that hierarchy return a promise which is just unpractical.
We may be jumping into the solution a little early. Why the bug is only reproducible on Android, we had this bug recently https://github.com/Expensify/App/issues/14258 and it was also reproducible on Android only.
@s77rt Sure let me explain - onPress logic in 100% cases it's something we can move in function like
onPress={onHandlePressFunction}
which can be something like this
function onHandlePressFunction(e) {
...anything which needed to be executed / any logic
}
And to prevent button in clicking we need to use ref logic
if (buttonRef.current) { return; }
buttonRef.current = true;
We can combine both logic in one
onPress={(e) => {
if (buttonRef.current) { return; }
buttonRef.current = true;
onHandlePressFunction(e)
}}
not all buttons should be disabled after one click - in that case we can add some prop to control which button should use ref logic and which should not. We also should define ref only inside onGlobalPress function - because it's controlling execution. I tested only on one screen, but for sure it may be some workarounds on other screens. Global onPress function will be looks something like:
const newRef = React.createRef();
function onGlobalPress(props, onPress, shouldBlock) {
if (newRef.current) {return ;}
if (shouldBlock) {
newRef.current = true;
}
return onPress(props);
}
Execution will be something like
onSendMoney={(paymentMethodType) => {
onGlobalPress(paymentMethodType, () => {
sendMoney(paymentMethodType);
ReportScrollManager.scrollToBottom();
}, true)
}}
We may be jumping into the solution a little early. Why the bug is only reproducible on Android, we had this bug recently #14258 and it was also reproducible on Android only.
it's also happened on IOS for me as well, not sure why no one add IOS to bug description
If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!
HOLD on https://github.com/Expensify/App/pull/17404
Action Performed:
Expected Result:
Note: The issue is reproducible with all buttons e.g. the IOU's I'll settle up elsewhere button as reported on https://github.com/Expensify/App/issues/17167.
Actual Result:
Clicking ‘New workspace’ button ‘n’ number of times creates ‘n’ number of workspaces
If you follow the same step from 1-4 on web or mweb , then clicking the new workspace button ‘n’ number of times just creates a single workspace and not ‘n’ number of workspaces
Workaround:
unknown
Platforms:
Which of our officially supported platforms is this issue occurring on?
Version Number: 1.2.59-1 Reproducible in staging?: y Reproducible in production?: y If this was caught during regression testing, add the test name, ID and link from TestRail: Email or phone of affected tester (no customers): Logs: https://stackoverflow.com/c/expensify/questions/4856 Notes/Photos/Videos:
https://user-images.githubusercontent.com/43996225/214705874-d2b1fe24-327f-45d0-ae28-1b9c6d142cdf.mp4
https://user-images.githubusercontent.com/43996225/214707380-d4d32ef9-8634-41b9-ac81-b13861680387.mp4
Expensify/Expensify Issue URL: Issue reported by: @priya-zha Slack conversation: https://expensify.slack.com/archives/C049HHMV9SM/p1674643973375769
View all open jobs on GitHub
Upwork Automation - Do Not Edit