0

I'm working on a meteor react project and want to use load data from local storage which happens async. Unfortunately I wasn't able to get the data out of callback even with binds. I tried multiple ways but could not get any of them to work, surely I'm missing something.

I stripped as much away as I could but had to keep some for the context.

From my understanding it can only be related to the track object as setting those simple Integers, Booleans works fine.

render() {
    const { audioCollection } = this.props; // database collection, async hence the following if check
    if (audioCollection) {
        this.tracks = audioCollection.audio(); // setting tracks for later use
        // make sure tracks are loaded and only run once, as we do this in the react renderer
        if (this.tracks && !this.state.tracksLoaded) { 
            var trackLoadedCount = 0;
            this.tracks.forEach((track, i) => { // I tried to use forEach and map here
                // now we try to load the data from local storage and if it fails fall back to the remote server
                LocalForage.getItem(track.file_id).then(function(err, file) {
                    if (!err && file) {
                        console.log(track.file_id + ' from cache')
                        var blob = new Blob([file]);
                        fileURI = window.URL.createObjectURL(blob);
                    } else {
                        console.log(track.file_id + ' from database')
                        fileURI = audioCollection.audioLink(track.file_id);
                    }
                    track.fileURI = fileURI; // assigning the retrieved data uri to the track object, also tried to set it on the original parent object not the extracted one from the forEach/map
                    console.log(fileURI + ' ' + track.fileURI) // yes, both are set
                    trackLoadedCount++; // increasing the tracks loaded count, to calculate if all have been loaded and to test if it can write outside of the callback, which it does
                    // if all files have been processed, set state loaded, this works too.
                    if (trackLoadedCount == this.tracks.length) {
                        this.setState({
                            tracksLoaded: true,
                        })
                    }
                }.bind(track, this))
            });
        }
    }
    // once all has been processed we try to retrieve the fileURI, but it isn't set :(
    if (audioCollection && this.tracks && this.state.tracksLoaded) {
        console.log('all loaded ' + this.tracks.length)
        this.tracks.map(track => {
            console.log('track: ' + track.file_id + ' ' + track.fileURI) // file_id is set, fileURI is not
        })
    }

    // we only log
    return (
        <Content {...this.props}>
            <div>just console output</div>
        </Content>
    );
}

