Suppose I have this data in spanish.json
:
[
{"word": "casa", "translation": "house"},
{"word": "coche", "translation": "car"},
{"word": "calle", "translation": "street"}
]
And I have a Dictionary class that loads it and adds a search method:
// Dictionary.js
class Dictionary {
constructor(url){
this.url = url;
this.entries = []; // we’ll fill this with a dictionary
this.initialize();
}
initialize(){
fetch(this.url)
.then(response => response.json())
.then(entries => this.entries = entries)
}
find(query){
return this.entries.filter(entry =>
entry.word == query)[0].translation
}
}
And I can instantiate that, and use it to look up ‘calle’ with this little single-page app:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>spanish dictionary</title>
</head>
<body>
<p><input placeholder="Search for a Spanish word" type="">
<p><output></output>
<script src=Dictionary.js></script>
<script>
let es2en = new Dictionary('spanish.json')
console.log(es2en.find('calle')) // 'street'
input.addEventListener('submit', ev => {
ev.preventDefault();
let translation = dictionary.find(ev.target.value);
output.innerHTML = translation;
})
</script>
</body>
</html>
So far so good. But, let’s say I want to subclass Dictionary
and add a method that counts all the words and adds that
count to the page. (Man, I need some investors.)
So, I get another round of funding and implement CountingDictionary
:
class CountingDictionary extends Dictionary {
constructor(url){
super(url)
}
countEntries(){
return this.entries.length
}
}
The new single page app:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Counting Spanish Dictionary</title>
</head>
<body>
<p><input placeholder="Search for a Spanish word" type="">
<p><output></output>
<script src=Dictionary.js></script>
<script>
let
es2en = new CountingDictionary('spanish.json'),
h1 = document.querySelector('h1'),
input = document.querySelector('input'),
output = document.querySelector('output');
h1.innerHTML = es2en.countEntries();
input.addEventListener('input', ev => {
ev.preventDefault();
let translation = es2en.find(ev.target.value);
if(translation)
output.innerHTML = `${translation}`;
})
</script>
</body>
</html>
When this page loads, the h1
gets populated with 0
.
I know what my problem is, I just don’t how to fix it.
The problem is that the fetch
call returns a Promise
,
and the .entries
property is only populated with the data
from the URL once that Promise has returned. Until then,
.entries
remains empty.
How can I make .countEntries
wait for the fetch promise to resolve?
Or is there a better way entirely to achieve what I want here?
Suppose I have this data in spanish.json
:
[
{"word": "casa", "translation": "house"},
{"word": "coche", "translation": "car"},
{"word": "calle", "translation": "street"}
]
And I have a Dictionary class that loads it and adds a search method:
// Dictionary.js
class Dictionary {
constructor(url){
this.url = url;
this.entries = []; // we’ll fill this with a dictionary
this.initialize();
}
initialize(){
fetch(this.url)
.then(response => response.json())
.then(entries => this.entries = entries)
}
find(query){
return this.entries.filter(entry =>
entry.word == query)[0].translation
}
}
And I can instantiate that, and use it to look up ‘calle’ with this little single-page app:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>spanish dictionary</title>
</head>
<body>
<p><input placeholder="Search for a Spanish word" type="">
<p><output></output>
<script src=Dictionary.js></script>
<script>
let es2en = new Dictionary('spanish.json')
console.log(es2en.find('calle')) // 'street'
input.addEventListener('submit', ev => {
ev.preventDefault();
let translation = dictionary.find(ev.target.value);
output.innerHTML = translation;
})
</script>
</body>
</html>
So far so good. But, let’s say I want to subclass Dictionary
and add a method that counts all the words and adds that
count to the page. (Man, I need some investors.)
So, I get another round of funding and implement CountingDictionary
:
class CountingDictionary extends Dictionary {
constructor(url){
super(url)
}
countEntries(){
return this.entries.length
}
}
The new single page app:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Counting Spanish Dictionary</title>
</head>
<body>
<p><input placeholder="Search for a Spanish word" type="">
<p><output></output>
<script src=Dictionary.js></script>
<script>
let
es2en = new CountingDictionary('spanish.json'),
h1 = document.querySelector('h1'),
input = document.querySelector('input'),
output = document.querySelector('output');
h1.innerHTML = es2en.countEntries();
input.addEventListener('input', ev => {
ev.preventDefault();
let translation = es2en.find(ev.target.value);
if(translation)
output.innerHTML = `${translation}`;
})
</script>
</body>
</html>
When this page loads, the h1
gets populated with 0
.
I know what my problem is, I just don’t how to fix it.
The problem is that the fetch
call returns a Promise
,
and the .entries
property is only populated with the data
from the URL once that Promise has returned. Until then,
.entries
remains empty.
How can I make .countEntries
wait for the fetch promise to resolve?
Or is there a better way entirely to achieve what I want here?
Share Improve this question edited Sep 8, 2016 at 15:44 Bergi 666k161 gold badges1k silver badges1.5k bronze badges asked Sep 8, 2016 at 15:29 user2467065user24670653 Answers
Reset to default 5The problem is that the
fetch
call returns aPromise
, and the.entries
property is only populated with the data from the URL once that Promise has returned. Until then,.entries
remains empty.
You would need to make entries
a promise. That way, all of your methods had to return promises, but the Dictionary
instance is immediately usable.
class Dictionary {
constructor(url) {
this.entriesPromise = fetch(url)
.then(response => response.json())
}
find(query) {
return this.entriesPromise.then(entries => {
var entry = entries.find(e => e.word == query);
return entry && entry.translation;
});
}
}
class CountingDictionary extends Dictionary {
countEntries() {
return this.entriesPromise.then(entries => entries.length);
}
}
let es2en = new CountingDictionary('spanish.json'),
h1 = document.querySelector('h1'),
input = document.querySelector('input'),
output = document.querySelector('output');
es2en.countEntries().then(len => {
fh1.innerHTML = len;
});
input.addEventListener(ev => {
ev.preventDefault();
es2en.find(ev.target.value).then(translation => {
if (translation)
output.innerHTML = translation;
});
});
Or is there a better way entirely to achieve what I want here?
Yes. Have a look at Is it bad practice to have a constructor function return a Promise?.
class Dictionary {
constructor(entries) {
this.entries = entries;
}
static load(url) {
return fetch(url)
.then(response => response.json())
.then(entries => new this(entries));
}
find(query) {
var entry = this.entries.find(e => e.word == query);
return entry && entry.translation;
}
}
class CountingDictionary extends Dictionary {
countEntries() {
return this.entries.length;
}
}
let es2enPromise = CountingDictionary.load('spanish.json'),
h1 = document.querySelector('h1'),
input = document.querySelector('input'),
output = document.querySelector('output');
es2enPromise.then(es2en => {
fh1.innerHTML = es2en.countEntries();
input.addEventListener(…);
});
As you can see, this appraoch requires less overall nesting pared to an instance that contains promises. Also a promise for the instance is better posable, e.g. when you would need to wait for domready before installing the listeners and showing output you would be able to get a promise for the DOM and could wait for both using Promise.all
.
You have to assign the result of the fetch()
call to some variable, for example:
initialize(){
this.promise = fetch(this.url)
.then(response => response.json())
.then(entries => this.entries = entries)
}
Then you can call the then()
method on it:
let es2en = new CountingDictionary('spanish.json'),
h1 = document.querySelector('h1'),
input = document.querySelector('input'),
output = document.querySelector('output');
es2en.promise.then(() => h1.innerHTML = es2en.countEntries())
input.addEventListener('input', ev => {
ev.preventDefault();
let translation = es2en.find(ev.target.value);
if(translation)
output.innerHTML = `${translation}`;
})
A simple solution: Keep the promise after you do fetch()
, then add a ready()
method that allows you to wait until the class has initialized pletely:
class Dictionary {
constructor(url){
/* ... */
// store the promise from initialize() [see below]
// in an internal variable
this.promiseReady = this.initialize();
}
ready() {
return this.promiseReady;
}
initialize() {
// let initialize return the promise from fetch
// so we know when it's pleted
return fetch(this.url)
.then(response => response.json())
.then(entries => this.entries = entries)
}
find(query) { /* ... */ }
}
Then you just call .ready()
after you've constructed your object, and you'll know when it's loaded:
let es2en = new CountingDictionary('spanish.json')
es2en.ready()
.then(() => {
// we're loaded and ready
h1.innerHTML = es2en.countEntries();
})
.catch((error) => {
// whoops, something went wrong
});
As a little extra advantage, you can just use .catch
to detect errors that occur during loading, e.g., network errors or uncaught exceptions.