xstate: how can I initialize the state to a specific node? - javascript

I have a multi step form that basically has these basic steps: select services -> contact -> billing. I display a progress bar and emit events when the user changes the step they're on, and this is my current basic pattern with xstate:
const formMachine = new Machine({
id: 'form-machine',
initial: 'selectService',
context: {
progress: 0,
pathname: '/select-service',
},
states: {
selectService: {
entry: assign({
progress: 0,
pathname: '/select-service',
}),
on: {
NEXT: 'contact',
}
},
contact: {
entry: assign({
progress: 1 / 3,
pathname: '/billing'
}),
on: {
PREVIOUS: 'selectService',
NEXT: 'billing',
}
},
// ... there's more actions but that's the gist of it
}
});
Here's the visualization:
In my react component, I watch this service for changes in pathname so I can push to the history
function SignupFormWizard() {
const history = useHistory();
const [state, send, service] = useMachine(formMachine);
useEffect(() => {
const subscription = service.subscribe((state) => {
history.push(state.context.pathname);
});
return subscription.unsubscribe;
}, [service, history]);
// ...
}
However, here's the problem: whenever I revisit a route (say, I directly navigate to /billing), it will immediately bring me back to /select-service. This makes sense with my code because of the initial state, and the subscription, that it will do that.
How would I go about initializing the state machine at a specific node?

useMachine hook accepts second parameter which is a configuration object. In this object you can set state using state key, but you will have to construct state yourself, and it would look something like this:
let createdState = State.create(stateDefinition);
let resolvedState = formMachine.resolve(state);
let [state, send, service] = useMachine(formMachine, {state: resolvedState});
In my opinion it works well when you need to restore persisting state, but creating stateDefinition manually from scratch is just too much hustle.
What you can do, is create initial state and choose where you want to actually start:
initial: {
always: [
{
target: "selectService",
cond: "isSelectServicePage",
},
{
target: "contact",
cons: "isContactPage",
},
{
target: "billing",
cond: "isBillingPage",
},
],
}
Then, when you are starting your machine, all you have to do is set initial context value:
let location = useLocation();
let [state, send, service] = useMachine(formMachine, {
context: { pathname: location.pathname },
});

