agile icon indicating copy to clipboard operation
agile copied to clipboard

Computed value is not recomputed after dependencies change

Open leopf opened this issue 3 years ago • 3 comments

🐛 Bug report

I have a problem with not updating computed values. I am using a demo API to download and insert users into a collection. I want to use a computed value to get a user from this collection based on a user id, which is defined in a state. The first compute works with the default user id, but after updating the user id the value is not recomputed. I took a look at the deps prop and found all the necessary observers with the correct values, though the value of the computed user was incorrect.

The first compute only works if I define the computed value after inserting the users into the collection, otherwise this computed value is stuck on undefined.

🤖 Current Behavior

const data: any[] = await fetch('https://jsonplaceholder.typicode.com/users').then(res => res.json());

const userCollection = createCollection();
const selectedUserId = createState(1);
selectedUserId.watch(() => console.log("---- new selected user id!"));

for (const item of data) {
    userCollection.collect(item);
}

const selectedUser = createComputed(() => {
    return userCollection.getItemValue(selectedUserId.value);
});

console.log("auto:", selectedUser.value);
selectedUserId.set(2);
console.log("manual:", userCollection.getItemValue(selectedUserId.value));
console.log("auto:", selectedUser.value);
console.log(selectedUser.deps);

output:

auto: {
  id: 1,
  name: 'Leanne Graham',
  username: 'Bret',
  email: '[email protected]',
  address: {
    street: 'Kulas Light',
    suite: 'Apt. 556',
    city: 'Gwenborough',
    zipcode: '92998-3874',
    geo: { lat: '-37.3159', lng: '81.1496' }
  },
  phone: '1-770-736-8031 x56442',
  website: 'hildegard.org',
  company: {
    name: 'Romaguera-Crona',
    catchPhrase: 'Multi-layered client-server neural-net',
    bs: 'harness real-time e-markets'
  }
}
---- new selected user id!
manual: {
  id: 2,
  name: 'Ervin Howell',
  username: 'Antonette',
  email: '[email protected]',
  address: {
    street: 'Victor Plains',
    suite: 'Suite 879',
    city: 'Wisokyburgh',
    zipcode: '90566-7771',
    geo: { lat: '-43.9509', lng: '-34.4618' }
  },
  phone: '010-692-6593 x09125',
  website: 'anastasia.net',
  company: {
    name: 'Deckow-Crist',
    catchPhrase: 'Proactive didactic contingency',
    bs: 'synergize scalable supply-chains'
  }
}
auto: {
  id: 1,
  name: 'Leanne Graham',
  username: 'Bret',
  email: '[email protected]',
  address: {
    street: 'Kulas Light',
    suite: 'Apt. 556',
    city: 'Gwenborough',
    zipcode: '92998-3874',
    geo: { lat: '-37.3159', lng: '81.1496' }
  },
  phone: '1-770-736-8031 x56442',
  website: 'hildegard.org',
  company: {
    name: 'Romaguera-Crona',
    catchPhrase: 'Multi-layered client-server neural-net',
    bs: 'harness real-time e-markets'
  }
}
Set(2) {
  StateObserver {
    agileInstance: [Function (anonymous)],
    key: undefined,
    dependents: Set(1) { [StateObserver] },
    subscribedTo: Set(0) {},
    value: 2,
    previousValue: 1,
    state: [Function (anonymous)],
    nextStateValue: 2
  },
  StateObserver {
    agileInstance: [Function (anonymous)],
    key: 2,
    dependents: Set(1) { [StateObserver] },
    subscribedTo: Set(0) {},
    value: {
      id: 2,
      name: 'Ervin Howell',
      username: 'Antonette',
      email: '[email protected]',
      address: [Object],
      phone: '010-692-6593 x09125',
      website: 'anastasia.net',
      company: [Object]
    },
    previousValue: null,
    state: [Function (anonymous)],
    nextStateValue: {
      id: 2,
      name: 'Ervin Howell',
      username: 'Antonette',
      email: '[email protected]',
      address: [Object],
      phone: '010-692-6593 x09125',
      website: 'anastasia.net',
      company: [Object]
    }
  }
}