I tried more approaches:

  • Writing the tracks as an array to state like tracksLoaded (didn't work work)
  • Defining a new var before the async call and setting its values from within the callback, like trackLoadedCount (with and without bind) (doesn't work)

Why isn't this working while its working for tracksLoaded and trackLoadedCount?

Update regarding Firice Nguyen Answer

render() {
    const { audioCollection } = this.props;
    if (audioCollection) {
        this.tracks = audioCollection.audio();
        if (this.tracks && !this.state.tracksLoaded) {
            var trackLoadedCount = 0;
            this.tracks.forEach((track, i, trackArray) => {
                LocalForage.getItem(track.file_id).then(function(err, file) {
                    if (!err && file) {
                        console.log(track.file_id + ' from cache')
                        var blob = new Blob([file]);
                        fileURI = window.URL.createObjectURL(blob);
                    } else {
                        console.log(track.file_id + ' from database')
                        fileURI = audioCollection.audioLink(track.file_id);
                    }
                    track.fileURI = fileURI;
                    console.log('1. ' + track.file_id + ' ' + track.fileURI);
                    trackArray[i] = track;
                    console.log('2. ' + track.file_id + ' ' + trackArray[i].fileURI);
                    trackLoadedCount++;
                    if (trackLoadedCount == this.tracks.length) {
                        this.setState({
                            tracksLoaded: true,
                        })
                    }
                }.bind(track, this))
            });
        }
    }
    if (audioCollection && this.tracks && this.state.tracksLoaded) {
        console.log('all loaded ' + this.tracks.length)
        this.tracks.map(track => {
            console.log('3. ' + track.file_id + ' ' + track.fileURI) // file_id is set, fileURI is not
        })
    }

    return (
        <Content {...this.props}>
            <div>just console output</div>
        </Content>
    );
}

returns

MXqniBNnq4zCfZz5Q from database
1. http://localhost:3000/cdn/storage/files/MXqniBNnq4zCfZz5Q/original/MXqniBNnq4zCfZz5Q.m4a
2. http://localhost:3000/cdn/storage/files/MXqniBNnq4zCfZz5Q/original/MXqniBNnq4zCfZz5Q.m4a
keBWP6xb9PyEJhEzo from database
1. http://localhost:3000/cdn/storage/files/keBWP6xb9PyEJhEzo/original/keBWP6xb9PyEJhEzo.m4a
2. http://localhost:3000/cdn/storage/files/keBWP6xb9PyEJhEzo/original/keBWP6xb9PyEJhEzo.m4a
K2J2W9W26DDBNoCcg from database
1. http://localhost:3000/cdn/storage/files/K2J2W9W26DDBNoCcg/original/K2J2W9W26DDBNoCcg.m4a
2. http://localhost:3000/cdn/storage/files/K2J2W9W26DDBNoCcg/original/K2J2W9W26DDBNoCcg.m4a
all loaded 3
3. MXqniBNnq4zCfZz5Q undefined
3. keBWP6xb9PyEJhEzo undefined
3. K2J2W9W26DDBNoCcg undefined

hence the issue persists.

user2693017
  • 1,750
  • 6
  • 24
  • 50

1 Answers1

0

The forEach give out a copy of the element. The track is just a copy, not the original one. It does not point to the element in your array. You can try this:

this.tracks.forEach(track, i, trackArray) => {
    // change `track` value
    ...
    trackArray[i] = track;
});

Or try map method

An Nguyen
  • 1,487
  • 10
  • 21
  • There is NO WAY that forEach does that is it o.0 It would make no freaking sense! I am not saying it is untrue, but i'm shocked. Do you have any documentational reference for this claim? – Dellirium Jul 20 '16 at 14:37
  • I worked with C++ until this June. May be the way I'm explaining is more likely to the C pointer. As far as I know, Javascript doesn't have thing like "pass-by-reference", it depends on the calling context. In `forEach` case, it depends on the callback that trigger `forEach`, not the array itself. So far, those are what I understand about Javascript until now. The solution I suggest is from my experience, please correct me if I am wrong. – An Nguyen Jul 20 '16 at 20:20
  • Unfortunately I used map before which didn't work either and I also tried to assigning it to the parent object `this.tracks[i].fileURI = fileURI;` which didn't work either :( – user2693017 Jul 20 '16 at 20:27
  • If you are assigning it inside the `forEach` block, I'm not sure `this` is pointing to your component, which contain the `tracks` props. Please use the array as in my example. – An Nguyen Jul 20 '16 at 20:35
  • @FiriceNguyen according to this https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach the callback function gets passed the three arguments so within it you can use the 1st argument passed to reference the initial array's element, or by index (2nd argument). Now if this helps awesome, if not. idk – Dellirium Jul 20 '16 at 22:15
  • According to the link: "If a thisArg parameter is provided to forEach(), it will be passed to callback when invoked, for use as its `this` value". And within `forEach`, `this` is not pointing to the array itself. So that the `element` we are modifying inside is not referenced to the original one. I found another answer with picture to demonstrate the `forEach` case: http://stackoverflow.com/questions/19707969/the-invocation-context-this-of-the-foreach-function-call/19709342#19709342 – An Nguyen Jul 21 '16 at 07:05
  • @FiriceNguyen I tested it exactly as you wrote it, unfortunately it didn't change a thing, see updated question. – user2693017 Jul 21 '16 at 21:21
  • You missed the third parameter in your `forEach`: `this.tracks.forEach((track, i) => {`. It should be `this.tracks.forEach(track, i, trackArray) => {`. The 3rd parameter `trackArray`is the one point to your array so that later you can assign `trackArray[i] = track;` – An Nguyen Jul 22 '16 at 06:11
  • ahh sorry, this was an oversight when copying. I'll see about creating a minimalistic project which you can run with one command, so that it isn't just guessing. Seeing now I should have done that from the beginning. – user2693017 Jul 22 '16 at 20:31