How to track analytics events with xstate - reactjs

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',
},
}
}
}
})

Related

Adding series to highchart and remove loading without creating new chart

I'm using Highchart with React.
For the moment I created a starting options:
/**
* Initial chart options, with "loading" enabled
*/
const chartOptions = {
accessibility: {
enabled: false
},
chart: {
type: "column",
events: {
load() {
const chart = this;
chart.showLoading(`${LOADING}...`);
}
}
}
};
In that component I call an API that returns some data to use. So, at that moment, I want to remove the "loading" and adding the data.
I solved for the moment with:
const [options, setOptions] = useState(chartOptions);
useEffect(() => {
if (detail) {
setOptions({
...chartOptions,
chart: {
type: "column"
},
xAxis: {
categories: detail.map(
(item) =>
`${item.data_em.substring(4, 6)}/${item.data_em.substring(0, 4)}`
)
},
series: [
{
name: "Alfa",
color: "#69BC66",
data: detail.map((item) => item.ricevuti)
},
{
name: "Beta",
color: "#DC3545",
data: detail.map((item) => item.mancanti)
},
]
});
}
}, [detail]);
<ChartComponent options={options} />
So, setting a new options in local State when data are ready. But I imagine that in this manner I get a different chart.
It works, but is there a better method?
With using the highcharts-react-official wrapper, the chart is recreated on state update only if immutable option is enabled. Otherwise, only chart.update method is called to apply changes.
Therefore, it is better to apply only the new options. In your case it will be:
useEffect(() => {
if (detail) {
setOptions({
chart: {
type: "column"
},
xAxis: {
categories: detail.map(
(item) =>
`${item.data_em.substring(4, 6)}/${item.data_em.substring(0, 4)}`
)
},
series: [{
name: "Alfa",
color: "#69BC66",
data: detail.map((item) => item.ricevuti)
},
{
name: "Beta",
color: "#DC3545",
data: detail.map((item) => item.mancanti)
},
]
});
}
}, [detail]);
Live demo: https://codesandbox.io/s/highcharts-react-demo-3lvomg?file=/demo.jsx
Docs: https://www.npmjs.com/package/highcharts-react-official#optimal-way-to-update

Why are multiple canvases being made in my Phaser/React app?

I am trying to develop a phaser3 application with React. I am just setting up the first canvas for the Phaser.Game. Here is my App.js from the create-react-app.
import "./App.css";
import Phaser, { Game } from "phaser";
import PhaserMatterCollisionPlugin from "phaser-matter-collision-plugin";
import { useCallback, useEffect, useState } from "react";
function App() {
const [game, setGame] = useState(null);
// Creating game inside a useEffect in order to ensure 1 instance is created
console.log("before use effect");
useEffect(() => {
console.log("Going into useEffect");
console.log(game);
if (game) {
console.log("game detected. stop creation");
return;
}
const phaserGame = new Phaser.Game({
width: 512,
height: 412,
backgroundColor: "#333333",
type: Phaser.AUTO,
parent: "survival-game",
scene: [],
scale: {
zoom: 2,
},
physics: {
default: "matter",
matter: {
debug: true,
gravity: { y: 0 },
},
},
plugins: {
scene: [
{
plugin: PhaserMatterCollisionPlugin,
key: "matterCollision",
mapping: "matterCollision",
},
],
},
});
setGame(true);
return;
}, [game]);
}
export default App;
I used useEffect() with useState in order to prevent multiple game instances, but for some reason I am still getting a duplicate canvas and can see that it is running through the useEffect multiple times. console.log of the react app
You should use a ref instead of state for the game object. Here's a small custom hook that sets up a Phaser.Game based on a given configuration:
function usePhaserGame(config) {
const phaserGameRef = React.useRef(null);
React.useEffect(() => {
if (phaserGameRef.current) {
return;
}
phaserGameRef.current = new Game(config);
return () => {
phaserGameRef.current.destroy(true);
phaserGameRef.current = null;
};
}, [] /* only run once; config ref elided on purpose */);
return phaserGameRef.current;
}
const config = {
width: 512,
height: 412,
backgroundColor: '#333333',
type: Phaser.AUTO,
parent: 'survival-game',
scene: [],
scale: {
zoom: 2,
},
physics: {
default: 'matter',
matter: {
debug: true,
gravity: {y: 0},
},
},
plugins: {
scene: [
{
plugin: PhaserMatterCollisionPlugin,
key: 'matterCollision',
mapping: 'matterCollision',
},
],
},
};
function App() {
const game = usePhaserGame(config);
}

