-
Notifications
You must be signed in to change notification settings - Fork 76
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
Incorrect result for Onyx update with delete and object property delete #615
Comments
Hey, I'm Fábio from Callstack – expert agency – and I would like to work on this issue. |
I've created a unit test for this case here: main...callstack-internal:react-native-onyx:bugfix/615 From my analysis, it looks like this behavior happens because:
If we change the second operation to
In summary, the current behavior looks correct to me. |
@fabioh8010 thanks for the investigation! It mostly makes sense, although I'm a bit confused why the line you linked to in Onyx is calling multiSet. Maybe you meant to link to this line? You describe how the updates are merged together and I think that's where the problem lies. If we're setting a value for a key to null then we need to fully remove that key. The subsequent update removing the pendingAction property could be ignored since it will do nothing. The way we merge the updates together currently changes the meaning from "clear the value for the action with key '5738984309614092595', and then remove the pendingAction property on that action" to "remove the pendingAction property on that action". These two are not the same, so batching the updates this way is invalid. Also, you recommend changing the action to set. Although that could work for the exact example I gave in this issue, it doesn't really work in general for our app. In the linked App issue an optimistic report preview action is created on the client for a new IOU report, because it hasn't loaded the existing IOU report and therefore doesn't know it exists and should be used. The backend finds the existing IOU report and uses that, so it sends this update to merge null for that action value to clear it from the UI. The pending action update comes from the Onyx successData on the client. When the requests succeeds we want to clear the pending action. It must be a merge, because if we do actually create a new report preview action, we can't wipe out the action entirely; we only want to remove this specific key. A set operations sets the object to an empty one because it only has the pendingAction key which is set to null. Therefore using set doesn't seem like an option and we should fix the Onyx behavior. How does that sound? Lmk if you have questions. |
@neil-marcellini Sorry, I meant this line 🙈 I understand your reasoning about the updates merging, and we do have such logic in place. For every update passed to With the So let's say you have an initial state like this: await Onyx.set(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, {
id: 'sub_entry1',
someKey: 'someValue',
}); And you call const queuedUpdates: OnyxUpdate[] = [
{
key: `${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`,
onyxMethod: 'merge',
value: null,
},
{
key: `${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`,
onyxMethod: 'merge',
value: {
pendingAction: null,
},
},
];
await Onyx.update(queuedUpdates); The first update in the queue set the value to Now, let's analyse your situation. You have an initial state like this: await Onyx.set(`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`, {
sub_entry1: {
id: 'sub_entry1',
someKey: 'someValue',
},
}); Notice that now we are dealing with a collection of collections (report actions). You call const queuedUpdates: OnyxUpdate[] = [
{
key: `${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`,
onyxMethod: 'merge',
value: {
sub_entry1: null,
},
},
{
key: `${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry1`,
onyxMethod: 'merge',
value: {
sub_entry1: {
pendingAction: null,
},
},
},
]; The first update in the queue set the value is not
So, in summary, we have the logic you are mentioning but it doesn't apply recursively inside the objects when doing Some options I can think of for now:
About this issue in specific, I have a question. @neil-marcellini Why do we have these two updates in sequence? const queuedUpdates = [
{
key: 'reportActions_8865516015258724',
onyxMethod: 'merge',
value: {
'5738984309614092595': null,
},
},
{
onyxMethod: 'merge',
key: 'reportActions_8865516015258724',
value: {
'5738984309614092595': {
pendingAction: null,
},
},
},
]; If we are resetting the data and right after it setting to const queuedUpdates = [
{
key: 'reportActions_8865516015258724',
onyxMethod: 'merge',
value: {
'5738984309614092595': null,
},
},
]; That would solve the problem. |
I edited my comment to clarify more things and fix a mistake |
We talked via Slack DM and agreed that the problem lies in Onyx. Fabio is going to create a draft PR with a failing test and work on a proposal. |
Here's the WIP Draft PR: #619 |
ProposalPlease re-state the problem that we are trying to solve in this issue.When calling Currently when batching these updates, if there is an update that sets a value to What is the root cause of that problem?The problem happens because, during batching of the merges/updates and unlike the top-level changes, there is no mechanism to make the current value of a nested property be reset after a In summary, when batching top-level objects: Onyx.set('someKey', {
subKey: 'someValue',
});
const queuedUpdates = [
{
key: 'someKey',
onyxMethod: 'merge',
value: null,
},
{
key: 'someKey',
onyxMethod: 'merge',
value: {
newKey: 'wowNewValue',
},
},
];
Onyx.update(queuedUpdates); The final result will be Now, when batching nested properties: Onyx.set('someKey', {
sub_entry1: {
subKey: 'someValue',
}
});
const queuedUpdates = [
{
key: 'someKey',
onyxMethod: 'merge',
value: {
sub_entry1: null // nested property, not the top-level one
},
},
{
key: 'someKey',
onyxMethod: 'merge',
value: {
sub_entry1: {
newKey: 'wowNewValue',
}
},
},
];
Onyx.update(queuedUpdates); The final result will be This issue happens to both What changes do you think we should make in order to solve the problem?
I've created a new WIP Draft PR here, since the first one was addressing this problem with a wrong solution. What specific scenarios should we cover in automated tests to prevent reintroducing this issue in the future?Unit tests will be added to cover this specific scenario. What alternative solutions did you explore? (Optional)N/A |
Updates:
|
I don't quite understand the wording for this. It's ok to allow subsequent updates to change the same property. I would say something like "When Onyx.update is called with updates for a collection, where the first update sets a value to null for a key that is not at the top level, and another update sets a property to null for that same key, then the value remains unchanged for Onyx subscribers when it should be set to an empty object."
Sounds good. Please elaborate on "the priority of
I'm not sure exactly what you mean, but "discarding" merges over a It sounds like you have found a pretty good approach from your testing. I want to make sure I understand it well. |
Hi @neil-marcellini , sorry about the proposal. I did it when I was still evaluating a solution, so it looked a little bit generic. Now I edited my comment to a more complete version of it, please have a look. |
That's ok, thanks for updating it. I'll review it again now.
Are you sure that's the case? Oh wow, I tested something similar to your example and it's true. I don't know if it's causing any problems specifically because it would be unusual for us to clear a top level value entirely and then have another update filling it in, but it's not what I would expect intuitively when imagining how Onyx.update would work.
I'm not quite sure what you mean by "give priority over react-native-onyx/lib/utils.ts Lines 81 to 92 in 7c9b135
Next steps Side note: I was going to suggest we set Lines 310 to 323 in b92a374
|
Posted in Slack here. I think I'm good with the proposal after thinking about it a bit more. Let's wait a day or two before merging the PR to see if anyone objects to the behavior. Please DM me when the PR is read for a full review. |
Rory and Chris chimed in and agree the current behavior of ignoring subsequent updates doesn't make sense, so let's pause any current efforts until we find a new approach. |
@fabioh8010 @neil-marcellini i think we should be able to re-use most of the logic for combining and applying multiple delta changes from IMO, @fabioh8010 I'll let you decide whether you want to work on this refactor, i'm happy to help in any way whenever you need me! |
@neil-marcellini @chrispader Following our last discussions on Slack, I've updated my proposal again. I believe it's much clear now what is the problem and what we want to achieve. Please have a look and let me know if you have any further questions! |
Very nice! Your updated proposal looks solid to me. Please let me know when the PR is ready for review. |
looks good to me too! 🙌🏼 |
Updates:
|
Updates:
|
@chrispader Do you think we would want this part to stay during the
|
that sounds very good, i'd probably have done it exactly the same 🙌🏼 |
@fabioh8010 i'm not sure either if we'd want to keep that logic or if we can safely remove this. I'd definitely try to remove as much duplicate code around null handling and batching, but i'm not sure if we can just switch from |
To be honest, i feel like we should refactor the API design of Onyx more holistically in order to fix this issue. We should remove all redundant/duplicate code around merging, batching and null key removal (null handling) altogether. Batching and key removal (null handling)We currently have at least 3-4 spots where we handle batching and (nested) key removal differently:
Therefore, i think we have lots of room for removing redundant code around batching and null value handling in the codebase. (We technically also have duplicate code for null value removal for set operations, though it's not that complex there.) Streamlining the storage provider backendsA while back i added an common interface for all storage providers, so the Onyx public API side can except the same functionality from each of the storage providers. Still, the backend implementations don't really match up with what the function names suggest, e.g. We should therefore think about also streamlining the storage provider implementations, so we can expect every storage provider to do exactly the same thing. On SQLite, this would mean that we try to facilitate as much of SQLites low-level functionality (such as Imo, ideally we want to handle all the null checking, batching and merging EITHER in the storage layer or the public Onyx API layer, but not in both. I understand, that we have e.g. the In general, i just feel like we have way to much redundant code and especially too many loops around keys and for merging existing values with updates. I'm tagging the people here that we're involved in lots of these changes back then and who might have more ideas or opinions on this. I'm very curious what you guys think about the current state of Onyx. I hope these thoughts were not to vague. Lmk what you think! cc @fabioh8010 @blazejkustra @tgolen @marcaaron @neil-marcellini |
This idea seems reasonable and would definitely help reduce redundancy. I’m a bit concerned it might become a scope creep. Do we have confidence that our current tests are enough to catch issues if we go ahead with these refactors? |
I like the proposed refactor, but I would prefer to make less impactful changes right now just to be able to get my fix working for all situations, respectively make The reason is to avoid mixing the two things (big holistic refactor and fix to solve the issue's problem) together in one big PR, so I'm trying to see a way to change |
I agree, we shouldn't handle the refactor in this PR, but in another one. I still think it would be worth to tackle this refactor after we find a fix for this issue
No, it would definitely be a good idea to expand the test suite for such a big refactor |
@chrispader I think you bring up very valid concerns. Something we could consider is to develop a |
Problem
When there is an item set in a collection in Onyx, and Onyx.update is called with two merge updates where the first sets the collection item to null in order to remove it, and the second one sets a property of the object to null, then the item remains unchanged in the collection when it should be set to an empty object.
Please see the related App issue for more context.
Issue example
Proof that setting an action to an empty object will remove it from the UI in Expensify/App
Solution
Find the root cause and fix it.
The text was updated successfully, but these errors were encountered: