Take the following TypeScript/Angular 2 code sample:
query(): Rx.Observable<any> {
return Observable.create((o) => {
var refinedPosts = new Array<RefinedPost>();
var observable = this.server.get('http://localhost/rawData.json').toRx().concatMap(
result =>
result.json().posts
)
.map((post: any) => {
// Assume I want to convert the raw JSON data into a nice class object with
// methods, etc.
var refinedPost = new RefinedPost();
refinedPost.Message = post.Message.toLowerCase();
refinedPosts.push(refinedPost);
})
.subscribeOnCompleted(() => {
o.onNext(refinedPosts);
})
});
}
Written out, the database is returning JSON. I want to iterate over the raw JSON and create a custom object, eventually returning to subscribers an Array<RefinedPost>.
The code works and the final subscribers get what they need, but I can't help but feel like I didn't do it the "Reactive Way". I cheated and used an external accumulator to gather up the elements in the Array, which seems to defeat the purpose of using streams.
So, the question is, is there a better, more concise, reactive way to write this code?
Answering my own question.
query(): Rx.Observable<any> {
return this.server.get('http://localhost/rawData.json').toRx().concatMap(
result =>
result.json().posts
)
.map((post: any) => {
var refinedPost = new RefinedPost();
refinedPost.Message = post.Message.toLowerCase();
return refinedPost;
}).toArray();
}
This removes the internal accumulator and the wrapped Observable. toArray() took the sequence of items and brought them together into an array.
Related
I'm trying to display game information from the Steam API in a React Native Flatlist. I'm new to React and JSX, so a lot of what I'm reading doesn't make sense.
I want the Flatlist to display a list of game titles owned by a particular account. The data returned from Steam's API call (via fetch) looks like this:
{
"response": {
"game_count": 69,
"games": [
{
"appid": 220,
"name": "Half-Life 2",
"playtime_forever": 24,
"img_icon_url": "fcfb366051782b8ebf2aa297f3b746395858cb62",
"img_logo_url": "e4ad9cf1b7dc8475c1118625daf9abd4bdcbcad0",
"has_community_visible_stats": true,
"playtime_windows_forever": 0,
"playtime_mac_forever": 0,
"playtime_linux_forever": 0
},
{
"appid": 320,
"name": "Half-Life 2: Deathmatch",
"playtime_forever": 0,
"img_icon_url": "795e85364189511f4990861b578084deef086cb1",
"img_logo_url": "6dd9f66771300f2252d411e50739a1ceae9e5b30",
"has_community_visible_stats": true,
"playtime_windows_forever": 0,
"playtime_mac_forever": 0,
"playtime_linux_forever": 0
},
and so on. Since I'm trying to display a list of games by name, the name attribute is the only one I need.
The data lists each game as an anonymous object, so I can't access the properties within each game using dot notation like I normally would. I tried using a for loop to iterate through them, but that doesn't work either. From my research, it seems like people normally use an Array.map for this kind of thing, but I'm unclear if that can be used with Objects.
Another problem I've encountered is the Flatlist keyExtractor property. I know it's supposed to be an anonymous function that returns some unique index or property about each Flatlist item, for the purpose of making the structure more efficient and to allow it to track updates to the list. However, I have no idea how to create this function myself. I think the appid field from the JSON data would be a good candidate, but I'm not sure how to get that into the keyExtractor function.
So, to put it as a question: How would I go about displaying data from a JSON object containing anonymous sub-objects in a Flatlist, and how would I populate the keyExtractor of that list with a different data entry (the appid from that list?
Below is my starting code:
import React, {Component} from 'react';
import {FlatList, Stylesheet, Text, View} from 'react-native';
export default class App extends Component {
state = {
dataset: []
};
componentWillMount() {
this.fetchData();
}
fetchData = async () => {
const response = await fetch("<API URL>");
const json = await response.json();
//const data = json.map((item) => item.games.name);
var key = 0;
const data = json[games][0][name];
this.setState({ dataset: data });
}
render() {
console.log(this.state.dataset);
return (
<View>
<FlatList
data={this.state.dataset}
keyExtractor={(x, i) => i} //I have no idea what this does, or if it makes sense here.
//Where do x and i come from? (I got this from a tutorial video
//and this was glossed over)
renderItem={({ item }) => //Where does item come from?
<Text>
{item}
</Text>
}
/>
</View>
);
}
}
Alright, it seems you're having a few minor problems with understanding how FlatList works. Let me break it down for you.
Let's start with the Steam API request. In your example, you're first declaring dataset as an empty array in your state, then trying to update it with the result of a network request which is the way to go. The problem is, when you do json['games'][0]['name'] you're accessing the first item (index 0) of the games array and getting its name property and then setting that name as your dataset. Although you forgot the quotes around property names, it won't work. What you need to do instead is something like this:
fetchAllGames = async () => {
const steamResponse = await fetch("<API URL>");
const json = await steamResponse.json();
// We get all the games back from Steam in the form of an array
this.setState({ games : json.games });
}
We're now correctly updating the array inside our state with the data from the games array.
Let's move on to the keyExtractor and renderItem functions. The keyExtractor function is used to tell React about a unique identifier for each of your list items. In this case, this would be the appid property of a game. React then uses this unique ID to differentiate between items and determine which ones need updating. This function provides you with two parameters, namely the actual item and its index. Using these, we can then do something like this:
keyExtractor = (item, index) => {
return item.appid.toString();
}
We're now returning the appid property as a string (which is the type React expects key to be).
The renderItem function is a bit different, React is providing you with a parameter which contains your item plus a lot of other properties. Since we're only interested in the actual item, we're destructuring it using brackets like so: { item }. This is a technique commonly used in JavaScript to "extract" properties from objects. It is normally used like this:
const testObj = {
name : "John",
surname : "Doe"
}
const { name, surname } = testObj;
This way, you can directly refer to name and surname as if they were independent variables. Another way of doing this would be:
const testObj = {
name : "John",
surname : "Doe"
}
const name = testObj.name;
const surname = testObj.surname;
I hope this cleared some of the questions you might've been asking yourself! Here's the complete working code below. You may notice I moved some inline functions to class members, this is just a performance optimization to prevent the functions from being recreated on every render, you can ignore that.
import React, { Component } from 'react';
import { FlatList, Text } from 'react-native';
export default class App extends Component {
state = {
games : []
};
componentDidMount() {
this.fetchAllGames();
}
fetchAllGames = async () => {
const steamResponse = await fetch("<API URL>");
const json = await steamResponse.json();
// We get all the games back from Steam in the form of an array
this.setState({ games : json.response.games });
}
keyExtractor = (item, index) => {
return item.appid.toString();
}
renderItem = ({item}) => {
return (
<Text>{item.name}</Text>
);
}
render() {
return (
<FlatList
data={this.state.games}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem} />
);
}
}
EDIT #1 - As pointed out by the OP, I made a typo and corrected it. I also changed the JSON object to reflect the response property.
I have a problem concating 2 json objects together. Basicly my app is doing a get on my rest server every second and i'm only sending the newest data back so as angular is refreshing the whole object i found on google that i can concat the 2 jsons together (old and new) so i can keep everything. But the problem is that none of the concat/merge/extend functions work and i don't know what i'm missing.
data: any = null;
constructor(private _http: Http) {
setInterval(() => this.getLogs(), 1000)
}
public getLogs() {
return this._http.get('http://localhost')
.map((res: Response) => res)
.subscribe(data => {
if data._body != ''{
//this.data = data.json()
if this.data == null
this.data = data.json();
else
extend(this.data,data.json()); // PROBLEM HERE
}
console.log(this.data);
});
}
So far i tried this.data.concat(data.json()); if i try extend(this.data, data.json()) or merge(this.data, data.json()); I get errors saying that it's not defined. The concat function doesn't do anything. Doesn't trigger errors neither concat so i don't know what it is doing.
I'm logging the object everytme and i can see the object always stays at the first ever response i get (meaning it only does the if this.data == null).
https://www.w3schools.com/jsref/jsref_concat_array.asp states
The concat() method is used to join two or more arrays.
This method does not change the existing arrays, but returns a new
array, containing the values of the joined arrays.
So you need to concat the two arrays into the data variable
data: any = null;
constructor(private _http: Http) {
setInterval(() => this.getLogs(), 1000)
}
public getLogs() {
return this._http.get('http://localhost')
.map((res: Response) => res)
.subscribe(data => {
if data._body != ''{
//this.data = data.json()
if this.data == null
this.data = data.json();
else
this.data = this.data.concat(data.json());
}
console.log(this.data);
});
}
You can use spread operator to generate new object:
this.data = {...this.data, ...data.json()};
What this does is create a new object and then first migrates all the fields and values from this.data and then same thing from data.json() while overriding any existing fields that were already in this.data.
Not sure where you're getting extend from. That's not a function.
You can't concat two objects together. You're calling res.json(), so the return is no longer JSON. Even if you were, you can't just concat JSON strings together and expect the result to be valid.
You'd want to merge the objects together, which can be done with Object.assign(this.data, data.json() or a spread: this.data = {...this.data, ...data.json()}.
On top of that, you'd want to try/catch your JSON parsing before assigning. Plus, your map function is doing literally nothing. You can parse it there instead.
You can also streamline this by just initializing data to an empty object.
public data: any = {}
public getLogs() {
return this._http.get('http://localhost')
.map(res => res.json())
.filter(res => !!res) // ensure data exists
.subscribe(data => {
Object.assign(this.data, data);
});
}
Having said that, making a REST call every second seems like an egregious waste of resources and will put strain on Angular's change detection, with performance degrading as data increases. If the objects don't need to be merged, i.e. each call is segmented data, consider pushing new data to an array instead of an object. Plus, you might want to consider doing something a little more sane, like implementing an event stream like SSE (server sent events) on the backend.
I developped an Angular2 service to retrieve a list a categories from a backend server and count how many 'links' exist per category.
Once I have the number of links for each category, I add a property to the Json object to 'store' the value.
Here is the code:
nbLinks = '';
...
getCategories() {
return this.category.find({where: {clientId: this.userApi.getCurrentId()}}).map((data) => {
this.categoriesList = data;
for (var i = 0; i < this.categoriesList.length; i++) {
var obj = this.categoriesList[i].id;
this.category.countLinks(obj).subscribe((linksCount) => {
this.nbLinks = linksCount;
}, err => {
console.log(err);
});
}
return data;
},
err => {
console.log(err);
}
);
I am getting the categories in a json object with the correct 'where' clause.
I am looping on the Json to 'count' the number of link in this category.
My problem is that outside the for loop (getting out) the variable i is bigger than my Json length so the app is crashing.
My second problem is that I do not have the visiblity of this.nbLinks outside the for ... loop.
Thanks an Regards
I'm not sure I understand your code, but two things stand out:
1) It looks like you're mixing synchronous and asynchronous code. It cannot work.
Sync code: the for loop. Async code: the observable.
Instead, could you refactor your code to ONLY work with observables and chain all the operations? You can wrap any piece of data in an observable with Observable.from() or Observable.of().
For instance:
getCategories() {
const categories = this.category.find({where: {clientId: this.userApi.getCurrentId()}});
return Observable.from(categories)
.map(category => countLinksInCategory(category));
}
If countLinksInCategory() is an async operation, then have that function return an Observable, and use .mergeMap() instead of .map() in the code above.
2) Try avoiding setting an outside variable from within your observable
// This part is not ideal
Obs.subscribe(linksCount => {
this.nbLinks = linksCount;
});
I would suggest renaming getCategories() to getNumLinks() to reflect the role of the function. The only job of the Observable inside this function is to produce a value. Then, the consumer of the Observable can use that value (i.e. assign it, display it...).
In terms of code:
getNumLinks(): Observable<number> {
// Here, count the number of links - See example code above.
// Eventually, return an observable wrapping the final value.
}
Then, elsewhere in your code:
// This is where you assign the value returned by the Observable.
// Note that we are OUTSIDE the Observable now.
getNumLinks().subscribe(numLinks => this.nbLinks = numLinks);
I would like to fetch multiple files at once using the new fetch api (https://fetch.spec.whatwg.org/). Is is possible natively? If so, how should I do it leveraging the promises?
var list = [];
var urls = ['1.html', '2.html', '3.html'];
var results = [];
urls.forEach(function(url, i) { // (1)
list.push( // (2)
fetch(url).then(function(res){
results[i] = res.blob(); // (3)
})
);
});
Promise
.all(list) // (4)
.then(function() {
alert('all requests finished!'); // (5)
});
This is untested code! Additionally, it relies on Array.prototype.forEach and the new Promise object of ES6. The idea works like this:
Loop through all URLs.
For each URL, fetch it with the fetch API, store the returned promise in list.
Additionally, when the request is finished, store the result in results.
Create a new promise, that resolves, when all promises in list are resolved (i.e., all requests finished).
Enjoy the fully populated results!
While implementing Boldewyn's solution in Kotlin, I pared it down to this:
fun fetchAll(vararg resources: String): Promise<Array<out Response>> {
return Promise.all(resources.map { fetch(it) }.toTypedArray())
}
Which roughly translates to this in JavaScript:
function fetchAll(...resources) {
var destination = []
resources.forEach(it => {
destination.push(fetch(it))
})
return Promise.all(destination)
}
Earlier, I tried to use map instead of forEach + pushing to a new array, but for some reason that simply didn't work.
I have a practice problem and I need to replicate JSON.stringify, without actually using JSON.stringify. I was having trouble getting to the return value when it's an object that your inputting into the function. For example, if you insert
var obj = {a:1, b:2, c:3};
JSON.stringify (obj); // returns "{"a":1,"b":2,"c":3}"
That being said, I was trying to use a for in loop to set the property to the string value of the object that's being passed through. I was doing this only for an object container, but the function should actually work with anything you pass through it and it would JSON.stringify it.
var stringifyJSON = function(obj) {
var newObj = {};
for (var prop in obj){
newObj ={
stringProp:obj[prop]
};
}
return newObj;
};
I think I have the array portion down. I'm pretty terrible with object traversing. The result here is 'Object {stringProp:3}'. This is a practice problem within a recursion problem set, so I think they don't want me to use a loop. I was just trying to make it a little simpler by looping it, and then I would try to replicate it through recursion.
Any help would be appreciated!
Thanks,
B
Since it is a practice problem I'll just try to point you in the right direction:
You will need a loop in the code, as you will be required to recursively loop over each property in the object.
As for the recursive part, first you need to define your 'base case'. What is the condition based on the input that should not result in a recursive call? I'll give you a big hint, it's when the argument is not an object.
The tricky part for you is determining how you are going to be appending the resulting string from the recursive calls.
Some basic template code to start you off:
function myStringify( obj ) {
if ( typeof obj !== 'object' ) {
// base case
return ""+obj;
}
var str = "";
for ( var prop in obj ) {
if ( obj.hasOwnProperty( prop ) ) {
// recursive calls and string formatting magic
}
}
return str;
}