Reusable actions in mobx/mobx-state-tree - javascript

I have multiple mobx stores and find myself having actions in each of them that are pretty much identical. I'm therefor hoping to be able to generalise and reuse them between stores. Below I have tried to break out the create action hoping to be able to import it to multiple stores but it doesn't work as self is not available.
I want to go from this:
export const CategoriesStore = types
.model("CategoriesStore", {
})
.views(self => ({
}))
.actions(self => {
const collection = "categories"
const create = flow(function* create(newItem) {
const newItemRef = firestore.collection(collection).doc()
const id = newItemRef.id
self[collection].set(id, newItem)
yield newItemRef.set(newItem)
return id
})
return {
create
}
})
To something like this, where the create action could be reused in other stores:
const create = flow(function* create(newItem, collection) {
const newItemRef = firestore.collection(collection).doc()
const id = newItemRef.id
this[collection].set(id, newItem)
yield newItemRef.set(newItem)
return id
})
export const CategoriesStore = types
.model("CategoriesStore", {
})
.views(self => ({
}))
.actions(self => {
const collection = "categories"
const _create = create.bind(self)
return {
_create
}
})
Any ideas for how to achieve this?

While I've never done anything like that, but I was thinking to and had an impression that it should work. But if it doesn't, you can do something like:
const create = (self) => flow(function* create(newItem, collection) {
const newItemRef = firestore.collection(collection).doc()
const id = newItemRef.id
self[collection].set(id, newItem)
yield newItemRef.set(newItem)
return id
})
export const CategoriesStore = types
.model("CategoriesStore", {
})
.views(self => ({
}))
.actions(self => {
const collection = "categories"
return {
create: create(self)
}
})
This should definitely work.

Related

React-query setQueryData not re-rendering component

