简体   繁体   中英

Knockout JS - Promise recursion

I have a situation in which I have to update an Knockout observable bound to an HTML element and its value is being updated by an asynchronous operation (fetch from a server).

I wrote the following sample code:

 const viewModel = function() { const self = this; self.fetchResults = function() { const id = ko.observable(0); fetch("https://jsonplaceholder.typicode.com/photos") .then(function(response) { return response.json(); }) .then(function(data) { id(data.length); console.log(id()); }) return id; }; }; ko.applyBindings(new viewModel()); 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script> <div data-bind="text: fetchResults()"></div> 

It is creating an infinite recursive loop.

Is Knockout calling the function repeatedly because value is being updated inside an asynchronous function? What could be the root cause of this issue?

TIA!

Edit:

I have multiple HTML elements on the page requesting different information to be bound to them, for example,

<div data-bind="text: fetchResults('some-url')"></div>
<div data-bind="text: fetchResults('some-different-url')"></div>
<div data-bind="text: fetchResults('another-url-altogether')"></div>

That would mean I need to create observable inside the fetchResults function which I did (please correct me if my understanding is amiss :))

The problem is that since you're using an expression, the function gets run on render, returning an observable. When the observable's value changes, it makes that part of the view render again, which calls the function again, which cycles forever.

In a comment you've said:

Actually I need to call this method multiple times for different fetch URIs in the same page, and bind to separate HTML elements.

...and added this example to the question:

 <div data-bind="text: fetchResults('some-url')"></div> <div data-bind="text: fetchResults('some-different-url')"></div> <div data-bind="text: fetchResults('another-url-altogether')"></div> 

That does change things a bit. I'd probably use a custom binding rather than a function:

 ko.bindingHandlers.textFetch = { update(element, valueAccessor) { const url = ko.unwrap(valueAccessor()); element.textContent = "0"; fetch(url) .then((response) => { if (!response.ok) { throw new Error("HTTP error " + response.status); } return response.json(); }) .then((data) => { element.textContent = data.length; }); } }; const viewModel = function() { }; ko.applyBindings(new viewModel()); 
 <div data-bind="textFetch: 'https://jsonplaceholder.typicode.com/photos'"></div> <div data-bind="textFetch: 'https://jsonplaceholder.typicode.com/posts'"></div> <div data-bind="textFetch: 'https://jsonplaceholder.typicode.com/comments'"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script> 


But if you want to keep it more similar to your current code, keep a cache of observables for urls:

 const viewModel = function() { const self = this; // In a modern environment, `observables` could be a Map rather than an object const observables = Object.create(null); self.fetchResults = function(url) { // Get the observable if we have it let id = observables[url]; if (!id) { // We don't, create and remember one, do the fetch id = observables[url] = ko.observable(0); fetch(url) .then(function(response) { return response.json(); }) .then(function(data) { id(data.length); }) } return id; }; }; ko.applyBindings(new viewModel()); 
 <div data-bind="text: fetchResults('https://jsonplaceholder.typicode.com/photos')"></div> <div data-bind="text: fetchResults('https://jsonplaceholder.typicode.com/posts')"></div> <div data-bind="text: fetchResults('https://jsonplaceholder.typicode.com/comments')"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script> 


Side note: You're not checking for success in your fetch callback. You're not the only one, I'd go so far as to say the fetch API is poorly-designed as I see this footgun being fired just about every day here on SO. I've written it up on my anemic little blog, but basically, you need to add:

if (!response.ok) {
  throw new Error("HTTP error " + response.status);
}

...in your fulfillment handler.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM