To be frank, I'm struggling to justify using legend-state in even my hobby projects at the moment. The changes introduced to make it compatible with react compiler seem to have left the docs mostly inaccurate and I spend more time trying to hunt around for what appears to be the intended pattern for different problems than actually writing code. In addition to that, even after thinking I have a grip on what the expected behaviour is I then run head first into behaviour that seems to be a bug and have to invent workarounds that sidestep the issues.
(Here's a prior example I submitted a report for 2 years ago, which is still present in the latest beta version #395)
The problem I'm writing this report for is removing and inserting elements in observable arrays. The docs don't have a direct example, and I've found two old issues discussing this that suggest two different approaches (#12 and #41). My assumption is that there should be some way to move elements around in an observable array such that:
- the final data matches what is intended
- this can be accomplished by mutating the existing array rather than creating a new one and copying the elements over
- observation handlers of the array fire when elements are moved, and only once if the mutations are batched
Here are several attempts that have various issues:
common setup:
const state$ = observable({
arr: [
{id: 0},
{id: 1},
{id: 2},
]
})
function printArr(name: string, arr: { id: number }[]) {
console.log(name, arr.map((item) => item.id).join(' '))
}
observe(() => {
printArr('observe', state$.arr.get());
})
attempt 1:
function moveItemOnObservableBatched(indexSrc: number, indexDest: number) {
console.log('moving', indexSrc, indexDest);
// Approach closer to https://github.com/LegendApp/legend-state/issues/12
const item$ = state$.arr[indexSrc];
if (!item$) throw new Error('out of bounds')
beginBatch();
state$.arr.splice(indexSrc, 1);
state$.arr.splice(indexDest, 0, item$);
endBatch();
}
printArr('before', state$.arr.peek());
moveItemOnObservableBatched(2, 1);
printArr('after', state$.arr.peek());
output logs:
observe 0 1 2
before 0 1 2
moving 2 1
observe 0 1 1
observe 0 1 1
after 0 1 1
Problems:
- the end data is wrong (id=1 is duplicated, id=2 is gone)
- the observe handler fires twice despite batching mutations
attempt 2:
function moveItemOnObservableUnbatched(indexSrc: number, indexDest: number) {
console.log('moving', indexSrc, indexDest);
// Approach closer to https://github.com/LegendApp/legend-state/issues/12
const item$ = state$.arr[indexSrc];
if (!item$) throw new Error('out of bounds')
state$.arr.splice(indexSrc, 1);
state$.arr.splice(indexDest, 0, item$);
}
printArr('before', state$.arr.peek());
moveItemOnObservableUnbatched(2, 1);
printArr('after', state$.arr.peek());
output logs:
observe 0 1 2
before 0 1 2
moving 2 1
observe 0 1
observe 0 1 1
observe 0 1 1
after 0 1 1
Problems:
- the end data is wrong (id=1 is duplicated, id=2 is gone)
- the observe handler fires once for the splice that removes the element as expected, but then fires twice for the splice insertion
attempt 3:
function moveItemOnRaw(indexSrc: number, indexDest: number) {
console.log('moving', indexSrc, indexDest);
// Approach outlined in https://github.com/LegendApp/legend-state/issues/41
const arr = state$.arr.peek();
const item = arr[indexSrc];
if (!item) throw new Error('out of bounds')
arr.splice(indexSrc, 1);
arr.splice(indexDest, 0, item);
state$.arr.set(arr);
}
printArr('before', state$.arr.peek());
moveItemOnRaw(2, 1);
printArr('after', state$.arr.peek());
output logs:
observe 0 1 2
before 0 1 2
moving 2 1
after 0 2 1
Problems:
- the observe handler doesn't fire, presumably because the reference of the array doesn't change. Whilst that makes sense in isolation, it seems to contradict the behaviour where mutating the array with other methods like splice will make it fire
attempt 4:
insert a .slice() to clone the array and make sure the reference we set isn't what we started with
function moveItemOnRaw(indexSrc: number, indexDest: number) {
console.log('moving', indexSrc, indexDest);
// Approach outlined in https://github.com/LegendApp/legend-state/issues/41
const arr = state$.arr.peek();
const item = arr[indexSrc];
if (!item) throw new Error('out of bounds')
arr.splice(indexSrc, 1);
arr.splice(indexDest, 0, item);
state$.arr.set(arr.slice());
}
printArr('before', state$.arr.peek());
moveItemOnRaw(2, 1);
printArr('after', state$.arr.peek());
output logs:
observe 0 1 2
before 0 1 2
moving 2 1
after 0 2 1
Problems:
- the observe handler still doesn't fire
- (we are now no longer sticking to mutation only, leading to greater memory churn)
attempt 5:
move the .slice() from being after the splicing to before the splicing
function moveItemOnRaw(indexSrc: number, indexDest: number) {
console.log('moving', indexSrc, indexDest);
// Approach outlined in https://github.com/LegendApp/legend-state/issues/41
const arr = state$.arr.peek().slice();
const item = arr[indexSrc];
if (!item) throw new Error('out of bounds')
arr.splice(indexSrc, 1);
arr.splice(indexDest, 0, item);
state$.arr.set(arr);
}
printArr('before', state$.arr.peek());
moveItemOnRaw(2, 1);
printArr('after', state$.arr.peek());
output logs:
observe 0 1 2
before 0 1 2
moving 2 1
observe 0 2 1
after 0 2 1
Problems:
- the observe handler fires now but I am at a loss as to why changing the order of operations like this changes the outcome
- (we are now no longer sticking to mutation only, leading to greater memory churn)
in conclusion
The behaviour outlined above is largely unintuitive and unpredictable, at least for me. Maybe it's different for someone familiar with the internals of this codebase. I've unfortunately run into enough issues like this that it remains hard to have faith in the general robustness of the library, which is a problem when it's meant to be handling something critical.
My post probably comes across as being quite harsh and entitled, and that's because it is. I'm not really interested in contributing my own time to deeply learning and fixing the frameworks I want to build on top of, as the simpler path is to migrate to a different framework that works instead even if its ergonomics is less aligned with my preferences. Nevertheless, perhaps this gives some insight into how exasperating it can be to try to use legend-state at the moment.
To be frank, I'm struggling to justify using legend-state in even my hobby projects at the moment. The changes introduced to make it compatible with react compiler seem to have left the docs mostly inaccurate and I spend more time trying to hunt around for what appears to be the intended pattern for different problems than actually writing code. In addition to that, even after thinking I have a grip on what the expected behaviour is I then run head first into behaviour that seems to be a bug and have to invent workarounds that sidestep the issues.
(Here's a prior example I submitted a report for 2 years ago, which is still present in the latest beta version #395)
The problem I'm writing this report for is removing and inserting elements in observable arrays. The docs don't have a direct example, and I've found two old issues discussing this that suggest two different approaches (#12 and #41). My assumption is that there should be some way to move elements around in an observable array such that:
Here are several attempts that have various issues:
common setup:
attempt 1:
output logs:
Problems:
attempt 2:
output logs:
Problems:
attempt 3:
output logs:
Problems:
attempt 4:
insert a
.slice()to clone the array and make sure the reference we set isn't what we started withoutput logs:
Problems:
attempt 5:
move the
.slice()from being after the splicing to before the splicingoutput logs:
Problems:
in conclusion
The behaviour outlined above is largely unintuitive and unpredictable, at least for me. Maybe it's different for someone familiar with the internals of this codebase. I've unfortunately run into enough issues like this that it remains hard to have faith in the general robustness of the library, which is a problem when it's meant to be handling something critical.
My post probably comes across as being quite harsh and entitled, and that's because it is. I'm not really interested in contributing my own time to deeply learning and fixing the frameworks I want to build on top of, as the simpler path is to migrate to a different framework that works instead even if its ergonomics is less aligned with my preferences. Nevertheless, perhaps this gives some insight into how exasperating it can be to try to use legend-state at the moment.