🎯 Expected behavior

I would expect this to work, as defined in the docs.

const MY_COMPUTED = createComputed(() => {
    const user = USERS.getItemValue(CURRENT_USER_ID.value);
    return `My name is '${user?.name} and I am ${user?.age} years old.`;
});
MY_COMPUTED.value; // Returns "My name is 'hans' and I am 10 years old."
USERS.update(CURRENT_USER_ID.value, {name: 'jeff'})
MY_COMPUTED.value; // Returns "My name is 'jeff' and I am 10 years old." 

📄 Reproducible example

I created a simple main.ts file and ran it with ts-node.

import { createState, createCollection, createComputed } from "@agile-ts/core";
import fetch from 'cross-fetch';

async function init() {
    const data: any[] = await fetch('https://jsonplaceholder.typicode.com/users').then(res => res.json());

    const userCollection = createCollection();
    const selectedUserId = createState(1);
    selectedUserId.watch(() => console.log("---- new selected user id!"));

    for (const item of data) {
        userCollection.collect(item);
    }

    const selectedUser = createComputed(() => {
        return userCollection.getItemValue(selectedUserId.value);
    });

    console.log("auto:", selectedUser.value);
    selectedUserId.set(2);
    console.log("manual:", userCollection.getItemValue(selectedUserId.value));
    console.log("auto:", selectedUser.value);
    console.log(selectedUser.deps);
}

init();

💡 Suggested solution(s)

➕ Additional notes

💻 Your environment

Software Version(s)
TypeScript v4.5.4
npm/Yarn v8.3.2
NodeJs v14.18.2
@agile-ts/core v0.2.8
@agile-ts/react N/A
@agile-ts/api N/A
@agile-ts/multieditor N/A
ts-node v10.4.0

leopf avatar Jan 24 '22 13:01 leopf

Thanks for reporting this issue @leopf ;D It looks like that the computed value isn't ingested into the runtime correctly. The compute() method is called as expected, and returns the correct value (see red marked part). However, this returned value doesn't seem to get applied to the State.

image https://codesandbox.io/s/issue-219-ufcck?file=/src/main.js

I'll dig into your issue this evening and keep you updated.

Thanks ;D


  • You can also use Selectors to select a specific State in a Collection.

bennobuilder avatar Jan 24 '22 14:01 bennobuilder

Thanks for the quick response! I did some digging as well, and it seems like the computed value is updated asynchronously. I added a sideeffect to test this:

...
const selectedUser = createComputed(() => {
    return userCollection.getItemValue(selectedUserId.value);
});

selectedUser.addSideEffect("somerandomkey", (instance) => console.log("NEW SELECTED USER:", instance.value), { weight: 0 });
...

The new output is the same except after the already posted output it outputs the new value for the computed instance.

NEW SELECTED USER: {
  id: 2,
  name: 'Ervin Howell',
  username: 'Antonette',
  email: '[email protected]',
  address: {
    street: 'Victor Plains',
    suite: 'Suite 879',
    city: 'Wisokyburgh',
    zipcode: '90566-7771',
    geo: { lat: '-43.9509', lng: '-34.4618' }
  },
  phone: '010-692-6593 x09125',
  website: 'anastasia.net',
  company: {
    name: 'Deckow-Crist',
    catchPhrase: 'Proactive didactic contingency',
    bs: 'synergize scalable supply-chains'
  }
}

This would make sense, since the compute function within the Computed instance is asynchronous. For this to work, it might be necessary to differentiate between async and sync compute functions under the hood.

If my suspicion proves correct, I will follow up with a PR.

leopf avatar Jan 24 '22 14:01 leopf

Yeah, you are right 👍 The fact that the wrapper compute method itself is async seems to create this issue. Adding a small timeout shortly after mutating the userId seems to give the compute() method enough time to compute itself asynchronously, as the wrapper method is async.

See code sandbox: https://codesandbox.io/s/issue-219-ufcck?file=/src/main.js

bennobuilder avatar Jan 24 '22 17:01 bennobuilder

fixed in #222

bennobuilder avatar Sep 04 '22 05:09 bennobuilder