I have xstate react script whereby a user fills in a form, and presses submit. On submit the xstate received a send("VALIDATE", {formData}) and that is run through a service that validates the form. On success the script transitions to the target: "success" and i need that final "success" state to call an external function which actually does the saving of the script.
I can get the data into the validator function, BUT, after the onDone, the subsequent success state doesn't appear to see the data.
How can I wire the data from the validating event to the success event??
id: 'validator',
initial: 'populating',
context: {},
states: {
populating: {
on: {
VALIDATE: 'validating'
}
},
validating: {
invoke: {
src: (context, data) => doValidate(data),
onDone: {
target: 'success',
actions: assign({ data: "hello world"})
},
onError: 'failure'
}
},
success: {
invoke: {
// I do see the hello world here, but what I want is the 'data' from the doValidate(data)
src: (ctx)=>{console.log("invoked success, what can I see here: ", ctx)}
}
},
I'm triggering the validate via: send("VALIDATE", formData)
If I understood you correctly you want the event from the first service onDoneto be available to the second service?
The easiest way would be to put the data on the context and access it in the other state/service and delete it afterward.
Or you can model your machine to send custom event to itself when the first
service is done.
import { createMachine, assign, send, interpret } from "xstate";
const machine = createMachine({
preserveActionOrder: true, //make sure actions are executed in order
id: "validator",
initial: "populating",
context: {},
states: {
populating: {
on: {
VALIDATE: "validating"
}
},
validating: {
on: {
// Listen to the event that fires when the first service is done
FIRST_SERVICE_DONE: {
target: "success"
}
},
invoke: {
src: (context, data) => Promise.resolve({ prop: "first service" }),
onDone: {
// target: 'success',
actions: [
assign({ data: "hello world" }),
//instead of using target:'success' send a custom
//event that has access to the data from the service
send((context, event) => {
//event here has event.data.prop === 'first service'
console.log("send evt ", event);
return {
type: "FIRST_SERVICE_DONE",
prop: event.data.prop
};
})
]
},
onError: "failure"
}
},
success: {
invoke: {
src: (_ctx, evt) => {
console.log("evt ", evt.prop); //first service
}
}
},
failure: {}
}
});
const service = interpret(machine);
service.start();
service.send({ type: "VALIDATE" });
Codesandbox
In Xstate the context is the extended state, so it doesn't seem like a good practice to use the context as a "machine memory". The extended state is used so that you don't have a potentially infinite number of states.
In case you need to preserve information that is sent by the event going to the state that invokes a Promise, you can add that information to the response. For example:
export const myMachineServices = {
myRequest: (context, event) =>
new Promise(resolve => {
setTimeout(() => {
resolve({
payload: 'some data',
})
}, 1000)
}).then(res => ({
...res,
event.data
}))
.catch(err => ({
...err,
event.data
})),
}
Related
I would like to extract all active modules and actions with given roles.
getAvailableModulesAndActionsWithRoles(roles: string[]): IHubModule[] {
const modules = [...this.getModules()];
const activeModules = modules.filter(module => module.isEnabled);
activeModules.map(module => {
Object.keys(module.actions).map(action => {
roles.map(role => {
if (!module.actions[action]?.includes(role)) {
delete module.actions[action];
return;
}
});
});
});
return activeModules;
}
Here is how my module interface looks like
export interface IHubModule {
name: ModuleName;
isEnabled: boolean;
actions: Record<Topic, string[]>;
}
I passed an array of roles as an argument and I would like to extract all active actions for a given roles.
Here is how my test looks like
it('returns only active modules and actions for a list of roles', () => {
const moduleInfos: IHubModule[] = [
{
name: ModuleNameTest.TEST as any,
isEnabled: true,
actions: {
get: [BaseRole.STUDENT, BaseRole.EMPLOYEE],
post: [BaseRole.ADMIN, BaseRole.UNVERIFIED_GUEST],
update: [],
del: [BaseRole.ADMIN, BaseRole.UNVERIFIED_GUEST, BaseRole.STUDENT],
},
},
];
jest
.spyOn(hubService, 'getModules')
.mockImplementation(() => moduleInfos);
const results: IHubModule[] = [
{
name: ModuleNameTest.TEST as any,
isEnabled: true,
actions: {
post: [BaseRole.ADMIN, BaseRole.UNVERIFIED_GUEST],
del: [BaseRole.ADMIN, BaseRole.UNVERIFIED_GUEST, BaseRole.STUDENT],
},
},
{
name: ModuleNameTest.TEST as any,
isEnabled: true,
actions: {
get: [BaseRole.STUDENT, BaseRole.EMPLOYEE],
del: [BaseRole.ADMIN, BaseRole.UNVERIFIED_GUEST, BaseRole.STUDENT],
},
},
]
expect(
hubService.getAvailableModulesAndActionsWithRoles([
BaseRole.ADMIN,
BaseRole.STUDENT,
]),
).toStrictEqual(results);
});
This only work for one argument and I would like to get all module with available action for a given role. Is there a way to skip the action key instead of deleting it if the role is not found ? Because I needed on the next iteration.
I think that instead of modifying the original modules, you should create copies with the required content.
getAvailableModulesAndActionsWithRoles(roles: string[]): IHubModule[] {
const activeModules = this.getModules().filter(module => module.isEnabled);
// selecting modules corresponding to each action
return roles.flatMap(role => activeModules.map(
module => {
const actions = {};
Object.keys(module.actions)
.filter(key => Array.isArray(module.actions[key]) && module.actions[key].includes(role))
.forEach(key => {
actions[key] = module.actions[key];
});
return {...module, actions } as IHubModule;
}
));
//TODO We should also filter modules having empty actions
}
I'm using Paginated for showing data and the user can remove the item. user after a click on the button remove send request delete and get response success.
I want to remove the item in catch react-query.
I don't want to use method refetch
get all items on the server :
const useGetAll = () =>
useQuery(['applications/getAll', page], () => axios.get<GetAllApplication>('localhost:...', { params: { page } }), {
keepPreviousData: true,
})
interface response data
interface GetAllApplication {
hasError: boolean
data: {
meta: {
itemsPerPage: number
totalItems: number
currentPage: number
totalPages: number
sortBy: [['id', 'DESC']]
}
response: {
id: number
name: string
status: 'enable' | 'disable'
}[]
}
}
remove item request with useMutation :
const useRemoveApplication = () =>
useMutation('applications/remove', removeApplication, {
onSuccess({ message },id ) {
toast(message, { type: 'success' })
},
})
You should use queryClient's method setQueryData in your onSuccess.
Reference: react-query docs
I'm implementing the PayPal Smart Payment Buttons with React, and every time my component re-renders I receive a duplicate of the buttons (with the one on the bottom holding the correct transaction information).
Clearly I need to close the buttons, if I try so I receive the error that window.paypal.close()is not a function.
I tried to follow this example: Paypal React shows extra buttons after changing amount
Here is my code, I'm using Redux for state management and I need to rerender the component if items in the shopping cart are removed (to update the item information of the transaction):
useEffect(() => {
if (window.myButton) {
window.myButton.close()
}
window.myButton = window.paypal
.Buttons({
createOrder: (data, actions) => {
return actions.order.create({
purchase_units: [
{
description: "test transaction",
amount: {
currency_code: "USD",
value: document.getElementById("totalAmount").innerHTML,
breakdown: {
item_total: {
currency_code: "USD",
value: document.getElementById("totalAmount").innerHTML
}
}
}
,
items: itemsInCart.map(item => {
console.log(item.value)
return {
name: item.name,
unit_amount: {
currency_code: "USD",
value: String(item.price)
},
quantity: "1"
}
})
}
]
});
},
onApprove: async (data, actions) => {
const order = await actions.order.capture();
}
.catch(function(error) {
console.error("Error writing document: ", error);
});
},
onError: err => {
// setError(err);
console.error(err);
}
})
.render(paypalRef.current)
}, [itemsInCart]);
})
.render(paypalRef.current)
The problem is you are setting myButton to the .render() promise result, not the Button itself.
You need to store a reference to the actual Button (before rendereing it), and only then .render() it -- so that later you can call .close() on the reference. Basically:
let myButton = paypal.Buttons(
....
});
myButton.render(paypalRef.current)
// and at some later point in time...
myButton.close();
I am having a ReactJS component which does two things:
- on ComponentDidMount it will retrieve a list of entries
- on Button click it will submit the select entry to a backend
The problem is that i need to mock both requests (made with fetch) in order to test it properly. In my current testcase i want to test a failure in the submit on the button click. However due some odd reason the setState is triggered however the update from that is received after i want to compare it.
Dumps i did for the test. First one is the state as listen in the test. The second is from the code itself where it is setting state().error to the error received from the call
FAIL react/src/components/Authentication/DealerSelection.test.jsx (6.689s)
● Console
console.log react/src/components/Authentication/DealerSelection.test.jsx:114
{ loading: true,
error: null,
options: [ { key: 22, value: 22, text: 'Stationstraat 5' } ] }
console.log react/src/components/Authentication/DealerSelection.jsx:52
set error to: my error
The actual test code:
it('throws error message when dealer submit fails', done => {
const mockComponentDidMount = Promise.resolve(
new Response(JSON.stringify({"data":[{"key":22,"value":"Stationstraat 5"}],"default":22}), {
status: 200,
headers: { 'content-type': 'application/json' }
})
);
const mockButtonClickFetchError = Promise.reject(new Error('my error'));
jest.spyOn(global, 'fetch').mockImplementation(() => mockComponentDidMount);
const element = mount(<DealerSelection />);
process.nextTick(() => {
jest.spyOn(global, 'fetch').mockImplementation(() => mockButtonClickFetchError);
const button = element.find('button');
button.simulate('click');
process.nextTick(() => {
console.log(element.state()); // state.error null even though it is set with setState but arrives just after this log statement
global.fetch.mockClear();
done();
});
});
});
This is the component that i actually use:
import React, { Component } from 'react';
import { Form, Header, Select, Button, Banner } from '#omnius/react-ui-elements';
import ClientError from '../../Error/ClientError';
import { fetchBackend } from './service';
import 'whatwg-fetch';
import './DealerSelection.scss';
class DealerSelection extends Component {
state = {
loading: true,
error: null,
dealer: '',
options: []
}
componentDidMount() {
document.title = "Select dealer";
fetchBackend(
'/agent/account/dealerlist',
{},
this.onDealerListSuccessHandler,
this.onFetchErrorHandler
);
}
onDealerListSuccessHandler = json => {
const options = json.data.map((item) => {
return {
key: item.key,
value: item.key,
text: item.value
};
});
this.setState({
loading: false,
options,
dealer: json.default
});
}
onFetchErrorHandler = err => {
if (err instanceof ClientError) {
err.response.json().then(data => {
this.setState({
error: data.error,
loading: false
});
});
} else {
console.log('set error to', err.message);
this.setState({
error: err.message,
loading: false
});
}
}
onSubmitHandler = () => {
const { dealer } = this.state;
this.setState({
loading: true,
error: null
});
fetchBackend(
'/agent/account/dealerPost',
{
dealer
},
this.onDealerSelectSuccessHandler,
this.onFetchErrorHandler
);
}
onDealerSelectSuccessHandler = json => {
if (!json.error) {
window.location = json.redirect; // Refresh to return back to MVC
}
this.setState({
error: json.error
});
}
onChangeHandler = (event, key) => {
this.setState({
dealer: event.target.value
});
}
render() {
const { loading, error, dealer, options } = this.state;
const errorBanner = error ? <Banner type='error' text={error} /> : null;
return (
<div className='dealerselection'>
<Form>
<Header as="h1">Dealer selection</Header>
{ errorBanner }
<Select
label='My dealer'
fluid
defaultValue={dealer}
onChange={this.onChangeHandler}
maxHeight={5}
options={options}
/>
<Button
primary
fluid
onClick={this.onSubmitHandler}
loading={loading}
>Select dealer</Button>
</Form>
</div>
);
}
}
export default DealerSelection;
Interesting, this one took a little while to chase down.
Relevant parts from the Node.js doc on Event Loop, Timers, and process.nextTick():
process.nextTick() is not technically part of the event loop. Instead, the nextTickQueue will be processed after the current operation is completed, regardless of the current phase of the event loop.
...any time you call process.nextTick() in a given phase, all callbacks passed to process.nextTick() will be resolved before the event loop continues.
In other words, Node starts processing the nextTickQueue once the current operation is completed, and it will continue until the queue is empty before continuing with the event loop.
This means that if process.nextTick() is called while the nextTickQueue is processing, the callback is added to the queue and it will be processed before the event loop continues.
The doc warns:
This can create some bad situations because it allows you to "starve" your I/O by making recursive process.nextTick() calls, which prevents the event loop from reaching the poll phase.
...and as it turns out you can starve your Promise callbacks as well:
test('Promise and process.nextTick order', done => {
const order = [];
Promise.resolve().then(() => { order.push('2') });
process.nextTick(() => {
Promise.resolve().then(() => { order.push('7') });
order.push('3'); // this runs while processing the nextTickQueue...
process.nextTick(() => {
order.push('4'); // ...so all of these...
process.nextTick(() => {
order.push('5'); // ...get processed...
process.nextTick(() => {
order.push('6'); // ...before the event loop continues...
});
});
});
});
order.push('1');
setTimeout(() => {
expect(order).toEqual(['1','2','3','4','5','6','7']); // ...and 7 gets added last
done();
}, 0);
});
So in this case the nested process.nextTick() callback that logs element.state() ends up running before the Promise callbacks that would set state.error to 'my error'.
It is because of this that the doc recommends the following:
We recommend developers use setImmediate() in all cases because it's easier to reason about
If you change your process.nextTick calls to setImmediate (and create your fetch mocks as functions so Promise.reject() doesn't run immediately and cause an error) then your test should work as expected:
it('throws error message when dealer submit fails', done => {
const mockComponentDidMount = () => Promise.resolve(
new Response(JSON.stringify({"data":[{"key":22,"value":"Stationstraat 5"}],"default":22}), {
status: 200,
headers: { 'content-type': 'application/json' }
})
);
const mockButtonClickFetchError = () => Promise.reject(new Error('my error'));
jest.spyOn(global, 'fetch').mockImplementation(mockComponentDidMount);
const element = mount(<DealerSelection />);
setImmediate(() => {
jest.spyOn(global, 'fetch').mockImplementation(mockButtonClickFetchError);
const button = element.find('button');
button.simulate('click');
setImmediate(() => {
console.log(element.state()); // state.error is 'my error'
global.fetch.mockClear();
done();
});
});
});
There are several asynchronous calls required to update the state, so your process.nextTick() isn't sufficient. To update the state, this needs to happen:
your test code clicks, and the event handler callback is queued
the event handler callback runs, runs fetch, gets a promise rejection, and runs the error handler
the error handler runs setState, which queues the state update (setState is asynchronous!)
your test code runs, checking the element's state
the state update runs
In short, you need to wait longer before asserting on the state.
A useful idiom to "wait" without nested process.nextTick() calls is to define a test helper
function wait() {
return new Promise((resolve) => setTimeout(resolve));
}
and then do
await wait();
as many times as required in your test code. Note that this requires you to define test functions as
test(async () => {
})
rather than
test(done => {
})
So first off I will start by saying I added an optimistic response to my mutation so it would it stop producing duplicates as referenced here and from this previous S.O. question.
So that is all working but I have a set of dependant mutations that run after the first using async await.
submitForm = async () => {
// Only submit if form is complete
if (!this.state.saveDisabled) {
try {
// Optimistic Response is necessary because of AWS AppSync
// https://stackoverflow.com/a/48349020/2111538
const createGuestData = await this.props.createGuest({
name: this.state.name,
})
let guestId = createGuestData.data.addGuest.id
for (let person of this.state.people) {
await this.props.createPerson({
variables: {
name: person.name,
guestId,
},
optimisticResponse: {
addPerson: {
id: -1, // A temporary id. The server decides the real id.
name: person.name,
guestId,
__typename: 'Person',
},
},
})
}
this.setState({
redirect: true,
})
} catch (e) {
console.log(e)
alert('There was an error creating this guest')
}
} else {
Alert('Please fill out guest form completely.')
}
}
Now this works and it is using the same pattern for the mutation as per the sample project
export default compose(
graphql(CreateGuestMutation, {
name: 'createGuest',
options: {
refetchQueries: [{ query: AllGuest }],
},
props: props => ({
createGuest: guest => {
console.log(guest)
return props.createGuest({
variables: guest,
optimisticResponse: () => ({
addGuest: {
...guest,
id: uuid(),
persons: [],
__typename: 'Guest',
},
}),
})
},
}),
}),
graphql(CreatePersonMutation, {
name: 'createPerson',
}),
)(CreateGuest)
The only problem is that I can't force the state to get updated to the ID that actually gets inserted when using Async Await, so all the person entries get the place holder UUID. Note, I have also tried using id: -1 as is done with the createPerson mutation but that didn't change anything, it just used negative one for all the entires.
Is there a better way of doing this? I am doing something wrong. This all worked without the optimisticResponse but it always created two entries per mutation.
Can you try this again? There were enhancements to the AppSync SDK for Javascript which no longer require you to use Optimistic Response. You can use it optionally if you still want an optimistic UI.
Additionally you can also now disable offline if that's not a requirement for your app by using disableOffline like so:
const client = new AWSAppSyncClient({
url: AppSync.graphqlEndpoint,
region: AppSync.region,
auth: {
type: AUTH_TYPE.API_KEY,
apiKey: AppSync.apiKey,
},
disableOffline: true
});