Why does forEach loop only set the last value if finds to state. ReactJS

const CategoriesData = [
{
name: "Category1",
isActive: true,
children: [
{
name: "Category1Child",
isActive: false,
}
]
},
{
name: "Category2",
isActive: false,
},
{
name: "Category3",
isActive: true,
children: [
{
name: "Category3Child",
isActive: false,
}
]
}
];
const [disabledCategories, setDisabledCategories] = useState([]);
function notActiveCategories(categories) {
// Loop logs out at least 7 isActive: false categories.
categories.forEach((category) => {
if (category.isActive) notActiveCategories(category.children);
if (!category.isActive) {
setDisabledCategories([...disabledCategories, category]);
console.log(category);
}
});
};
useEffect(() => {
notActiveCategories(CategoriesData);
console.log(disabledCategories); // Only 1 category is in the array.
}, []);
I feel like the function the loop is in calling itself is causing the disabledCategories state to revert to when it was empty and that is leading to only the last step of the foreach to be set.
So how would i get this to loop through the categories array and have the disabledCategories state to contain all of the category objects that have isActive: false.
Which in the example of CategoriesData above, it would mean that the disabledCategories state would contain:
[
{
name: "Category1Child",
isActive: false,
},
{
name: "Category2",
isActive: false,
},
{
name: "Category3Child",
isActive: false,
},
];
Try changing your setDisabledCategories to use the previous state param that comes from setState:
setDisabledCategories(prevState => [...prevState, category])
When multiple setState calls are batched together you need to be careful so they don't override each other. Using this method ensures that your setState calls are "chained" so you always get the updated state.
Way 1: Affect after recursive loop
function notActiveCategoriesRecusive(categories) {
let notActive = []
categories.forEach((category) => {
if (category.isActive) notActive = [...notActive, ...(notActiveCategories(category.children))];
if (!category.isActive) {
notActive.push(category)
}
});
return notActive
};
function notActiveCategories(categories) {
setDisabledCategories(notActiveCategoriesRecusive(categories)
}
Way 2: Get the last state because it doesn't has time to refresh
function notActiveCategories(categories) {
categories.forEach((category) => {
if (category.isActive) notActiveCategories(category.children);
if (!category.isActive) {
setDisabledCategories(oldState => ([...oldState, category]))
}
});
};
I'd only call setState once with the filtered array:
const findInactive = data =>
data.filter(e => !e.isActive)
.concat(...data.filter(e => e.children)
.map(e => findInactive(e.children)))
;
const categoriesData = [ { name: "Category1", isActive: true, children: [ { name: "Category1Child", isActive: false, } ] }, { name: "Category2", isActive: false, }, { name: "Category3", isActive: true, children: [ { name: "Category3Child", isActive: false, } ] } ];
const inactive = findInactive(categoriesData)
// the following is neeeded if it's possible for a
// node to have children and be inactive
// .map(({name, isActive}) => ({name, isActive}))
;
console.log(inactive);
//setDisabledCategories(inactive); // one time in React
This makes the code a lot easier to reason about and decouples React's API out from the filtering logic, which can be moved out to a generic function agnostic of React.
As others have mentioned, if you do want to call setState multiple times as a batch update, you can use the prevState callback to chain the updates: setDisabledCategories(prevState => [...prevState, category]);.

Spawning children machines in xstate

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

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