Spawning children machines in xstate - reactjs

I am currently working on an application using xstate, I have a parent machine that spawns into two different children's machines, the children machines make a fetch to different API endpoint and they all send back to the parent a resolve or reject event depending on the status of the API call , I need help with how to make sure that all fetches are done before transitioning to the idle state on the parent machine.
fetchMachine:
const fetchMachine: FetchMachine =(
fetchFunction
) => (
{
id: 'fetch',
initial: States.Initialize,
context: {
response: null,
error: null
},
states: {
[States.Initialize]: {
on: {
'FETCH.REQUEST': {
target: States.Pending,
}
}
},
[States.Pending]: {
invoke: {
src: 'fetch',
onDone: {
target: States.Success,
actions: ['updateResponse']
},
onError: {
target: States.Failure,
actions: ['updateError']
}
},
},
[States.Success]: {
entry: ['fetchSuccess'],
on: {
'FETCH.REQUEST': States.Pending
}
},
[States.Failure]: {
entry: ['fetchFailure'],
on: {
'FETCH.REQUEST': States.Pending
}
}
}
}
The machine above sends the request of the event back to the parent.
The issue now is that the parent machines utilize this machine parallelly, I need help with how to make sure that all the fetches are done before transitioning to the idle state on the parent machine.

Ideally you'd make use of the final state for a case like this, it's located here in the documentation.
I've re-created your machine in the visualizer, with parallel states that each have a final state to show how it would transition.
Here is the code of the final machine for the sake of completeness:
const parentMachine = Machine({
id: 'your_id_here',
initial: 'pending',
states: {
pending: {
on: { CHANGE_EVENT: 'process' }
},
process: {
type: 'parallel',
states: {
fetchMachine1: {
initial: 'initialize',
states: {
initialize: {
on: {
'FETCH.REQUEST': {
target: 'pending',
}
}
},
pending: {
invoke: {
src: 'fetch',
onDone: {
target: 'success',
actions: ['updateResponse']
},
onError: {
target: 'failure',
actions: ['updateError']
}
},
},
success: {
entry: ['fetchSuccess'],
on: {
'FETCH.REQUEST': 'pending'
},
type: 'final' // 'success' is a final state node for 'fetchMachine1'
},
failure: {
entry: ['fetchFailure'],
on: {
'FETCH.REQUEST': 'pending'
}
}
}
},
fetchMachine2: {
initial: 'initialize',
states: {
initialize: {
on: {
'FETCH.REQUEST': {
target: 'pending',
}
}
},
pending: {
invoke: {
src: 'fetch',
onDone: {
target: 'success',
actions: ['updateResponse']
},
onError: {
target: 'failure',
actions: ['updateError']
}
},
},
success: {
entry: ['fetchSuccess'],
on: {
'FETCH.REQUEST': 'pending'
},
type: 'final' // 'success' is a final state node for 'fetchMachine1'
},
failure: {
entry: ['fetchFailure'],
on: {
'FETCH.REQUEST': 'pending'
}
}
}
}
},
onDone: 'pending'
}
}
});

Related

Sequelize unkown column in field list error

I have a MySql DB and using sequelize.
I have a recipe and ingredients tables.
I want to pass the ingredients as an array to the api.
So I researched and discovered I can use get/set to achieve this.
But I get an "Unknown coumn 'ingredients' in 'field list'".
This is my model. The line
console.info("getDataValue...", this);
never gets executed.
function model(sequelize) {
const attributes = {
recipeTitle: { type: DataTypes.STRING(255), allowNull: false },
category: { type: DataTypes.STRING(30), allowNull: false },
recipeSource: { type: DataTypes.STRING(100), allowNull: false },
recipeSourceData: { type: DataTypes.TEXT(), allowNull: true },
method: { type: DataTypes.TEXT(), allowNull: true },
comments: { type: DataTypes.TEXT(), allowNull: true },
prepTime: { type: DataTypes.STRING(10), allowNull: true },
cookTime: { type: DataTypes.STRING(10), allowNull: true },
rating: { type: DataTypes.FLOAT, allowNull: false },
owner_id: { type: DataTypes.INTEGER, allowNull: false },
ingredients: {
type: DataTypes.TEXT,
get() {
console.info("getDataValue...", this);
return JSON.parse(this.getDataValue("ingredients"));
},
set(val) {
if (!Array.isArray(val)) {
throw new Error("ingredients must to be an array");
}
this.setDataValue("ingredients", JSON.stringify(val));
},
},
};
This is my validate-request middle ware and it does have the ingredients
when i console.info("valreq...").
So it seems its the schema.validate that fails??
function validateRequest(req, next, schema) {
console.info("valreq...", req.body);
const options = {
abortEarly: false, // include all errors
allowUnknown: true, // ignore unknown props
stripUnknown: true, // remove unknown props
};
const { error, value } = schema.validate(req.body, options);
if (error) {
next(`Validation error: ${error.details.map((x) => x.message).join(", ")}`);
} else {
console.info("value...", value);
req.body = value;
next();
}
}

Define multiple layers in Kepler.gl with different Data?

iam new to react, redux, javascript and also Kepler.gl.
My Question is the following:
I managed it to define multiple datasets without any tutorial, by lucky try. (Kepler.gl has no good documentation sadly)
I tried to define "data2" and fill it with an other source of data, so i can bind it to i specific layer. But every time i associate it to a layer, the layer disappears in the List like there was no data. I checked both sources single and both are fine. Its like it only accept the data const not any other. I also replaced all data const with data2 const and it stopped working. So i think it is a problem to the const. Do you guys see what iam missing?
This is the original code with one data source:
import React from "react";
import keplerGlReducer from "kepler.gl/reducers";
import {createStore, combineReducers, applyMiddleware} from "redux";
import {taskMiddleware} from "react-palm/tasks";
import {Provider, useDispatch} from "react-redux";
import KeplerGl from "kepler.gl";
import {addDataToMap} from "kepler.gl/actions";
import useSwr from "swr";
const reducers = combineReducers({
keplerGl: keplerGlReducer,
});
const store = createStore(reducers, {}, applyMiddleware(taskMiddleware));
const mapboxApiAccessToken = "pk.eyJ1IjoiaHN1bGRlIiwiYSI6ImNsMWNlZ3ZtaTA0MXgzZXF4eGYybmExOWwifQ.-VMvsl27bE0dhoRRY8xMrw";
export default function Kglapp() {
return (
<Provider store={store}>
<Map/>
</Provider>
);
}
function Map() {
const dispatch = useDispatch();
const {data} = useSwr("test", async () => {
const response = await fetch(
".\\mapdatajson\\covid19.json"
);
const data = await response.json();
return data;
})
React.useEffect(() => {
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-1", id: "test-1"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-2", id: "test-2"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-3", id: "test-3"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-4", id: "test-4"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-5", id: "test-5"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-6", id: "test-6"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-7", id: "test-7"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-8", id: "test-8"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-9", id: "test-9"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-10", id: "test-10"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-11", id: "test-11"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-12", id: "test-12"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-13", id: "test-13"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-14", id: "test-14"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-15", id: "test-15"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-16", id: "test-16"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-17", id: "test-17"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-18", id: "test-18"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-19", id: "test-19"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
if (data) {
dispatch(
addDataToMap(
{
datasets: {
info: {label: "test-20", id: "test-20"}, data,
},
option: {
centerMap: true,
readOnly: true,
split: true
},
config: {}
}),
);
}
}
,
[dispatch, data]
);
return (
<KeplerGl
id="test"
mapboxApiAccessToken={mapboxApiAccessToken}
width={window.innerWidth}
height={window.innerHeight}
/>
);
}
Do somebody have any ideas how to set different sources to the layers without let it dissapear for dubious reasons?
Thank you for your help.

XState React - Invoke Service not triggered

I am using xstate with react to implement a basic login functionality. The code is here and the issue I am facing is, on the event AUTHENTICATING it is meant to invoke a service authenticateUser and it is not invoking. No visible errors in the console. The component looks like
import { useMachine } from "#xstate/react";
import { createMachine, assign } from "xstate";
import "./App.css";
const authenticateUserNew = async (c, e) => {
console.log("service invoked");
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve();
} else {
reject();
}
}, 1000);
});
};
const loginMachine = createMachine(
{
id: "login-machine",
initial: "unauthenticated",
context: {
isAuthenticated: false,
},
states: {
unauthenticated: {
on: {
AUTHENTICATING: {
invoke: {
id: "authenticateUser",
src: (c, e) => authenticateUserNew(c, e),
onDone: {
target: "authenticated",
actions: assign({ isAuthenticated: (context, event) => true }),
},
onError: {},
},
},
},
},
authenticated: {
on: {
LOGOUT: {
target: "unauthenticated",
},
},
},
},
},
{
services: {
authenticateUser: () => {
console.log("service invoked");
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve();
} else {
reject();
}
}, 1000);
});
},
},
}
);
function App() {
const [current, send] = useMachine(loginMachine);
return (
<div className="App">
<h2>{current.value}</h2>
<br />
<h3>
isAuthenticated: {current.context.isAuthenticated ? "True" : "False"}
</h3>
<br />
<button onClick={() => send("AUTHENTICATING")}>AUTHENTICATE</button>
<br />
<button onClick={() => send("LOGOUT")}>LOGOUT</button>
</div>
);
}
export default App;
I have tried both the approach where I can externalize function and use it or define it inside the service section of state machine, in both the case it wasn't invoked.
1st approach
invoke: {
id: "authenticateUser",
src: (c, e) => authenticateUserNew(c, e),
onDone: {
target: "authenticated",
actions: assign({ isAuthenticated: (context, event) => true }),
},
onError: {},
}
2nd approach
invoke: {
id: "authenticateUser",
src: "authenticateUser",
onDone: {
target: "authenticated",
actions: assign({ isAuthenticated: (context, event) => true }),
},
onError: {},
}
React version: ^17.0.2
xstate: ^4.3.5
#xstate/react: 2.0.1
From docs:
An invocation is defined in a state node's configuration with the invoke property
You are instead trying to invoke in an event node, not a state one.
For example, you could do:
...
states: {
unauthenticated: {
on: {
AUTHENTICATE: {
target: 'authenticating'
},
},
},
authenticating: {
invoke: {
id: "authenticateUser",
src: 'authenticateUser',
onDone: {
target: "authenticated",
actions: assign({ isAuthenticated: (context, event) => true }),
},
onError: {
target: 'unauthenticated'
},
},
},
authenticated: {
on: {
LOGOUT: {
target: "unauthenticated",
},
},
},
},
...
and send the AUTHENTICATE event:
<button onClick={() => send("AUTHENTICATE")}>AUTHENTICATE</button>
Moreover, I'd like to suggest to avoid the isAuthenticated at all. You can check if you're authenticated with the matches method:
<h3>
isAuthenticated: {current.matches('authenticated') ? "True" : "False"}
</h3>

