What we need to do: We need to feature flag a few things in our current state machine
My ideal solution: Always load, no matter what state is, all feature flags and assign them to the state machine context
Attempts: Tried using async actions and invoke services, however, I cannot find a way to always run either of them
This basically my state machine and how I envisioned loading feature flag. However, the invoke.src function just gets called for the first time when I'm first loading the state machine.
Every time that I hydrate the state machine and the state machine is in one of the state, for example create, the invoke.src function does not get called therefore no FF is loaded into the context
const stateMachine = createStateMachine({
id: 'state-machine',
invoke: {
src: async () => {
return await featureFlagService.load();
},
onDone: {
actions: assign(_context, event) => ({ featureFlagEvaluations: event.data }),
}
},
states: {
'create': { ... },
'retrieve': { ... },
}
});
Does anyone have any idea of how to implement such use case?
You should use the actor model approach. Each time when you need to refresh/fetch the FF you should spawn FF-machine and on done call parentSend() message which will update the context to your main SM(state-machine)
const stateMachine = createStateMachine({
id: 'state-machine',
invoke: {
src: async () => {
return await featureFlagService.load();
},
onDone: [{
actions: assign({
ffActorRef: () => spawn(featureFlagMachine, 'ffActor'),
}),
}],
states: {
'create': { ... },
'retrieve': { ... },
},
on:{
REFRESH_FEATURE_FLAG : [{
actions: assign(_context, event) => ({ featureFlagEvaluations: event.data }),
}]
}
});
const featureFlagMachine = createStateMachine({
id: 'ff-machine',
initial: 'retrieve',
invoke: {
src: async () => {
return await featureFlagService.load();
},
onDone: [{
actions: ['notifyRefresh']
}],
states: {
'create': { ... },
'retrieve': { ... },
},
},
{
actions: {
notifyRefresh: sendParent((ctx, event) => {
return {
type: 'REFRESH_FEATURE_FLAG',
data: { featureFlagEvaluations: event.data },
};
}),
},
}
}
);
Related
I'm playing around learning XState and wanted to include an action in a machine that would just log the current state to console.
Defining a simple example machine like so, how would I go about this? Also note the questions in the comments in the code.
import { createMachine, interpret } from "xstate"
const sm = createMachine({
initial: 'foo',
states: {
foo: {
entry: 'logState', // Can I only reference an action by string?
// Or can I add arguments here somehow?
on: {
TOGGLE: {target: 'bar'}
}
},
bar: {
entry: 'logState',
on: {
TOGGLE: {target: 'foo'}
}
}
}
},
{
actions: {
logState(/* What arguments can go here? */) => {
// What do I do here?
}
}
});
I know that actions are called with context and event as arguments but I don't see a way to get the current state from either of those. Am I missing something here?
For a simple use case like yours, you could try recording the state on transition.
let currentState;
const service = interpret(machine).onTransition(state => {
if (state.value != currentState) {
// TODO: terminate timer if any and start a new one
currentState = state.value;
}
});
Then use the value in your actions.
See more here: https://github.com/statelyai/xstate/discussions/1294
Actions receive three arguments - context, event and meta. meta have property state, which is current state.
import { createMachine } from "xstate";
let metaDemo = createMachine(
{
id: "meta-demo",
initial: "ping",
states: {
ping: {
entry: ["logStateValues"],
after: { TIMEOUT: "pong" },
},
pong: {
entry: ["logStateValues"],
after: { TIMEOUT: "ping" },
},
},
},
{
delays: {
TIMEOUT: 3000,
},
actions: {
logStateValues(ctx, event, meta) {
if (meta.state.matches("ping")) {
console.log("It's PING!");
} else if (meta.state.matches("pong")) {
console.log("And now it's PONG");
} else {
console.log(
`This is not supposed to happen. State is: ${meta.state
.toStrings()
.join(".")}`
);
}
},
},
}
);
I just need some help identifying what I am missing here. Just can't seem to send the correct data through:
Parent with the CommunicationPreference component:
<CommunicationPreference
v-for="(communication, index) in communicationPreference"
:key="index"
:consent="communication.consent"
:name="communication.name"
#update="updateConsent(consent)"
/>
METHOD
methods: {
async updateConsent(consent) {
await this.$store.dispatch('account/updateCommunicationPreferences', { consent })
},
},
CommunicationPrefernce.vue
<Button
class="mr-4"
:text="YES"
:type="consent === true ? 'primary' : 'secondary'"
#clicked="updateConsent(true)"
/>
<Button
:text="NO"
:type="consent !== true ? 'primary' : 'secondary'"
#clicked="updateConsent(false)"
/>
PROPS:
props: {
type: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
consent: {
type: Boolean,
default: true,
},
},
METHOD:
updateConsent(consent) {
this.$emit('update', consent)
},
STORE:
async updateCommunicationPreferences({ commit, state }, payload) {
const { consent } = payload
const { communicationTypeName } = state.communicationTypeName
try {
const response = await this.$axios.put(`/communication-consent/${communicationTypeName}`, consent)
const { data: updatedCommunicationPreferences } = response.data
commit('SET_UPDATED_COMMUNICATION_PREFERENCES', updatedCommunicationPreferences)
} catch (error) {
commit('ADD_ERROR', { id: 'updateCommunicationPreferences', error }, { root: true })
}
},
Attached is the UI I am working towards for reference. the idea is each time the user selects either YES or NO the selection is updated and reflected on the UI
Here is my Swagger doc:
I assume that you have a mapped getter for communicationPreference prop, so that this is correct.
I also assume that your #clicked event prop is proper provided the implementation of Button.vue.
So try to change #update="updateConsent(consent)" to #update="updateConsent"
Right now it seems to me that you are making a small mistake between a function call and declaration. Having it such as #update="updateConsent" will trigger updateConsent method, and the function declaration:
async updateConsent(consent) {
await this.$store.dispatch('account/updateCommunicationPreferences', { consent })
},
will take care of getting the consent you pass in your event trigger.
I use a vuex store to handle a session and loading default sessions for different pages onCreated hook. Data is changing and most of the time it works fine but now I noticed even though I change the data, the setter / mutation is not called.
session.js (mixin to load default session from main component)
export default {
data () {
return {
defaultSession: {}
}
},
created () {
this.$store.dispatch('session/update', { data: this.defaultSession })
},
computed: {
sessionData: {
get () {
console.log('GETTER')
return this.$store.getters['session/sessionData']
},
set (session) {
console.log('SETTER')
this.$store.dispatch('session/update', { data: session })
}
}
}
}
store
export default {
namespaced: true,
state: {
session: {},
},
getters: {
session (state) {
return state.session
},
sessionData (state) {
return state.session.data
},
},
mutations: {
UPDATE (state, session) {
state.session = session
console.log('[Session] Updated')
}
},
actions: {
update: ({ commit }, data) => {
commit('UPDATE', data)
}
}
defaultSession example (in component)
defaultSession: {
name: '',
viewId: '',
defaultImage: null,
dataTable: [{
title: '',
data: [],
selected: [],
}],
imageTableData: [],
selectedIcons: []
},
So in my component I access <input v-model="sessionData.name"/> for instance, but neither the setter, nor the mutation is called. (The object is quite nested, because I also store table data inside.)
UPDATE (possible workaround):
watch: {
sessionData: {
deep: true,
handler() {
//commit changes here
}
}
}
I am using Vue.js. I have declared a function in methods called watcher() and I want to call this function when page load at first, so I called this function in mounted like mounted(){this.watcher()}. It's not showing any error in browser's console. The website is also working fine but in command line, while rendering it says the watcher does not exist on type:
export default {
name: 'Products',
data() {
return {}
},
firestore() {},
methods: {
onFileChange(e) {},
addNewProduct() {},
watcher() {
db.collection('products').onSnapshot((querySnapshot) => {
this.products = [];
querySnapshot.forEach((doc) => {
const product = {
id: doc.id,
name: doc.data().name,
price: doc.data().price
}
this.products.push(product);
});
});
},
CancelUpdateProduct() {},
updateProduct(product) {},
productEdit(product) {},
showEditModal() {},
reset() {},
deleteProduct(id) {},
addProduct() {}
},
mounted() {
this.$watcher();
}
}
I just want to remove this error in comand line, as it may make any error in future!
I have got a Vue Component which has a list of values, when you select these values this changed the selected array, which in tern is posted to an endpoint.
I have an issue if the user spam clicks these values, as an individual post is created for each change, I want it so that if the user selects another item then the currently pending post is cancelled, so then the new value is posted and updates the endpoint with both the selected items.
However i'm having an issue with aborting the current axios request, I have provided the code below. There are no errors, the request simply doesn't cancel.
export default {
props: {
endpoint: {
default: '',
type: String
},
parameters: {
default: null,
type: Object
}
},
data: () => ({
loaded: false,
selected: [],
save: [],
data: [],
cancel: undefined
}),
methods: {
update() {
const self = this;
let params = this.parameters;
params.data = this.selected;
this.$root.$emit('saving', {
id: this._uid,
saving: true
});
if (self.cancel !== undefined) {
console.log('cancel');
this.cancel();
}
window.axios.post(this.endpoint + '/save', params, {
cancelToken: new window.axios.CancelToken(function executor(c) {
self.cancel = c;
})
}).then(() => {
this.$nextTick(() => {
this.loaded = true;
this.$root.$emit('saving', {
id: this._uid,
saving: false
});
});
}).catch(function (thrown) {
if (window.axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
}
});
}
}
}
I have got a global instance of Axios created on my Vue Application.