Send event to array of child services in XState - javascript

I have a scenario where I have one parent machine and several child machines that can be spawned from the parent machine.
The current setup looks like this:
const parentMachine = Machine({
context: {
children: [] //can contain any number of child services
},
...
on: {
ADD_CHILD: {
actions: assign({
children: (ctx, e) => {
return [
...ctx.children,
{
ref: spawn(childMachine)
},
];
},
}),
},
UPDATE_CHILDREN: {
actions: ??? //need to somehow loop through children and send the UPDATE event to each service
}
}
});
When the parent machine receives the "UPDATE_CHILDREN" event, I want to update each of the child services. I know you can send batch events by passing an array to send, but I want each event to also be sent to a different service. I've only seen examples where they are sent to a single service at a time. I've tried several things, including the following:
UPDATE_CHILDREN: {
actions: ctx => ctx.children.forEach(c => send("UPDATE", { to: () => c.ref }) //doesn't send
}
Am I missing something obvious? Is this possible?

Ah, I bumped into exactly the same issue as you!
It turns out that if you give actions a function, it assumes the function to be the actual action, not a function that returns actions.
If you want to generate your actions based on context, you need to use a pure action:
import { actions } from 'xstate';
const { pure } = actions;
...
actions: pure((context, event) =>
context.myActors.map((myActor) =>
send('SOME_EVENT', { to: myActor })
)
),
This is a tricky mistake to fall into as you get no feedback that you're doing something wrong..

Had a realization about how this is supposed to work in XState.
The references to the children are already being stored, so we can just basically send events to them directly without using the "to" property:
actions: ctx => ctx.children.forEach(c => c.ref.send("UPDATE"))

Related

How to specify function to use using an object in JS

I am trying to build a function that allows users to retrieve data from the database, but in a dynamic way. So allowing them to specify the source table, as well as which columns they would like to retrieve.
I have already built out the part that allows them to choose the table and columns, and these users only have access to data we are happy for them to retrieve.
I want this to be as dynamic as possible, so I am building a function to help me with this. I have run into one problem though so far. I have this function:
const modelMap = (model, action, criteria, options) => {
const models = {
EXTERNAL_USER: {
READ: services.externalUser.readExternalUser(criteria, options),
},
TASK: {
READ: services.task.readTask(criteria, options),
},
USER: {
READ: services.user.readUser(criteria, options),
},
}
return models[model][action]
}
So, for example, I call this function using
modelMap('EXTERNAL_USER', 'READ', { id: userID }, { populates: ['documents'] }).
This returns the data I want, however I get an ``OperationError` in my terminal:
OperationalError [UsageError]: Invalid populate(s).
Details:
Could not populate `documents`. There is no attribute named `documents` defined in this model.
The error is saying that the Task model has no attribute documents, which it doesn't. So I am guessing that even though I am not trying to access the readTask function, it is being called anyway. How can I get around this?
SOLUTION
I altered the models object to the following:
const models = {
EXTERNAL_USER: {
READ: () => services.externalUser.readExternalUsers(criteria, options),
},
TASK: {
READ: () => services.task.readTask(criteria, options),
},
USER: {
READ: () => services.user.readUser(criteria, options),
},
}
And I can then use await modelMap(model, action, criteria, options)() to get the data I need.
Yes. The functions called anyway

Reference to javascript object apparently returning different values in different places with no modifications in between

I'm using a variable twice within a function but it returns different values even though I'm making no modifications to it.
This is happening within a form component developed with Vue.js (v2) which dispatches a Vuex action. I think this has nothing to do with Vue/Vuex per se, but it's important to understand part of the code.
Here is the relevant piece of code from my component
import { mapActions } from 'vuex'
export default {
data() {
return {
product: {
code: '',
description: '',
type: '',
productImage: [],
productDocs: {},
}
}
},
methods: {
...mapActions(['event']),
save() {
console.log("this.product:", this.product)
const valid = this.$refs.form.validate() // this validates the form
console.log("this.product:", this.product)
if (valid) {
try {
this.event({
action: 'product/addProduct',
data: this.product
})
}
finally {
this.close()
}
}
},
// other stuff
and a small piece of code for the vuex action "event"
event: async ({ dispatch }, event) => {
const time = new Date()
const evid = `${Date.now()}|${Math.floor(Math.random()*1000)}`
console.log(`Preparing to dispatch... Action: ${event.action} | data: ${JSON.stringify(event.data)} | Event ID: ${evid}`)
// enriching event
event.evid = evid;
event.timestamp = time;
event.synced = 0
// Push user event to buffer
try {
await buffer.events.add(event)
} catch (e) {
console.log(`Error writing event into buffer. Action ${event.action} | evid: ${evid} `)
}
// dispatch action
try {
await dispatch(event.action, event)
}
catch (err) {
console.log(`Error dispatching action: ${event.action} | data: ${event.data}\n${err.stack || err}`)
window.alert('Could not save. Try again. \n' + err + `\n Action: ${event.action} | data: ${event.data}`)
}
},
The problem is with this.product. I've placed the several console.log to check out the actual values because it wasn't working as expected. The logs from the save() functions return undefined, but within the event function (a vuex action) the values are as expected, as shown in the console logs:
When I log this.product in the save() function. Both logs are the same.
When I log the event in the vuex action, it shows that event.data is actually the product.
I must be doing something terribly wrong here, but I'm totally blind to it. Any help is appreciated.
#Sumurai8: thanks for editing the question and for the hint.
Part of this may be because of that tiny i next to the opened product.
If you hover over it, it says that "the object has been evaluated just
now", which means it evaluates what is in the object when you open the
object, which is way after executing the action. [...] Whatever is
changing the product may very well happen after the event somewhere.
It actually helped me find the solution.
Basically within the this.close function called in the finally statement of the save() function, I was resetting the form and thus this.product, which was used solely to hold the form data. So at evaluation time, the object had undefined properties, while the event function managed to output to the console before the reset. However at the end the store would not get updated as expected (that's how I noticed the issue), because the event function and the action called within it are asynchronous and so the value got reset before the actual mutation of the vuex store.
Logging JSON.stringify(this.product) outputted the right value even within the save() method. I used that to create a more robust copy of the data and passed that to the event function as follows:
this.event({
action: 'product/addProduct',
data: JSON.parse(JSON.stringify(this.product))
})
Now everything works like a charme.

React async function is not a function

In one of my components I have the following functions:
addNewIndicator(attrs = {}) {
const value = attrs.value || 'Indicator'
const type = attrs.type || 'Generic Type'
this.createIndicator(value).then(
console.log('Indicator Created.')
)
}
async createIndicator(value) {
await this.props.createIndicatorMutation({
variables: {
value
},
update: (store, { data: { indicator }} ) => {
const data = store.readQuery({ query: INDICATOR_FEED_QUERY })
data.indicatorFeed.splice(0, 0, indicator)
store.writeQuery({
query: INDICATOR_FEED_QUERY,
data,
})
}
})
}
addNewIndicator() is triggered on a button click. When it runs, I get the following error:
TypeError: this.createIndicator is not a function
It is pointing to this line:
this.createIndicator(value).then(
I've done quite a bit of Googling, but haven't been able to figure out why this is the case. My understanding is that async functions can be called like that, but perhaps I'm missing something. Sorry if this is a silly question, I'm still learning React!
Also, I created the project using create-react-app and haven't modified it much other than adding some packages. Thanks for any help!
Edit to add how it is called. It is called from a child component props:
<Button primary onClick={this.handleAddSelectionClick}>Add Selected As Indicator</Button>
and handleAddSelectionClick:
handleAddSelectionClick = () => {
...snip...
this.props.addNewIndicator({
value: new_indicator_str,
})
}
try to validate that your 2 methods using the same context (this) -
you might need to do something like that in the constructor:
this.addNewIndicator = this.addNewIndicator.bind(this);
this.createIndicator = this.createIndicator.bind(this);
You probably just forgot to bind “this”. As you passed addNewIndicator as a callback, it lost its context. In this article several methods of binding callbacks are described, with all pros and cons of each.
https://reactjs.org/docs/handling-events.html

Firebase React Binding

I'm somewhat new to React, and using the re-base library to work with Firebase.
I'm currently trying to render a table, but because of the way my data is structured in firebase, I need to get a list of keys from two locations- the first one being a list of user keys that are a member of a team, and the second being the full user information.
The team node is structured like this: /teams/team_id/userkeys, and the user info is stored like this: /Users/userkey/{email, name, etc.}
My table consists of two react components: a table component and a row component.
My table component has props teamid passed to it, and I'm using re-base's bindToState functionality to get the associated user keys in componentWillMount(). Then, I use bindToState again to get the full user node, like so:
componentWillMount() {
this.ref = base.bindToState(`/teams/${this.props.data}/members`, {
context: this,
state: 'members',
asArray: true,
then() {
this.secondref = base.bindToState('/Users', {
context: this,
state: 'users',
asArray: true,
then() {
let membersKeys = this.state.members.map(function(item) {
return item.key;
});
let usersKeys = this.state.members.map(function(item) {
return item.key;
});
let onlyCorrectMembersKeys = intersection(membersKeys, usersKeys);
this.setState({
loading: false
});
}
});
}
});
}
As you can see, I create membersKeys and usersKeys and then use underscore.js's intersection function to get all the member keys that are in my users node (note: I do this because there are some cases where a user will be a member of a team, but not be under /Users).
The part I'm struggling with is adding an additional rebase call to create the full members array (ie. the user data from /Users for the keys in onlyCorrectMembersKeys.
Edit: I've tried
let allKeys = [];
onlyCorrectMembersKeys.forEach(function(element) {
base.fetch(`/Users/${element}`, {
asArray: true,
then(data) {
allKeys.prototype.concat(data);
}
});
});
But I'm receiving the error Error: REBASE: The options argument must contain a context property of type object. Instead, got undefined
I'm assuming that's because onlyCorrectMembersKeys hasn't been fully computed yet, but I'm struggling with how to figure out the best way to solve this..
For anyone dealing with this issue as well, I seemed to have found (somewhat) of a solution:
onlyCorrectMembersKeys.map(function(item) {
base.fetch(`/Users/${item}`, {
context: this,
asObject: true,
then(data) {
if (data) {
allKeyss.push({item,data});
this.setState({allKeys: allKeyss});
}
this.setState({loading: false});
},
onFailure(err) {
console.log(err);
this.setState({loading: false});
}
})
}, this);
}
This works fine, but when users and members state is updated, it doesn't update the allkeys state. I'm sure this is just due to my level of react knowledge, so when I figure that out I'll post the solution.
Edit: using listenTo instead of bindToState is the correct approach as bindToState's callback is only fired once.

Angular 2 Array Subject .next() not invoking another component's observer

I'm using firebase as my backend.
Inside of a data.service.ts, I create a Subject array which will be filled by the firebase observer on app init:
private orders = new Subject<any>();
orders$ = this.orders.asObservable();
firebase.database().ref(this.fbDataPath).on('child_added', (childSnapshot) => {
this.orders.next(
{
key: childSnapshot.key,
name: childSnapshot.val().name,
items: childSnapshot.val().items
}
)
})
I then provide a separate directory component with DataService and subscribe to its orders observable:
DataService.orders$.subscribe(
order => {
console.log('subscribe hit')
})
I can't seem to get the listener component to trigger on a next. I made this work for a boolean isLoggedIn, and I must be missing something in this scenario. Thanks!
It might be because you're using this in a closure. Remove the this from this.orders.next()

Categories