How to track analytics events with xstate

I'm using xstate in my React Native app to manage a rather complex flow. I would like to systematically log analytics events on each state transition and event happening in the state machine. So far, I haven't figured how to do that without manually calling a logEvent action from every event. For example on a much smaller machine than the one I built:
const machine = createMachine({
id: 'machine',
initial: 'idle',
context: {},
states: {
idle: {
on: {
NEXT: {
target: 'green',
actions: ['logEvent'] // <-------- here
}
}
},
green: {
on: {
NEXT: {
target: 'green',
actions: ['logEvent'] // <-------- here
},
BACK: {
target: 'idle',
actions: ['logEvent'] // <-------- here
},
}
},
red: {
on: {
NEXT: {
target: 'idle',
actions: ['logEvent'] // <-------- here
},
BACK: {
target: 'green',
actions: ['logEvent'] // <-------- here
},
}
}
}
})
So much repetition :(
Another way I read about was using interpret and adding a onTransition listener (https://xstate.js.org/docs/guides/interpretation.html#transitions). However this requires to also manually send event for the onTransition listener to fire, so it's not a solution imo.
I also found #xstate/analytics, but there are no docs and README says we shouldn't use it ^^
Is there a way to call an action on each transition without repeating myself so much?
You could try having it as an entry action on for every state.
const machine = createMachine({
id: 'machine',
initial: 'idle',
context: {},
states: {
idle: {
entry: ['logEvent'],
on: {
NEXT: {
target: 'green',
}
}
},
green: {
entry: ['logEvent'],
on: {
NEXT: {
target: 'green',
},
BACK: {
target: 'idle',
},
}
},
red: {
entry: ['logEvent'],
on: {
NEXT: {
target: 'idle',
},
BACK: {
target: 'green',
},
}
}
}
})

Apollo GraphQL (React) refetchQueries/update after mutation don't update the store

I have a component that behaves like one in Google drive, when you want to move your file/folder. It fetches all the necessary data about directories, displays it, and after one have been chosen - it moves a file into another folder. The goal I'm trying to aim is update data about currently displayed folder and folder where file has been moved. I tried the both way (refetchQueries, update), but non worked...
The main issue is that queries, defined in updateQueries are executed, but store doesn't update.
It would be great, if anyone could help!
const EntityMoveContainer = compose(
graphql(GET_DIRECTORIES, {
options() {/*...*/},
props(props) {/*...*/}
}),
graphql(MOVE_FILE, {
props(props) {
const { mutate, ownProps } = props;
const { entity, directoryId } = ownProps;
return {
async moveFile(destDirId) {
return mutate({
variables: {
fileId: entity.id,
directoryId: destDirId,
},
refetchQueries: () => [
{
query: GET_DIRECTORIES,
variables: {
id: directoryId,
sortKey: store.sortKey,
cursor: store.cursor,
filetype: store.filetype,
},
},
{
query: GET_DIRECTORIES,
variables: {
id: destDirId,
sortKey: store.sortKey,
cursor: store.cursor,
filetype: store.filetype,
},
},
],
/* update(proxy) {
console.log('update method');
try {
const storeData = proxy.readQuery({
query: GET_DIRECTORIES,
variables: {
id: destDirId,
sortKey: store.sortKey,
filetype: store.filetype,
cursor: store.cursor,
},
});
storeData.directory = {
...storeData.directory,
files: {
...storeData.directory.files,
edges: [
...storeData.directory.files.edges,
{ node: entity, __typename: 'File' },
],
},
};
proxy.writeQuery({
query: GET_DIRECTORIES,
variables: {
id: destDirId,
sortKey: store.sortKey,
filetype: store.filetype,
cursor: store.cursor,
},
data: storeData,
});
} catch (e) {
console.log(e);
}
}, */
});
},
};
},
}),
)(EntityMoveView)
The issue was with a
cursor : ''
property I passed into refetchQueries.

Resources