The other answer is almost correct, to be more precise, you can move to first state which will decide the next step dynamically, based on context assigned.
createMachine(
{
id: "Inspection Machine",
initial:
"decideIfNewOrExisting",
states: {
"decideIfNewOrExisting": {
always: [
{
target: "initialiseJob",
cond: "isNewJob"
},
{
target: "loadExistingJob"
}
]
},
...

Related

Mocking React Query useQueryClient to test cached data

I use a custom hook to share an increment function accross my app (it increments quantities in a shopping cart).
What that function does :
gets a data object from a React Query cache
increments the data quantity property
makes some API call via React Query useMutation then, on success, updates the React Query cache
After that, the components reads the React Query cart cache and updates the UI with the new quantity.
The hook does the job and finally the cache is updated as expected.
Now, I try to test the hook in a component to check that the UI is updated accordingly.
The API call is mocked using msw. Its returned value is used to update the cache :
rest.put(`${api}/carts/1`, (req, res, ctx) => {
return res(ctx.json({ data: [{ id: 1, quantity: 2 }] }));
})
I also mocked the react-query queryClient.setQueryData and getQueryData functions so I can test their returns values.
jest.mock("react-query", () => ({
...jest.requireActual("react-query"),
useQueryClient: () => ({
setQueryData: jest.fn(),
getQueryData: jest
.fn()
.mockReturnValueOnce({ data: [{ id: 1, quantity: 1 }] })
.mockReturnValueOnce({ data: [{ id: 1, quantity: 2 }] }),
}),
}));
Finally, I test the UI that should updates with the new quantity, but the mocked getQueryData always return the original quantity: 1, even with multiple call.
Now I'm not sure I have the right approach for that test.
Suppose I just want to mock getQueryData for a particular test case and leave the other functions like invalidateQueries, cancelQuery, and setQueryData as it is, then how can modify this mock function?
This is what I wrote. But getting this
TypeError: queryClient.cancelQueries is not a function
jest.mock("#tanstack/react-query", () => ({
...jest.requireActual("#tanstack/react-query"),
useQueryClient: () => ({
// setQueryData: jest.fn(() => ({ data: [{ label: 'Blue', id: 34 }] })),
// cancelQueries: jest.fn(),
// invalidateQueries: jest.fn(),
...jest.requireActual("#tanstack/react-query").useQueryClient(),
getQueryData: jest
.fn()
.mockReturnValueOnce({ data: [{ id: 1, quantity: 1 }] })
.mockReturnValueOnce({ data: [{ id: 1, quantity: 2 }] }),
}),
}));
Why would you need to mock setQueryData and getQueryData ? Mocking the network layer with msw should be all you need. If you wrap your rendered hook in a QueryClientProvider with a queryClient, that will be populated with the mocked data returned from msw, and queryClient.getQueryData will be able to read it without mocking it.

How to change Grid.js state from the plugin?

How can I change state of the Grid.js component from the plugin context?
I'm trying to implement a visual configurator for the pagination.limit prop (https://gridjs.io/docs/config/pagination):
import { BaseComponent, BaseProps, h, PluginPosition } from 'gridjs';
class LimitConfigurator extends BaseComponent {
setLimit(limitNew) {
// this.setState({ pagination: {limit: limitNew}});
// this.config.pagination.limit = limitNew;
}
render() {
const self = this;
return h(
'div',
{ class: 'limit-configurator' },
[
h('span', { onclick: function () { self.setLimit(10); } }, '10'),
h('span', {}, '25'),
h('span', {}, '50'),
]
);
}
}
It renders something like 10 | 25 | 50 under the grid's footer and dynamically sets new per-page number for the pagination.limit path in the original component. Registration code:
// somewhere in the Vue component (using gridjs-vue bridge)...
async mounted() {
await elementReady('.grid-operator', { stopOnDomReady: false });
const gridComponent = this.$refs.gridReference.$data.grid;
gridComponent.plugin.add(
{
id: 'limitEditor',
component: LimitConfigurator,
position: PluginPosition.Footer,
},
);
gridComponent.forceRender();
},
I see prop values in the this.config but don't know how to properly manipulate them from the plugin (currently learning preact + vue environment). Is there a "convenient" way to do it?
I'm not sure if you've found a solution. But here is my take as I recently came across this and figured.
In a plugin which extends the base component, we have access to this.config. On this.config we have eventEmitter property. We can emit events and listen on the grid object which we originally created as gridjs has a global EventEmitter object.
Eg:
const grid = new Grid({
columns: ['Name', 'Email', 'Phone Number'],
sort: true,
search: true,
data: [
['John', 'john#example.com', '(353) 01 222 3333'],
['Mark', 'mark#gmail.com', '(01) 22 888 4444'],
['Eoin', 'eo3n#yahoo.com', '(05) 10 878 5554'],
['Nisen', 'nis900#gmail.com', '313 333 1923']
]
});
grid.on('custom-event', data => console.log(data));
From your plugin method, you can just emit the 'custom-event' like this.config.eventEmitter.emit('custom-event', 'foobar')

Update dynamic components disabled state based on Vuex state value

I have no idea if what I'm doing is correct or not, but here's a simplified version of what I'm trying to do:
I want to have 3 file inputs, with the 2nd and 3rd disabled until the 1st one has had a file selected.
I've tried to do is set the Vuex state variable to whatever the first file input is has selected, but upon doing that the other 2 inputs don't update their disabled state.
I have some file inputs that are created dynamically, like so:
Vue.component('file-input', {
props: ['items'],
template: `<div><input type="file" v-on:change="fileSelect(item)" v-bind:id="item.id" v-bind:disabled="disabledState"></div>`,
methods: {
fileSelect: function(item) {
store.commit('fileSelect', file);
}
},
computed: {
disabledState: function (item) {
return {
disabled: item.dependsOn && store.getters.getStateValue(item.dependsOn)
}
}
}
}
The data for the component is from the instance:
var vm = new Vue({
data: {
items: [
{ text: "One", id: "selectOne" },
{ text: "Two", id: "selectTwo", dependsOn: "fileOne" },
{ text: "Three", id: "selectThree", dependsOn: "fileOne" }
}
});
Now, notice the "dependsOn". In the Vuex store, I have a corresponding state item:
const store = new Vuex.Store({
state: {
files: [
{
fileOne: null
}
]
},
mutations: {
fileSelect(state, file) {
state.files.fileOne = file;
}
},
getters: {
getStateValue: (state) => (stateObject) => {
return state.files.findIndex(x => x[stateObject] === null) === 0 ? true : false;
}
}
});
Now, the above works when everything is first initialized. But once the first input has something selected, the other two inputs don't change.
I'm not sure how to update the bindings once a mutation of the state occurs.
I think you need to refactor your mutation to make the state property mutable, like this:
fileSelect(state, file) {
Vue.set(state.files[0].fileOne, file);
}
Well, I figured it out...
Because my state object is an array of objects, I can't just change one of the property's values with state.files.fileOne. I needed to do state.files[0].fileOne.

How to deep clone the state and roll back in Vuex?

In Vuex I would like to take a snapshot / clone of an object property in the tree, modify it, and later possibly roll back to the former snapshot.
Background:
In an application the user can try out certain changes before applying them. When applying the changes, they should effect the main vuex tree. The user can also click «cancel» to discard the changes and go back to the former state.
Example:
state: {
tryout: {},
animals: [
dogs: [
{ breed: 'poodle' },
{ breed: 'dachshund' },
]
]
}
User enters »Try out« mode and changes one breed from poodle to chihuahua. She then decides either to discard the changes or apply them.
state: {
animals: [
dogs: [
{ breed: 'poodle' },
{ breed: 'dachshund' },
]
],
tryout: {
animals: [
dogs: [
{ breed: 'chihuahua' },
{ breed: 'dachshund' },
]
]
}
}
Discard (rolls back to previous state):
state: {
animals: [
dogs: [
{ breed: 'poodle' },
{ breed: 'dachshund' },
]
],
tryout: {}
}
Apply (saves the changes in main vuex tree):
state: {
animals: [
dogs: [
{ breed: 'chihuahua' },
{ breed: 'dachshund' },
]
],
tryout: {}
}
What are good solutions to deep clone a state, make changes on the clone, and later on either discard the changes or apply them?
The example here is very basic, the solution must work with more complex objects / trees.
Edit 1:
There is a library called vuex-undo-redo, which basically logs mutations, but has some problems. In another Stack Overflow topic Going back to States like Undo Redo on Vue.js vuex it is recommended to use the vuex function replaceState(state).
You can use JSON.stringify and JSON.parse with replaceState.
In vuex:
const undoStates = [];
// save state
undoStates.push(JSON.stringify(state));
// call state (remove from stack)
if (undoStates.length > 0) {
this.replaceState(JSON.parse(undoStates.pop()));
}
That will create a copy of the entire state, but you can also use a part of the store:
const animalStates = [];
// save state
animalStates.push(JSON.stringify(state.animals));
// call state (remove from stack)
if (animalStates.length > 0) {
let animals = JSON.parse(animalStates.pop());
this.replaceState({...state, animals} );
}
This will merge the current state with an object you chose (like animals in this case).

ReactJS + React Router: How to reference a specific object to pass down as properties?

I currently have a table and each cell has a button. Upon clicking the button, based on that particular day (Monday or Tuesday), class (class 1 or class 2), and name (Kev or Josh), how can I push an object related to that particular button in the table to a new page? Using, ReactJS + React Router, what would be the correct approach to this?
And when once navigated to the new page, the new page would then populate a table with the class information from the object passed in, related to button cell clicked.
Code:
http://jsfiddle.net/k7wbzc4j/16/
Should the table rows data be structured like so and reference the object based on ID like the following, or what would be a better structure -- would like to locate that particular id object based on the cell location (with day, name, and class number taken into account)?
list: [
{
name: Kev
monday: {
class1: {
id: 0,
classTitle: abc,
number: class1,
info: {
time: 1,
classSize: 2,
}
},
class 2: {
id: 1,
classTitle: def,
number: class2,
info: {
time: 1,
classSize: 2,
}
}
},
tuesday: {
class1: {
id: 2,
classTitle: ghi,
number: class1,
info: {
time: 1,
classSize: 2,
}
},
class 2: {
id: 3,
classTitle: jkl,
number: class2,
info: {
time: 1,
classSize: 2,
}
}
},
},
{
name: Josh,
monday: {
class1: {
id: 4,
classTitle: mno,
number: class1,
info: {
time: 1,
classSize: 2,
}
},
class2: {
id: 5,
classTitle: pqr,
number: class2,
info: {
time: 1,
classSize: 2,
}
}
},
tuesday: {
class1: {
id: 6,
classTitle: stu,
number: class1,
info: {
time: 1,
classSize: 2,
}
},
class2: {
id: 7,
classTitle: vwx,
number: class2,
info: {
time: 1,
classSize: 2,
}
}
},
}
}
]
For example, if the Kev's row in Monday column, class1, would like to pass the props of the following to the next page:
class1: {
id: 0,
classTitle: abc,
number: class1,
info: {
time: 1,
classSize: 2,
}
}
Will accept and upvote answer. Thank you in advance
This is where a centralized state management tool like redux comes in handy. You could dispatch an action (something like SELECT_CLASS) that sets the selected class in the store, then read that value from your new component. I'd recommend this approach.
However, there is another way. Each browser history entry has a concept of state. So, when you navigate to your new route, you can save the selected data in the state for that history entry, then read it when the component renders. It would look something like this:
Save state:
router.push({
to: '/path',
state: class1
});
To read it:
router.location.state
React router docs: https://github.com/ReactTraining/react-router/blob/master/docs/API.md#pushpathorloc
UPDATE: Yay for redux! So first, lets define the action + action creator for this:
export const SELECT_CLASS = 'SELECT_CLASS';
export const selectClass = (class) => {
return {
type: SELECT_CLASS,
class,
}
};
Now, lets take a look at what the reducer to handle this would look like:
import { SELECT_CLASS } from '/path/to/action'
const selectedClass = (
state={},
action
) => {
switch (action.type) {
case SELECT_CLASS:
return action.class;
default:
returns state;
}
};
Now, assuming you've connected your components via react-redux like this:
// First argument is mapStateToProps, the second is mapDispatchToProps
// https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options
export default connect((state) => state, { selectClass } )(YourComponent);
This is how you'll set the selected class:
// Wherever you need to navigate
function eventHandler(e) {
const { selectClass } = this.props;
selectClass(class1);
router.push('/next/location');
}
And in your next component (which should also be connected, but doesn't require mapDispatchToProps:
render() {
const { selectedClass } = this.props;
// do what you need with the class.
}
If you need any help setting up react + redux, the docs here are fantastic. If the ES6 syntax I used looks unfamiliar, a good reference are the babel docs here
UPDATE 2: I took your fiddle and ran with it: http://jsfiddle.net/k7wbzc4j/17/. Each button has a click event that will get the correct object.
FINAL UPDATE: So here's the rundown of handleClick
handleClick(e) {
const classTitle = e.target.innerHTML;
let obj = undefined;
data.forEach((schedule) => {
['monday', 'tuesday'].forEach((day) => {
const classes = schedule[day];
return Object.keys(classes).forEach((c) => {
const cls = classes[c];
if (cls.classTitle === classTitle) {
obj = cls;
}
});
})
});
if (obj) {
// do your navigation here
}
}
First, we use forEach to iterate over the array data that holds all of the schedules. Then, we iterate over each day (currently only monday and tuesday) to get all the classes on that day for the current schedule. Then, since we don't know the names of all the classes, we just grab all of the keys (which are the class names) and iterate over those. Finally, we can get the actual class objects and check if the classTitle matched the one that was clicked. If so, we save that object to obj. At the very end, we ensure that we found a matching object, then do whatever is necessary to properly handle that click. That's pretty messy, but necessary given your data structure. I'd recommend trying to store this data a bit more relationally and storing each type of item separately (eg. Users, Classes, Days, and Schedules objects all in separate arrays with ids linking them together)
To address your concern that some classTitles will be the same, you'll have to also figure out who's schedule was clicked (and potentially the day that it was clicked). You could do this a number of ways: add a data-user attribute to each button and grab that on click, implement a concept of a "Selected User", create a new component (maybe called UserSchedule) that renders a row in that table and takes, as props, that user's classes. That way, you'll know that any class that matches belongs to that user. Like I said: a number of ways.
As for showing you how to pass that to a new component, I've demonstrated 2 ways above of how to do that above: through history state or storing the value in a redux reducer and receiving it as props. Doing any more would equate to me solving the entire problem for you, which I'm not going to do since the only way this stuff sticks is for you to do it yourself. Trust in yourself, follow the advice I've given, and use the internet for any specific bugs you're having and you'll come out on top!

Categories

Resources