I've been dealing for a while with this problem and still can't tackle it.
I'm using React-query as a server state management library and I'm trying to get my UI state synchronized with my server state when a mutations occurs. Since I can use the mutation response to avoid a new API call, I'm using the setQueryData feature that React-query gives us.
The problem is that the old-data is being correctly modified (I can see it in the react-query DevTools) when a mutation is successful, but the component using it isn't being re-rendered, making my UI State not synchronized with my Server state (well, at least the user can't see the update).
Let me show some code and hope someone can give me some insights.
Component using the query:
const Detail = ({ orderId }) => {
const { workGroups } = useWorkGroups();
const navigate = useNavigate();
const queryClient = useQueryClient();
const orderQueries = queryClient.getQueryData(["orders"]);
const queryOrder = orderQueries?.find((ord) => ord.id === orderId);
// more code
Component mutating the query:
const Deliver = ({
setIsModalOpened,
artisan,
index,
queryQuantity,
queryOrder,
}) => {
const [quantity, setQuantity] = useState(() => queryQuantity);
const { mutate: confirmOrderDelivered } = useMutateOrderDeliveredByArtisan(
queryOrder.id
);
const onSubmit = () => {
confirmOrderDelivered(
{
id: queryOrder.artisan_production_orders[index].id,
artisan: artisan.user,
items: [
{
quantity_delivered: quantity,
},
],
},
{
onSuccess: setIsModalOpened(false),
}
);
};
// more code
Now the mutation function (ik it's a lot of logic but I dont' want to refetch the data using invalidateQueries since we're dealing with users with a really bad internet connection). Ofc you don't need to understand each step of the fn but what it basically does is update the old queried data. In the beginning I thought it was a mutation reference problem since React using a strict comparison under the hood but I also checked it and It doesn't look like it's the problem. :
{
onSuccess: (data) => {
queryClient.setQueryData(["orders"], (oldQueryData) => {
let oldQueryDataCopy = [...oldQueryData];
const index = oldQueryDataCopy.findIndex(
(oldData) => oldData.id === orderId
);
let artisanProdOrders =
oldQueryDataCopy[index].artisan_production_orders;
let artisanProductionOrderIdx = artisanProdOrders.findIndex(
(artProdOrd) => artProdOrd.id === data.id
);
artisanProdOrders[artisanProductionOrderIdx] = {
...artisanProdOrders[artisanProductionOrderIdx],
items: data.items,
};
const totalDelivered = artisanProdOrders.reduce((acc, el) => {
const delivered = el.items[0].quantity_delivered;
return acc + delivered;
}, 0);
oldQueryDataCopy[index] = {
...oldQueryDataCopy[index],
artisan_production_orders: artisanProdOrders,
items: [
{
...oldQueryDataCopy[index].items[0],
quantity_delivered: totalDelivered,
},
],
};
return oldQueryDataCopy;
});
},
onError: (err) => {
throw new Error(err);
},
}
And last but not least: I already checked that the oldQueryData is being correctly modified (console loging in the onSuccess fn in the mutation response) and, as I said before, the data is correctly modified in the React-query DevTools.
I know this is a lot of code and the problem seems to be complex but I really believe that it might be a really easy thing that I'm not pointing out because of how tired I already am.
Thanks!
Well, I fixed it in the worst possible way imho, so I will answer this question but I really would like to read your thoughts.
It looks like the new query data setted on the expected query is re-rendering the component only if the mutation function is located in the component that we actually want to re-render.
With that in mind what I did was just colocate my mutation function in the parent component and pass it down through the child component.
Something like this:
const Detail = ({ orderId }) => {
const { workGroups } = useWorkGroups();
const navigate = useNavigate();
const { mutate: confirmOrderDelivered } = useMutateOrderDeliveredByArtisan(
queryOrder.id
); ==============> THE MUTATION FN IS NOW IN THE PARENT COMPONENT
const queryClient = useQueryClient();
const orderQueries = queryClient.getQueryData(["orders"]);
const queryOrder = orderQueries?.find((ord) => ord.id === orderId);
// more code
First child:
const Assigned = ({ artisan, queryOrder, index, confirmOrderDelivered }) => {
// THE IMPORTANT PART HERE IS THE PROP BEING PASSED DOWN.
<Modal
isOpen={isModalOpened}
toggleModal={setIsModalOpened}
// className="w312"
width="80%"
height="fit-content"
justifyCont="unset"
alignItems="unset"
>
<Deliver
setIsModalOpened={setIsModalOpened}
artisan={artisan}
queryQuantity={quantity}
queryOrder={queryOrder}
index={index}
confirmOrderDelivered={confirmOrderDelivered} => HERE
/>
</Modal>
Component that actually needs the mutation fn:
const Deliver = ({
setIsModalOpened,
artisan,
index,
queryQuantity,
queryOrder,
confirmOrderDelivered,
}) => {
const [quantity, setQuantity] = useState(() => queryQuantity);
const onSubmit = () => {
confirmOrderDelivered( => HERE.
{
id: queryOrder.artisan_production_orders[index].id,
artisan: artisan.user,
items: [
{
quantity_delivered: quantity,
},
],
}
);
};
You can't mutate any prop.
You always need to create new versions of the objects and props and use destructuring.
Example
queryClient.setQueryData([QUERY_KEYS.MYKEY], (old) => {
const myArray = [...old.myArray];
return {
...old,
// WRONG
myArray[0].name: 'text1',
// CORRECT
myArray[0]: {
...myArray[0],
name: 'text1'
}
}})

Variable.map is not a function React Javascript

I am working on app and I am trying to extract three attributes of this string (I thought it was an array of objects is filteredResultsJSON, but it is a string) :
Well, when I want to get the news of a given section I am using this code:
export default function getNewsFilteredBySection (sectionSearched="") {
const URL= `https://api.nytimes.com/svc/mostpopular/v2/viewed/7.json?api-key=${API_KEY}`;
return fetch(URL)
.then(res=>res.json())
.then(response=> {
const { data = [] } = response;
if(Array.isArray(data)){
const {results} = response;
const filteredResults = results.filter(obj => {
return obj.section === sectionSearched;
});
const filteredResultsJSON = JSON.stringify(filteredResults);
const name = typeof(filteredResultsJSON);
console.log(name);
return filteredResultsJSON;
}
})
};
I did JSON.stringify(filteredResults) because I saw it when doing console.log()as an [Object Object] and I wanted to be able to read the information and to loop through it to be able to extract it.
So I am using this code to extract the attributes I want for each news:
export function getNewsAttributes (filteredResultsJSON=[]) {
const newsAttributes = filteredResultsJSON.map(filteredResultJSON=>{
const { title, abstract, url } = filteredResultJSON;
console.log(title, abstract, url);
return {title, abstract, url};
});
return newsAttributes;
};
But it returns the following error:
I have been researching and I totally think the matter is about JSON.Stringify(filteredResults). How could I maintain that variable as an object, being able to read it and extract the information from it?
I am going to use it like this, after an onClick event:
function showEvent (e) {
const sectionSearched = e.target.innerText;
const filteredResultsJSON = getNewsFilteredBySection(sectionSearched);
const newsAttributes = getNewsAttributes(filteredResultsJSON);
}
Thanks a lot :)
I solved it with this code:
export default function getNewsFilteredBySection (sectionSearched="") {
const URL= `https://api.nytimes.com/svc/mostpopular/v2/viewed/7.json?api-key=${API_KEY}`;
return fetch(URL)
.then(res=>res.json())
.then(response=> {
const { data = [] } = response;
if(Array.isArray(data)){
const {results} = response;
const filteredResults = results.filter(obj => {
return obj.section === sectionSearched;
});
console.log(filteredResults);
return filteredResults;
}
})
};
Finally I didnt need to do JSON.Stringify(filteredResults). However I am having this two errors and I dont know why, my app works:
Any idea? Thanks a lot :)
You need to add async
export default async function getNewsFilteredBySection (sectionSearched=""){ ...
showEvent = async (e) => { ...
and await
const filteredResultsJSON = await getNewsFilteredBySection(sectionSearched);

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));
});
....

Svelte derived stores and array sort

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.

Create an Observable object from a list of Observables

I'm still wrapping my head around RxJS and there is this pattern I keep running into and that I would like to find a more elegant way to write.
Implementing the model part of a Model-View-Intent pattern component, I have a function that takes actions as input an returns a single state$ Observable as output.
function model(actions) {
const firstProperty$ =
const anotherProperty$ = …
// Better way to write this?
const state$ = Rx.Observable.combineLatest(
firstProperty$, anotherProperty$,
(firstProperty, anotherProperty) => ({
firstProperty, anotherProperty
})
);
return state$;
}
So my model method computes a bunch of observables, every one of them emit items that represents a part of the state of my application. That is fine.
But how to I cleanly combine them into a single one observable that emits states, each state being a single object whose keys are the initial observable names?
I borrowed this pattern from https://github.com/cyclejs/todomvc-cycle :
function model(initialState$, actions){
const mod$ = modifications(actions)
return initialState$
.concat(mod$)
.scan( (state, mod) => mod(state))
.share()
}
function modifications(actions){
const firstMod$ = actions.anAction$.map(anAction => (
state => ({ ...state,
firstProperty: anAction.something
})
const secondMod$ = actions.otherAction$.map(otherAction => (
state => ({ ...state,
firstProperty: otherAction.something,
secondProperty: aComputation(otherAction)
})
return Rx.Observable.merge([firstMod$, secondMod$ ]).share()
}
In the main function :
const initialState$ = Rx.Observable.from({})
const actions = intent(DOM)
const state$ = model(initialState$, actions).share()
Using help from CHadrien, here is a working solution.
const prop1$ = Rx.Observable.of('foo');
const prop2$ = Rx.Observable.of('bar');
const prop3$ = Rx.Observable.of('baz');
const prop4$ = Rx.Observable.of('foobar');
function combineObservables(objectOfObservables) {
const keys = Object.keys(objectOfObservables);
const observables = keys.map(key => objectOfObservables[key]);
const combined$ = Rx.Observable.combineLatest(
observables, (...values) => {
var obj = {};
for (let i = 0 ; i < keys.length ; i++) {
obj[keys[i]] = values[i];
}
return obj;
}
);
return combined$;
}
combineObservables({prop1$, prop2$, prop3$, prop4$}).subscribe(x => console.log(x));
And the result:
[object Object] {
prop1$: "foo",
prop2$: "bar",
prop3$: "baz",
prop4$: "foobar"
}

Categories