Svelte derived stores and array sort - javascript

I set up a store containing a list of rides loaded from my API:
const loadRides = () => client.service('rides').find({
query: {
$sort: {
date: -1,
}
}
});
const createRides = () => {
const { subscribe, update } = writable([], async (set) => {
try {
const rides = await loadRides().then((result) => result.data);
set(rides);
} catch (e) {
console.error(e);
}
// Socket update binding?
});
subscribe((rides) => console.debug('rides', rides));
return {
subscribe,
refresh: () => loadRides().then((result) => update(() => result.data)),
};
};
export const rides = createRides();
Then I set a two derived stores for past and future rides:
export const pastRides = derived(
rides,
($rides) => $rides
.filter((ride) => ride.steps.every((step) => step.doneAt))
,
);
export const comingRides = derived(
rides,
($rides) => $rides
.filter((ride) => ride.steps.some((step) => !step.doneAt))
.sort((rideA, rideB) => {
const compare = new Date(rideA.date) - new Date(rideB.date);
console.log(rideA.date, rideB.date, compare);
return compare;
})
,
);
The sort method on the second one does not have any effect.
So I tried to put this method before the filter one. It works, but it also sort $pastRides. In fact, it is sorting the full $rides array and it make sens.
But I does not understand why the sort after filter does not work.
What did I miss?

Array.sort is mutable. Meaning, when you call rides.sort, it will sort and modify rides and return the sorted rides.
When you use derived(rides, ($rides) => ... ), the $rides you received is the original rides array that you call set(rides). So you can imagine that both the pastRides and comingRides received the same $rides array.
you can observe this behavior in this repl
To not having both derived interfere with each other, you can create a new array and sort the new array:
const sorted_1 = derived(array, $a => [...$a].sort());
you can try this out in this repl.

Related

Combine two objects in react, no success so far

