danielearwicker / computed-async-mobx Goto Github PK
View Code? Open in Web Editor NEWDefine a computed by returning a Promise
License: MIT License
Define a computed by returning a Promise
License: MIT License
Hi there
Thanks for this library! Could not yet test it but looks promising.
I had an issue during the installation using NPM saying Appears to be a git repo or submodule.
. After a quick search I found the problem was that there is a .git
directory inside the module root. After removing it everything worked fine (cd node_modules/computed-async-mobx && rm -rf .git
).
Just to let you and others know - will now give the library a try ๐
Cheers
Hi! I think this is related to #12
I have this wrapper around asyncComputed
for ease of use:
export const asyncComputed = (initial, fn) => {
// wrap asyncComputed so it follows the same sort of api as fromResource
const computed = originalAsyncComputed(initial, 1000, fn)
return {
busy: () => computed.busy,
current: computed.get,
}
}
Then I have an asyncComputed variable on the ERC20
class like so (pretty standard)
export default class ERC20 {
// ...
name = asyncComputed('...', async () => {
try {
const name = await this.contract.methods.name().call()
if (!name) { throw new Error() }
return name
} catch (error) {
return 'Invalid Token'
}
})
// ...
}
finally, I have these set of computed functions to derive info about the canonical tokens
@computed get canonicalTokens () {
const networkId = this.root.web3Context.network.id
if (!networkId) { return [] }
const tokenArtifacts = [Test1Token, Test2Token, Test3Token]
// this next line is the only relevant one
return tokenArtifacts.map(ct =>
new ERC20(ct.networks[networkId].address)
)
}
@computed get canonicalTokenInfo () {
return this.canonicalTokens.map(ct => ({
busy: ct.name.busy() || ct.symbol.busy(),
name: ct.name.current(),
symbol: ct.symbol.current(),
address: ct.address,
}))
}
// are any of the canonical token things busy?
@computed get isLoadingCanonicalTokens () {
return this.canonicalTokens.length === 0 ||
some(this.canonicalTokens, (ct) => ct.busy)
}
The error occurs when accessing canonicalTokenInfo
with
Unhandled Rejection (Error): promisedComputed must be used inside reactions
110 | @computed get canonicalTokenInfo () {
111 | return this.canonicalTokens.map(ct => ({
112 | busy: ct.name.busy() || ct.symbol.busy(),
> 113 | name: ct.name.current(),
114 | symbol: ct.symbol.current(),
115 | address: ct.address,
116 | }))
108 | }
109 |
110 | @computed get canonicalTokenInfo () {
> 111 | return this.canonicalTokens.map(ct => ({
112 | busy: ct.name.busy() || ct.symbol.busy(),
113 | name: ct.name.current(),
114 | symbol: ct.symbol.current(),
34 | _lazyInitForm = async () => {
35 | await when(() => !this.props.store.domain.isLoadingCanonicalTokens)
36 |
> 37 | this.form = buildRecipeForm(this.props.store.domain.canonicalTokenInfo)
38 |
39 | // add initial input
40 | this._addInput()
Some questions:
isLoadingCanonicalTokens
function an anti-pattern?Any ideas?
I realize that this package is meant to be used with MobX. However, I would have expected it to work with manual polling, too.
Consider this Node code:
const { computedAsync } = require('computed-async-mobx');
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function main() {
const observableValue = computedAsync({
init: 'initial Value',
fetch: async () => {
console.log('fetch called');
await delay(1);
return 'final value';
},
});
for (let i = 0; i < 5; i++) {
console.log('Accessing observableValue twice.');
console.log('observableValue:', { busy: observableValue.busy, value: observableValue.value });
await delay(1);
}
}
main();
The output looks like this:
Accessing observableValue twice.
fetch called
fetch called
observableValue: { busy: true, value: 'initial Value' }
Accessing observableValue twice.
fetch called
fetch called
observableValue: { busy: true, value: 'initial Value' }
Accessing observableValue twice.
fetch called
fetch called
observableValue: { busy: true, value: 'initial Value' }
Accessing observableValue twice.
fetch called
fetch called
observableValue: { busy: true, value: 'initial Value' }
Accessing observableValue twice.
fetch called
fetch called
observableValue: { busy: true, value: 'initial Value' }
Each time I access a property of observableValue
, its fetch
function is re-evaluated. That is unfortunate, but probably cannot be helped; after all, when called by non-observed code, observableValue
has no way of knowing whether fetch
will return the same value again. What surprises me, though, is that I never get the actual value returned by fetch
.
If fetch
has been called in the past and has resolved with a value, I'd expect observableValue.value
to return that value.
Probably need a use case before designing API.
I noticed that the busy
property doesn't behave quite as I would have expected. It seems that when a computedAsync
value gets initialized, busy
returns false
, then quickly switches to true
. I would have expected it to return true
from the beginning. As it stands, this behavior doesn't play nice with React (via mobx-react
).
Consider this code:
import React from 'react';
import ReactDOM from 'react-dom';
import { computedAsync } from 'computed-async-mobx';
import delay from 'delay';
import { observer } from 'mobx-react';
async function timeConsumingOperation() {
for (let i = 0; i < 5; i++) {
await delay(500);
console.log(`Waiting (${i})...`);
}
}
@observer
class UseCase extends React.Component {
observableValue = computedAsync({
init: 'Initial dummy value',
fetch: async () => {
await timeConsumingOperation();
return 'Computed value';
},
});
render() {
const { value, busy } = this.observableValue;
console.log('render()', { value, busy });
return (<ul>
<li>value: {value}</li>
<li>busy: {JSON.stringify(busy)}</li>
</ul>);
}
}
ReactDOM.render(
<UseCase />,
document.body.appendChild(document.createElement('div'))
);
This results in the following console output:
render() Object {value: "Initial dummy value", busy: false}
Warning: forceUpdate(...): Cannot update during an existing state transition (such as within
render
or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved tocomponentWillMount
.
render() Object {value: "Initial dummy value", busy: true}
Waiting (0)...
Waiting (1)...
Waiting (2)...
Waiting (3)...
Waiting (4)...
render() Object {value: "Computed value", busy: false}
It seems that this almost-instantaneous switch from busy: false
to busy: true
forces React to re-render the component before the first rendering is completely finished.
As a test, I introduced a computed helper property busy
that is true
if observableValue.busy
is true
or observableValue.value
is the default value. This emulates the behavior I would have expected for observableValue.busy
. With this hack in place, React doesn't print the warning.
Cannot find module 'mobx-utils' from 'promisedComputed.js'
However, Jest was able to find:
'./promisedComputed.d.ts'
'./promisedComputed.js'
'./promisedComputed.js.map'
You might want to include a file extension in your import, or update your 'moduleFileExtensions', which is currently ['js', 'json', 'jsx'].
had to install mobx-utils
seperately
I added special failed/error properties, but this is unnecessary.
Everything can be done inside the fetch
function itself, usingtry
/catch
inside async
to transform errors into plain values, or Promise#catch
.
The rethrow
property should be removed - the behaviour should be to always throw if the value
is accessed while in an error state, for consistency with computed
in the latest MobX.
Running into a similar error as others (#19) but can't figure out why.
My store has a substore 'data' which is making use of asyncComputed:
//inside data store
@computed get concepts() {
return this.fetchedConcepts.get()
}
fetchedConcepts = asyncComputed([], 0, async () => {
return await getConcepts({ dataset }) // a promise
})
The store is then used by a component:
const store = useExplorerStore()
<MyComponent data={store} />
The store is working apart from the store.data
properties which are using asyncComputed.
Any hints appreciated ๐
One weird and common behavior that I've noticed is this (where store.query
is a computedAsync
):
const Comp = observer(({ store }) => (
store.query.busy
? <div>Loading...</div>
: <div>Results are loaded: {query.value}</div>}
))
The problem is that query.busy
gets set to true, and then the component technically stops observing query.value
, so the autorun is cancelled and the promise is never resolved. An example fiddle is here: https://jsfiddle.net/qc26pb4k/6/.
A fix is simply to have the component explicitly depend on query.value
, even while query.busy
is true. See line 136 in the fiddle for an example of that fix.
Maybe depending on busy
should automatically indicate that the component depends on value
? Let me know your thoughts.
import { observable, autorun } from 'mobx';
import { promisedComputed } from 'computed-async-mobx';
class MyState {
@observable foo = 1;
bar = promisedComputed('loading', async () => {
return Promise.resolve('loaded' + this.foo);
}
@computed
get bar2(){
return this.bar.get();
}
}
let myState = new MyState();
autorun(() => {
console.log(myState.bar); //doesnt work
});
autorun(() => {
console.log(myState.bar2); //works
});
is there a way to use bar directily without .get() like i do with @computed?
there where no full examples in the documentation.
Hey, I've noticed that after updating to 2.0.0 my computed-async
with delay
stopped working. I've run through the new code and I can see that the delay
variable is not used anywhere.
After going back to 1.2.0 everything works fine again.
Hello,
I am new to MobX so it could be stupid question :)
I wrote the following code which works like a charm:
@observable time1: number | undefined
@observable near: string | undefined
@observable far: string | undefined
fixingDateNearPromise = promisedComputed('',
async () => {
if(!this.time1 || !this.near)
return ''
const request = {
underlyingCurrency: this.near,
valuedCurrency: this.far,
crossCurrency: '',
period: formatDate(this.time1)
}
const response = await REST.quote.getFixingDate(request)
return response.data.valueDateAndFixingDate.fixingDate
}
)
@computed
get fixingDateNear() {
return this.fixingDateNearPromise.get()
}
Then I tried to optimize this code, since I need two computed values that derive their value from this.time1 and this.time2:
fixingDatePromise = (time) => promisedComputed('',
async () => {
if(!time || !this.near)
return ''
const request = {
underlyingCurrency: this.near,
valuedCurrency: this.far,
crossCurrency: '',
period: formatDate(time)
}
const response = await REST.quote.getFixingDate(request)
return response.data.valueDateAndFixingDate.fixingDate
}
)
@computed
get fixingDateNear() {
return this.fixingDatePromise(this.time1).get()
}
@computed
get fixingDateFar() {
return this.fixingDatePromise(this.time2).get()
}
The code above resulted in cycle in MobX:
Uncaught Error: [mobx] Cycle detected in computation [email protected]: function get() {
try {
this.refreshCallCount;
var promiseOrValue = this.fetch();
return isPromiseLike(promiseOrValue) ? mobx_utils_1.fromPromise(promiseOrValue.then(value, function (e) {
return error(e);
})) : value(promiseOrValue);
} catch (x) {
return error(x);
}
}
at invariant (mobx.module.js?7277:90)
at fail (mobx.module.js?7277:85)
at ComputedValue.get (mobx.module.js?7277:879)
at ObservableObjectAdministration.read (mobx.module.js?7277:3880)
at PromisedComputed.get (mobx.module.js?7277:4144)
at eval (eval at <anonymous> ($rfsq-panel.tsx?efef:67), <anonymous>:1:6)
at PromisedComputed.eval [as fetch] ($rfsq-panel.tsx?efef:324)
at PromisedComputed.get (promisedComputed.js?1a17:34)
at trackDerivedFunction (mobx.module.js?7277:1154)
at ComputedValue.computeValue (mobx.module.js?7277:946)
Could you please let me know what is wrong here?
Thanks.
@observable values
throttledValues = throttledComputed(() => {
return this.values
}, 2000)
When changing values, throttledValues changes immediately. This function is actually run only in the beginning. Is it the intended behavior ?
If so, how my intention can still be implemented?
Hi,
this an excerpt of my current code:
export class ConnectorsStore {
constructor(private _orgStore = orgStore) {}
promisedConnectors = promisedComputed(null, () => {
return fetch(`${this._orgStore.selectedOrgId}/connectors/`).catch(e => e);
});
}
and I'm using the promisedConnectors
property inside a React component. The function correctly refetches the "connectors" when the selectedOrgId
changes.
However I wonder how I can manually reattempt a fetch / invalidate the cache etc.?
This is in particular required in cases where the fetch
request failed / errored and the user wants to retry it (via a "retry" button).
Mobx-utils for example has the refresh
method on lazyObservable
: https://github.com/mobxjs/mobx-utils#lazyobservable which serves a similar purpose.
I was able to get a similar behaviour with promisedComputed with the following code, but I feel it's kind of a hack:
export class ConnectorsStore {
constructor(private _orgStore = orgStore) {}
@observable refreshRequests = 0;
@action.bound
refresh() {
this.refreshRequests++;
}
promisedConnectors = promisedComputed(null, () => {
this.refreshRequests;
return fetch(`${this._orgStore.selectedOrgId}/connectors/`).catch(e => e);
});
}
In my react component I would have a a button <Button onClick={connectorsStore.refresh}>Retry</Button>
.
Is there a cleaner "refresh" solution with promisedComputed possible?
Thanks and Best Regards Christian
I understand how to use this library with mobx but I can't seem to find examples on how to use it with mobx-state-tree? Any thoughts?
I thought the idea of throttling was to only delay long enough to prevent repeated re-evaluations within the delay period. ie delay should not happen unless there has been a re-evaluation in the last delay
ms.. Or am I missing something?
Hi, we're using computed-async-mobx (thanks for it!) and recently when trying to update to Mobx6 we encountered an error.
This prompted me to look into this repo and I see it's been a while since it was last updated, I wonder whether it's still maintained and if there are any plans to make it work with Mobx6?
Thanks!
Is there any React Hooks implementation of same?
I discovered this nasty bug which I've reproduced here: https://jsfiddle.net/qc26pb4k/2/. With your console open, press "Show". This will trigger the computed being observed, and it will resolve. Then press "Hide". This unmounts a react component, which "unobserves" the computed async value.
It will throw a very cryptic mobx error [mobx] Invariant failed: INTERNAL ERROR only onBecomeUnobserved shouldn't be called twice in a row
. It seems like it only happens when .value
is used in a computed property. For some reason, unobserving the computed property triggers onBecomeUnobserved
twice. Weird.
Also, I apologize for the copy/pasted computed-async-mobx code--it was the only way I could get it running in a fiddle.
(This might actually be a mobx bug, let me know what you think).
How it can be done ?
I am changing the dataset variable but asyncComputed is not triggering.
What could be the cause?
@observable dataset = undefined;
indicators = asyncComputed([], 0, async () => {
const dataset = this.dataset;
const indicators = await fetchSomething( {dataset} )
return indicators
})
Maybe I forgot .get()?
If so, it would be very helpful to have its package version spec to not pull in mobx 3.x in mobx 2.x projects.
Hi @danielearwicker ๐
first of all, thanks for this amazing little library. This is exactly I was looking for :)
We've written a little helper function in order to wait for async computed properties in regular async functions while still benefiting from memoization of computed properties:
import { PromisedComputedValue } from "computed-async-mobx";
import { when } from "mobx";
export const waitForAsyncComputed = async <T>(
asyncComputed: PromisedComputedValue<T>,
): Promise<T> => {
let value: T;
await when(() => {
value = asyncComputed.get();
return asyncComputed.busy === false;
});
return value!;
};
What do you think? Is this good or kind of an anti-pattern? ๐ Would you be interested in adding this to the library?
Is this a better pattern than the normal way of having actions w/ side effects mutate state after making an async call and then static/normal computed functions derive from that state? Is there something missing from that paradigm that is accomplished here? It's probably obvious but I am missing it.
pretty self explanatory, this is a very important dependency and needs to be maintained, maybe under mobx-utils dependency
I am getting following error while using promiseComputed
inside mobx project with strict mode enabled.
promisedComputed.js:26 Uncaught Error: [mobx] Since strict-mode is enabled, changing observed observable values outside actions is not allowed. Please wrap the code in an `action` if this change is intended. Tried to modify: [email protected]
at invariant (mobx.module.js:148)
at fail (mobx.module.js:143)
at checkIfStateModificationsAreAllowed (mobx.module.js:1186)
at ObservableValue.prepareNewValue (mobx.module.js:793)
at ObservableObjectAdministration.write (mobx.module.js:3956)
at PromisedComputed.set [as refreshCallCount] (mobx.module.js:4179)
at PromisedComputed.set [as refreshCallCount] (mobx.module.js:369)
at new PromisedComputed (promisedComputed.js:26)
at promisedComputed (promisedComputed.js:109)
at asyncComputed (asyncComputed.js:28)
Enabled flag is
configure({ enforceActions: 'always' });
Mobx Versions:
"mobx": "^5.8.0",
"mobx-react": "^5.4.3",
"mobx-react-lite": "^1.4.1"
In this section: https://github.com/danielearwicker/computed-async-mobx#gotchas
It would be significantly more helpful to provide a "do" and "don't do" examples. Please consider :)
Hi @danielearwicker ,
when I use .get() inside the render() function of an @observer I get an error ร Error: promisedComputed must be used inside reactions
. I thought the render function of a React component is also considered an reaction or am I wrong?
Here's a small example
@observer
class ConnectorTable extends React.Component<Props> {
render() {
const { connectorsStore } = this.props;
const connectors = connectorsStore.connectors.get();
if (!connectors) {
return (
<Loader active size="huge">
Loading Connectors please wait...
</Loader>
);
}
return (
<Page title="Connectors">
<Item.Group>{connectors.map(renderConnector)}</Item.Group>
</Page>
);
}
}
export class ConnectorsStore {
constructor(private _orgStore = orgStore) {}
connectors = promisedComputed(null, async () => {
return fetchConnectors(this._orgStore.selectedOrgId);
});
}
When I use .value
it works as expected (because .value doesn't do the reaction check), but because .value is private typescript complains about it's usage.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.