I'm trying to combine two object together without any luck...
I'm using the spread operator as you should in es6. I'm pretty sure it has something to do with useState. I will start by showing you the console.logs from my objects then the code :)
CONSOLE.LOGS
Object #1:
Object #2:
After combining the objects and logging the combined result I only get Object #1 like this:
MY CODE
const PortfolioComponent = () => {
const [clickedElement, setClickedElement] = useState({})
const [stockObject, setStockObject] = useState({})
const [stockData, setStockData] = useState({})
const getStock = () => {
axios.request(stock)
.then( res => {
console.log("2. Object #2 from API: ", res.data)
setStockData(res.data) // HERE I SET THE OBJECT FROM MY API CALL
})
.catch( error => {
console.error(error);
}
const handleOpen = name => { // In this function i call the api function and combine the two objects.
let findClickedStock = props.stocksArray.find(item => item.ticker === name)
setClickedElement(findClickedStock)
getStock();
console.log("1. Object #1: ", findClickedStock)
setTimeout(() => { // Waitning for the api to fetch
setStockObject({...findClickedStock, ...stockData}) // Here i combine the two objects, no success
console.log("3. Combined object - Only gets Object #1...", stockObject)
}, 2000);
setOpen(true);
};
}
export default PortfolioComponent;
From the Hooks API Reference:
Note
Unlike the setState method found in class components, useState does not automatically merge update objects. You can replicate this behavior by combining the function updater form with object spread syntax:
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
Also, check out useReducer which is generally recommended for managing state with multiple sub-values.
const getStock = () => {
return axios.request(stock)
.then( res => res.data)
.catch( error => {
console.error(error);
}
const handleOpen = name => { // In this function i call the api function and combine the two objects.
let findClickedStock = props.stocksArray.find(item => item.ticker === name)
setClickedElement(findClickedStock)
console.log("1. Object #1: ", findClickedStock)
getStock().then((dataFromStockApi) => {
setStockObject({ ...dataFromStockApi, ...findClickedStock });
});
setOpen(true);
};

setState in nested async function - React Hooks

How can I build a function which gets some data asynchronously then uses that data to get more asynchronous data?
I am using Dexie.js (indexedDB wrapper) to store data about a direct message. One thing I store in the object is the user id which I'm going to be sending messages to. To build a better UI I'm also getting some information about that user such as the profile picture, username, and display name which is stored on a remote rdbms. To build a complete link component in need data from both databases (local indexedDB and remote rdbms).
My solution returns an empty array. It is being computed when logging it in Google Chrome and I do see my data. However because this is not being computed at render time the array is always empty and therefor I can't iterate over it to build a component.
const [conversations, setConversations] = useState<IConversation[]>()
const [receivers, setReceivers] = useState<Profile[]>()
useEffect(() => {
messagesDatabase.conversations.toArray().then(result => {
setConversations(result)
})
}, [])
useEffect(() => {
if (conversations) {
const getReceivers = async () => {
let receivers: Profile[] = []
await conversations.forEach(async (element) => {
const receiver = await getProfileById(element.conversationWith, token)
// the above await is a javascript fetch call to my backend that returns json about the user values I mentioned
receivers.push(receiver)
})
return receivers
}
getReceivers().then(receivers => {
setReceivers(receivers)
})
}
}, [conversations])
/*
The below log logs an array with a length of 0; receivers.length -> 0
but when clicking the log in Chrome I see:
[
0: {
avatarURL: "https://lh3.googleusercontent.com/..."
displayName: "Cool guy"
userId: "1234"
username: "cool_guy"
}
1: ...
]
*/
console.log(receivers)
My plan is to then iterate over this array using map
{
receivers && conversations
? receivers.map((element, index) => {
return <ChatLink
path={conversations[index].path}
lastMessage={conversations[index].last_message}
displayName={element.displayName}
username={element.username}
avatarURL={element.avatarURL}
key={index}
/>
})
: null
}
How can I write this to not return a empty array?
Here's a SO question related to what I'm experiencing here
I believe your issue is related to you second useEffect hook when you attempt to do the following:
const getReceivers = async () => {
let receivers: Profile[] = []
await conversations.forEach(async (element) => {
const receiver = await getProfileById(element.conversationWith, token)
receivers.push(receiver)
})
return receivers
}
getReceivers().then(receivers => {
setReceivers(receivers)
})
}
Unfortunately, this won't work because async/await doesn't work with forEach. You either need to use for...of or Promise.all() to properly iterate through all conversations, call your API, and then set the state once it's all done.
Here's is a solution using Promise.all():
function App() {
const [conversations, setConversations] = useState<IConversation[]>([]);
const [receivers, setReceivers] = useState<Profile[]>([]);
useEffect(() => {
messagesDatabase.conversations.toArray().then(result => {
setConversations(result);
});
}, []);
useEffect(() => {
if (conversations.length === 0) {
return;
}
async function getReceivers() {
const receivers: Profile[] = await Promise.all(
conversations.map(conversation =>
getProfileById(element.conversationWith, token)
)
);
setReceivers(receivers);
}
getReceivers()
}, [conversations]);
// NOTE: You don't have to do the `receivers && conversations`
// check, and since both are arrays, you should check whether
// `receivers.length !== 0` and `conversations.length !== 0`
// if you want to render something conditionally, but since your
// initial `receivers` state is an empty array, you could just
// render that instead and you won't be seeing anything until
// that array is populated with some data after all fetching is
// done, however, for a better UX, you should probably indicate
// that things are loading and show something rather than returning
// an empty array or null
return receivers.map((receiver, idx) => <ChatLink />)
// or, alternatively
return receivers.length !== 0 ? (
receivers.map((receiver, idx) => <ChatLink />)
) : (
<p>Loading...</p>
);
}
Alternatively, using for...of, you could do the following:
function App() {
const [conversations, setConversations] = useState<IConversation[]>([]);
const [receivers, setReceivers] = useState<Profile[]>([]);
useEffect(() => {
messagesDatabase.conversations.toArray().then(result => {
setConversations(result);
});
}, []);
useEffect(() => {
if (conversations.length === 0) {
return;
}
async function getReceivers() {
let receivers: Profile[] = [];
const profiles = conversations.map(conversation =>
getProfileById(conversation.conversationWith, token)
);
for (const profile of profiles) {
const receiver = await profile;
receivers.push(receiver);
}
return receivers;
}
getReceivers().then(receivers => {
setReceivers(receivers);
});
}, [conversations]);
return receivers.map((receiver, idx) => <ChatLink />);
}
i think it is happening because for getReceivers() function is asynchronous. it waits for the response, in that meantime your state renders with empty array.
you can display spinner untill the response received.
like
const[isLoading,setLoading]= useState(true)
useEffect(()=>{
getReceivers().then(()=>{setLoading(false)}).catch(..)
} )
return {isLoading ? <spinner/> : <yourdata/>}
Please set receivers initial value as array
const [receivers, setReceivers] = useState<Profile[]>([])
Also foreach will not wait as you expect use for loop instead of foreach
I am not sure it is solution for your question
but it could help you to solve your error

Recursion and Observable RxJs

I am performing pagination inside and Observable stream.
The pagination is implemented with a cursor and a total count using recursion.
I am able to emit the every page using the following code observer.next(searches);, by the way I would like to use just observable and no promises but I cannot express recursion using RxJs operators.
Any suggestions?
const search = id =>
new Observable(observer => { recursePages(id, observer) })
const recursePages = (id, observer, processed, searchAfter) => {
httpService.post(
"http://service.com/search",
{
size: 50,
...searchAfter ? { search_after: searchAfter } : null,
id,
})
.toPromise() // httpService.post returns an Observable<AxiosResponse>
.then(res => {
const body = res.data;
const searches = body.data.hits.map(search => ({ data: search.data, cursor: search.id }));
observer.next(searches);
const totalProcessed = processed + searches.length;
if (totalProcessed < body.data.total) {
return recursePages(id, observer, totalProcessed, searches[searches.length - 1].cursor);
}
observer.complete();
})
}
// General Observer
incomingMessages.pipe(
flatMap(msg => search(JSON.parse(msg.content.toString()))),
concatAll(),
).subscribe(console.log),
these methods will recursively gather all the pages and emit them in an array. the pages can then be streamed with from as shown:
// break this out to clean up functions
const performSearch = (id, searchAfter?) => {
return httpService.post(
"http://service.com/search",
{
size: 50,
...searchAfter ? { search_after: searchAfter } : null,
id,
});
}
// main recursion
const _search = (id, processed, searchAfter?) => {
return performSearch(id, searchAfter).pipe( // get page
switchMap(res => {
const body = res.data;
const searches = body.data.hits.map(search => ({ data: search.data, cursor: search.id }));
const totalProcessed = processed + searches.length;
if (totalProcessed < body.total) {
// if not done, recurse and get next page
return _search(id, totalProcessed, searches[searches.length - 1].cursor).pipe(
// attach recursed pages
map(nextPages => [searches].concat(nextPages)
);
}
// if we're done just return the page
return of([searches]);
})
)
}
// entry point
// switch into from to emit pages one by one
const search = id => _search(id, 0).pipe(switchMap(pages => from(pages))
if what you really need is all of the pages to emit one by one before they're all fetched, for instance so you can show page 1 as soon as it's available rather than wait on page 2+, then that can be done with some tweaking. let me know.
EDIT: this method will emit one by one
const _search = (id, processed, searchAfter?) => {
return performSearch(id, searchAfter).pipe( // get page
switchMap(res => {
const body = res.data;
const searches = body.data.hits.map(search => ({ data: search.data, cursor: search.id }));
const totalProcessed = processed + searches.length;
if (totalProcessed < body.total) {
// if not done, concat current page with recursive call for next page
return concat(
of(searches),
_search(id, totalProcessed, searches[searches.length - 1].cursor)
);
}
// if we're done just return the page
return of(searches);
})
)
}
const search = id => _search(id, 0)
you end up with an observable structure like:
concat(
post$(page1),
concat(
post$(page2),
concat(
post$(page3),
post$(page4)
)
)
)
and since nested concat() operations reduce to a flattened structure, this structure would reduce to:
concat(post$(page1), post$(page2), post$(page3), post$(page4))
which is what you're after and the requests run sequentially.
it also seems like expand might do the trick as per #NickL 's comment, soemthing like:
search = (id) => {
let totalProcessed = 0;
return performSearch(id).pipe(
expand(res => {
const body = res.data;
const searches = body.data.hits.map(search => ({ data: search.data, cursor: search.id }));
totalProcessed += searches.length;
if (totalProcessed < body.data.total) {
// not done, keep expanding
return performSearch(id, searches[searches.length - 1].cursor);
}
return EMPTY; // break with EMPTY
})
)
}
though I've never used expand before and this is based off some very limited testing of it, but I am pretty certain this works.
both of these methods could use the reduce (or scan) operator to gather results if you ever wanted:
search(id).pipe(reduce((all, page) => all.concat(page), []))
This is my used solution combining the expand and reduce operator
searchUsers(cursor?: string) {
return from(
this.slackService.app.client.users.list({
token: this.configService.get('SLACK_BOT_TOKEN'),
limit: 1,
...(cursor && { cursor }),
}),
);
}
Usage
.......
this.searchUsers()
.pipe(
expand((res) => {
if (!!res.response_metadata.next_cursor) {
return this.searchUsers(res.response_metadata.next_cursor);
}
return EMPTY;
}),
reduce((acc, val) => {
return [...acc, ...val.members];
}, []),
)
.subscribe((users) => {
console.log(JSON.stringify(users));
});
....

Is there a way to use a observable returning function for each element of another observable array?

I get an Observable<Group[]> from my Firebase collection.
In this Group class is an id which I wanna use to retrieve another dataset array from Firebase, which would be messages for each unique group Observable<Message[]>.(each group has its own chat: Message[])
And it want to return an observable which hold an array of a new Type:
return { ...group, messages: Message[] } as GroupWithMessages
the final goal should be Observable<GroupWithMessages[]>
getGroupWithChat(): Observable<GroupWithMessages[]> {
const groupColl = this.getGroups(); // Observable<Group[]>
const messages = groupColl.pipe(
map(groups => {
return groups.map(meet => {
const messages = this.getMessagesFor(group.uid);
return { messages:messages, ...group} as GroupWithMessages
});
})
);
return messages;
}
}
and here the Message function
getMessagesFor(id: string): Observable<Message[]> {
return this.afs.collection<Message>(`meets/${id} /messages`).valueChanges();
}
sadly that doesnt work because when i create the new Obj I cannot bind messages:messages because messages ist vom typ Observable<Message[]>
I hope that cleares things
UPDATE:
my main problem now comes down to this:
getGroupsWithMessages() {
this.getJoinedGroups()
.pipe(
mergeMap(groups =>
from(groups).pipe(
mergeMap(group => {
return this.getMessagesFor(group.uid).pipe(
map(messages => {
return { ...group, messages } as GroupIdMess;
})
);
}),
tap(x => console.log('reaching here: ', x)),
toArray(),
tap(x => console.log('not reaching here = completed: ', x))
)
),
tap(x => console.log('not reaching here: ', x))
)
.subscribe(x => console.log('not reaching here: ', x));
}
when i call that function my console.log is as follows:
Not sure if I follow what you're doing here but the logic look like you'd want:
getGroupWithChat() {
return this.getGroups.pipe(map(groups=> {
return groups.map(group => this.getMessagesFor(group.uid));
})).subscribe(); // trigger "hot" observable
}
Let me know if I can help further after you clarify.
UPDATE:
So it looks like you need to get the UID of the group before making the call to get the GroupMessages[]?
get Group: Observable
call getMessagesFor(Group.uid)
this example gets groups result$ then
concatMap uses groups result$ to make the messages query
this.getGroups().pipe(
concatMap((group: Group) => this.getMessagesFor(group.uid))
).subscribe((messages: GroupWithMessages[]) => {
console.log(messages);
});
You may still want to map them together but it seems like you know how to do that. concatMap waits for the first to finish, then makes the second call which you need.
Is this closer?
Use forkJoin to wait for messages to be received for all groups. Then map the result of forkJoin to an array of GroupWithMessages like this -
getGroupWithChat(): Observable<GroupWithMessages[]> {
return this.getGroups()
.pipe(
switchMap(groups => {
const messagesForAllGroups$ = groups.map(group => this.getMessagesFor(group.uid));
return forkJoin(messagesForAllGroups$)
.pipe(
map(joined => {
//joined has response like -
//[messagesArrayForGroup0, messagesArrayForGroup1, messagesArrayForGroup2....];
const messagesByGroup = Array<GroupWithMessages>();
groups.forEach((group, index) => {
//assuming that GroupWithMessages has group and messages properties.
const gm = new GroupWithMessages();
gm.group = group;
gm.messages = joined[index];
messagesByGroup.push(gm);
});
return messagesByGroup;
})
)
})
)
}
I usually do that by splitting Observable<any[]> to Observable<any> and then mergeMap the results to inner Observable.
Something like this should work:
getMessagesFor(id: string): Observable<number> {
return of(1);
}
getGroups(): Observable<string[]> {
return of(["1", "2"]);
}
getGroupWithChat() {
this.getGroups().pipe(
mergeMap(groups => from(groups)), // Split the stream into individual group elements instead of an array
mergeMap(group => {
return this.getMessagesFor(group).pipe(
map(messages => {
return Object.assign(group, messages);
})
);
})
);
}
Edit:
Consider BehaviorSubject. It doesn't complete at all:
const behSub: BehaviorSubject<number[]> = new BehaviorSubject([1, 2, 3]);
setTimeout(() => {
behSub.next([4, 5, 6]);
}, 5000);
behSub
.pipe(
mergeMap(arr =>
from(arr).pipe(
tap(), // Do something with individual items, like mergeMap to messages
toArray() // Go back to array
)
)
)
.subscribe(console.log, null, () => {
console.log('Complete');
});

How to reset a RXJS scan operator based on another Observable

I have a component which triggers an onScrollEnd event when the last item in a virtual list is rendered. This event will do a new API request to fetch the next page and merge them with the previous results using the scan operator.
This component also has a search field which triggers an onSearch event.
How do I clear the previous accumulated results from the scan operator when a search event is triggered? Or do I need to refactor my logic here?
const loading$ = new BehaviorSubject(false);
const offset$ = new BehaviorSubject(0);
const search$ = new BehaviorSubject(null);
const options$: Observable<any[]> = merge(offset$, search$).pipe(
// 1. Start the loading indicator.
tap(() => loading$.next(true)),
// 2. Fetch new items based on the offset.
switchMap(([offset, searchterm]) => userService.getUsers(offset, searchterm)),
// 3. Stop the loading indicator.
tap(() => loading$.next(false)),
// 4. Complete the Observable when there is no 'next' link.
takeWhile((response) => response.links.next),
// 5. Map the response.
map(({ data }) =>
data.map((user) => ({
label: user.name,
value: user.id
}))
),
// 6. Accumulate the new options with the previous options.
scan((acc, curr) => {
// TODO: Dont merge on search$.next
return [...acc, ...curr]);
}
);
// Fetch next page
onScrollEnd: (offset: number) => offset$.next(offset);
// Fetch search results
onSearch: (term) => {
search$.next(term);
};
To manipulate the state of a scan you can write higher order functions that get the old state and the new update. Combine then with the merge operator. This way you stick to a clean stream-oriented solution without any side-effects.
const { Subject, merge } = rxjs;
const { scan, map } = rxjs.operators;
add$ = new Subject();
clear$ = new Subject();
add = (value) => (state) => [...state, value];
clear = () => (state) => [];
const result$ = merge(
add$.pipe(map(add)),
clear$.pipe(map(clear))
).pipe(
scan((state, innerFn) => innerFn(state), [])
)
result$.subscribe(result => console.log(...result))
add$.next(1)
add$.next(2)
clear$.next()
add$.next(3)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>
This method can easily be extended and/or adapted for other state usecases in rxjs.
Example (remove last item)
removeLast$ = new Subject()
removeLast = () => (state) => state.slice(0, -1);
merge(
..
removeLast$.pipe(map(removeLast)),
..
)
I think you could achieve what you want just by restructuring your chain (I'm omitting tap calls that trigger loading for simplicity):
search$.pipe(
switchMap(searchterm =>
concat(
userService.getUsers(0, searchterm),
offset$.pipe(concatMap(offset => userService.getUsers(offset, searchterm)))),
).pipe(
map(({ data }) => data.map((user) => ({
label: user.name,
value: user.id
}))),
scan((acc, curr) => [...acc, ...curr], []),
),
),
);
Every emission from search$ will create a new inner Observable with its own scan that will start with an empty accumulator.
Found a working solution: I check the current offset by using withLatestFrom before the scan operator and reset the accumulator if needed based on this value.
Stackblitz demo
This is an interesting stream. Thinking about it, offset$ and search$ are really 2 separate streams, though, with different logic, and so should be merged at the very end and not the beginning.
Also, it seems to me that searching should reset the offset to 0, and I don't see that in the current logic.
So here's my idea:
const offsettedOptions$ = offset$.pipe(
tap(() => loading$.next(true)),
withLatestFrom(search$),
concatMap(([offset, searchterm]) => userService.getUsers(offset, searchterm)),
tap(() => loading$.next(false)),
map(({ data }) =>
data.map((user) => ({
label: user.name,
value: user.id
})),
scan((acc, curr) => [...acc, ...curr])
);
const searchedOptions$ = search$.pipe(
tap(() => loading$.next(true)),
concatMap(searchTerm => userService.getUsers(0, searchterm)),
tap(() => loading$.next(false)),
map(({ data }) =>
data.map((user) => ({
label: user.name,
value: user.id
})),
);
const options$ = merge(offsettedOptions, searchedOptions);
See if that works or would make sense. I may be missing some context.
I know its old, but I just needed to do the same thing and have another solution to throw in to the mix.
There are really just 2 actions the user can trigger
const search$ = new Subject<string>();
const offset$ = new Subject<number>();
We don't really care about offset$ until search$ emits, and at that point, we want it to be 0 to start over. I would write it like this:
const results$ = search$.pipe( // Search emits
switchMap((searchTerm) => {
return offset$.pipe( // Start watching offset
startWith(0), // We want a value right away, so set it to 0
switchMap((offset) => {
return userService.getUsers(offset, searchTerm)) // get the stuff
})
)
}))
At this point we are resetting the offset every time search$ emits, and any time offset$ emits we make a fresh api call fetching the desired resources. We need the collection to reset if search$ emits, so I believe the right place is inside switchMap wrapping the offset$ pipe.
const results$ = search$.pipe( // Search emits
switchMap((searchTerm) => {
return offset$.pipe( // Start watching offset
startWith(0), // We want a value right away, so set it to 0
switchMap((offset) => {
return userService.getUsers(offset, searchTerm)) // get the stuff
}),
takeWhile((response) => response.links.next), // stop when we know there are no more.
// Turn the data in to a useful shape
map(({ data }) =>
data.map((user) => ({
label: user.name,
value: user.id
}))
),
// Append the new data with the existing list
scan((list, response) => {
return [ // merge
...list,
...response
]
}, [])
)
}))
This great part here is that the scan is reset on every new search$ emission.
The final bit here, I would move loading$ out of tap, and declare it separately. Final code should look something like this
const search$ = new Subject<string>();
const offset$ = new Subject<number>();
let results$: Observable<{label: string; value: string}[]>;
results$ = search$.pipe( // Search emits
switchMap((searchTerm) => {
return offset$.pipe( // Start watching offset
startWith(0), // We want a value right away, so set it to 0
switchMap((offset) => {
return userService.getUsers(offset, searchTerm)) // get the stuff
}),
takeWhile((response) => response.links.next), // stop when we know there are no more.
// Turn the data in to a useful shape
map(({ data }) =>
data.map((user) => ({
label: user.name,
value: user.id
}))
),
// Append the new data with the existing list
scan((list, response) => {
return [ // merge
...list,
...response
]
}, [])
)
}));
const loading$ = merge(
search$.pipe(mapTo(true)), // set to true whenever search emits
offset$.pipe(mapTo(true)), // set to true when offset emits
results$.pipe(mapTo(false)), // set to false when we get new results
);
results$.subscribe((results) => {
console.log(results);
})

Categories