In the view_submission type I set ack to clear the stack like this:
await submissionAck({ response_action: 'clear' } as any)
First question - why do I have to cast it to any? Without it code throws error
Argument of type '{ response_action: "clear"; }' is not assignable to parameter of type '(ViewUpdateResponseAction & void) | (ViewPushResponseAction & void) | (ViewClearResponseAction & void) | (ViewErrorsResponseAction & void) | undefined'.Type '{ response_action: "clear"; }' is not assignable to type 'ViewClearResponseAction & void'.
Type '{ response_action: "clear"; }' is not assignable to type 'void'.
Second question - the stack seems not to be cleared. When I submit modal for the first time it's okay, but if I try next time it throws:
[ERROR] bolt-app { Error: The receiver's `ack` function was called multiple times.
at ack (/home/ec2-user/metrics/node_modules/#slack/bolt/src/ExpressReceiver.ts:147:17)
at /home/ec2-user/metrics/app/actions.ts:43:17
at Generator.next (<anonymous>)
at /home/ec2-user/metrics/app/actions.ts:11:71
at new Promise (<anonymous>)
at __awaiter (/home/ec2-user/metrics/app/actions.ts:7:12)
at app.view (/home/ec2-user/metrics/app/actions.ts:40:70)
at process_1.processMiddleware (/home/ec2-user/metrics/node_modules/#slack/bolt/src/App.ts:660:19)
at invokeMiddleware (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:36:12)
at next (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:28:21)
at Array.<anonymous> (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/builtin.ts:201:11)
at invokeMiddleware (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:27:47)
at next (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:28:21)
at Array.exports.onlyViewActions (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/builtin.ts:110:11)
at invokeMiddleware (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:27:47)
at Object.processMiddleware (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:39:10) code: 'slack_bolt_receiver_ack_multiple_error' }
Any ideas? That's how I call these views: (by the way 3rd question - why do I have to cast body to BlockAction? Otherwise it throws error that trigger_id doesn't exists)
app.action('modify', async ({ body, ack }) => {
await ack()
await authenticate(body.team.id, async (customer: Customer) => {
await app.client.views.open({
trigger_id: (body as BlockAction).trigger_id,
token: 'token',
view: modificationModal,
})
app.view(
{
type: 'view_submission',
callback_id: 'yay',
},
async ({ body: submissionBody, ack: submissionAck, view }) => {
const receivedValues = submissionBody.view.state.values
await submissionAck({ response_action: 'clear' } as any)
},
)
})
})
I know that in the documentation stands:
view() requires a callback_id of type string or RegExp.
but that doesn't tell me much. What is that string? Is that a function? What should it do?
Sorry for noobish question and thanks for help!
I'll try to answer these in the reverse order, because I think that might make the most sense.
What is that string? Is that a function? What should it do? (referring to app.view())
When you create a Modal, you typically create it with a callback_id. You can see a description for that property in the documentation for a view payload.
That sentence is trying to say this is how you'd listen for a view submission for a view that was created with callback_id set to "some_callback_id":
app.view('some_callback_id', async () => {
/* listener logic goes here */
})
Note: You could also use a regular expression if you wanted the same function to handle view submissions for many views - views whose callback_ids all follow the same pattern. But the regular expression is a pretty advanced case that I don't think we should worry about it for now.
To create a Modal, you use the views.open method, and that's where you'll set the callback_id in the first place. I'm going to suggest an improvement. All Web API methods are available inside a listener as methods on the client argument. Then you don't need to worry about adding the token. Here's an example of using this:
// Add the `client` argument
app.action('modify', async ({ body, ack, client }) => {
await ack()
await authenticate(body.team.id, async (customer: Customer) => {
// Remove `app.`
await client.views.open({
// Let's come back to this cast later
trigger_id: (body as BlockAction).trigger_id,
// Not sure what's in modificationModal, but to illustrate, I used a literal
view: {
// *** Setting the callback_id ***
callback_id: 'modify_submission',
title: {
type: 'plain_text',
text: 'Modify something'
},
blocks: [{ /* add your blocks here */ }],
},
})
})
})
Next, don't handle a view submission inside another listener. When you do that, each time the outer listener runs, you're (re)registering the view submission listener to run. So the first time it will run once, the second time it will run twice, the third time it will run three times. This explains why the stacktrace is telling you that ack() was called multiple times. Instead, just handle the view submission outside that listener. The "linking" information between the two interactions is the callback_id. Building off the previous example:
// Further down in the same file, at the same level as the previous code
// *** Using the callback_id we set previously ***
app.view('modify_submission', async ({ body, ack, view }) => {
const receivedValues = body.view.state.values
// Let's come back to this cast later
await ack({ response_action: 'clear' } as any)
})
Okay this should all work, but now let's talk about the casts. When you handle an action with app.action(), the body argument is typed as BlockAction | InteractiveMessage | DialogSubmitAction. Within these interfaces, BlockAction and InteractiveMessage do have a trigger_id property, but DialogSubmitAction does not. So as far as TypeScript is concerned, it can't be sure the property body.trigger_id exists. You might know that the action you're handling is a BlockAction (let's assume it is), but TypeScript does not! However, Bolt was built to allow users to give TypeScript some more information by using a generic parameter. Here's a part of the first example, modified to use the generic parameter.
import { BlockAction } from '#slack/bolt';
app.action<BlockAction>('modify', async ({ body, ack, client }) => {
// `body` is now typed as a BlockAction, and therefore body.trigger_id is a string
});
It's a pretty similar story for app.view() and generic parameters. This time, the body argument is of type ViewSubmitAction | ViewClosedAction. Using a clear response action doesn't make sense for a ViewClosedAction, so we need to constrain the type once again. That's right, the generic parameter is hooked up to more than just the type of body, it can actually change (constrain) any of the listener arguments! In this case, the generic parameter changes the type of ack().
import { ViewSubmitAction } from '#slack/bolt';
app.view<ViewSubmitAction>('modify_submission', async ({ body, ack, view }) => {
// No errors now
await ack({ response_action: 'clear' });
});
Final note: The way you wrote the view submission handler with a constraints object ({ type: 'view_submission', callback_id: 'yay' } does seem like you've given TypeScript enough information to constrain the types of the listener arguments. That actually would work for app.action({ type: 'block_actions', ... }, ...) because we defined ActionConstraints to be generic. This is an area where Bolt could be improved and all it would take is making ViewConstraints generic in the same way.
Related
I am facing some troubles with my GraphQL optimistic rendering. I'm using Apollo Client, such as:
const history = useHistory();
const [addItem] = useMutation(ADD_ITEM_QUERY);
const onClick = () => {
addItem({
variables: {
id: '16b1119a-9d96-4bc8-96a3-12df349a0c4d',
name: 'Foo Bar'
},
optimisticResponse: {
addItem {
id: '16b1119a-9d96-4bc8-96a3-12df349a0c4d',
name: 'Foo Bar',
__typename: 'Item'
}
},
update: () => {
// Update item cached queries
history.push(`/items);
}
});
};
The issue comes from the redirect. As far as I understand it, the update function is called twice: a first time with the optimisticResponse, and a second time with the network response (which should normally be the same).
However, let's consider the following scenario:
I create a new item,
I receive the optimistic response, and I'm redirected to the list of items,
I click on "Add an item" to be redirected to the form
I receive the server response, hence I'm redirected again to the list.
What is the correct way to prevent this double redirection?
I thought checking the current cache value. If the value is already the latest one, I won't apply the redirect. Not sure if there is a better way? How do you proceed such a scenario?
You should call history.push outside of the update function. You can do
addItem({...}).then(() => history.push('/items')).
We're trying to get some events/messages to post when a user exits a chatbot window (or the site) (or a welcome message), but so far the events are not firing.
I can see within Inspector tools:
Screen Shot 2020-02-18 at 3 15 39 PM
Various activities/conversations are created, the chatbot works, but no welcome/exit events are triggered.
The code we're using is nearly if not identical to documentation code here: https://github.com/microsoft/BotFramework-WebChat/blob/master/docs/WELCOME_MESSAGE.md
and here: How to handle user leaving conversation
I have a function that fires when the window is closed, as follows:
const store = window.WebChat.createStore( {}, ( { dispatch } ) => next => async action => {
return next( action );});
window.addEventListener( 'sendEventActivity', ( { data } ) => {
store.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'user_event',
value: {
name: 'end_conversation',
value: 'user ended conversation'
},
text: 'The user has left the conversation.'
}
})
});
function exitEvent(){
const eventSendActivity = new Event( 'sendEventActivity' );
eventSendActivity.data = 'User left conversation';
window.dispatchEvent( eventSendActivity );
console.log('Exit Event Submitted (hopefully)');
}
exitEvent();
I have tried other variations, defining the store earlier, above render chat, after render chat, sending welcome messages from various locations and at various times but can't seem to get it to send.
We are using https://cdn.botframework.com/botframework-webchat/latest/webchat.js
Any idea what the issue might be? Not sure where we are going wrong or why it's not firing - copying in theory known to be working code straight into our code doesn't seem to do the trick.
Thanks in advance and please let me know if I have failed to include any necessary details- new to chatbot and do not post much on github. Many thanks,
EDIT:
I was able to marry the aforementioned code and code from here: https://github.com/microsoft/BotFramework-WebChat/issues/2120#issuecomment-516056614 in order to achieve what I wanted. I'll post below in case it helps anyone else...
const store = window.WebChat.createStore({}, ({ dispatch }) => next => action => {
if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'webchat/join'
}
});
}
return next(action);
});
window.addEventListener( 'sendEventActivity', ( { data } ) => {
store.dispatch( {
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'webchat/exit'
}
} );
} );
document.getElementById("action_menu_btn").addEventListener( 'click', function() {
const eventSendActivity = new Event( 'sendEventActivity' );
eventSendActivity.data = 'User left conversation';
window.dispatchEvent( eventSendActivity );
console.log('End Converstaion Event Fired');
});
Cheers!
I failed to mention this in the other post (I'll update it), but the reason the code works is because of the window.onbeforeunload() function. Without it, the window closes before any code can finish executing. The result being no event is created, is caught by a listener, nor is sent via the Web Chat store to the bot.
Here, using the above, refreshing the page produces the "User left conversation" activity.
Also, something to note, any function you create and pass thru like you have with exitEvent() is going to run as soon as the page loads. Take the following code which gets the user's location via the browser (placed just before the closing </script> tag). As you can see, it's loading even before Web Chat. If you are wanting a function to run according to some activity passed from the bot, then utilize either the store's actions (i.e. DIRECT_LINE/INCOMING_ACTIVITY, or some other) or via the available middleware.
let geoLoc = async () => {
await navigator.geolocation.getCurrentPosition(position => {
console.log('Latitude: ', position.coords.latitude);
console.log('Longitude: ', position.coords.longitude);
});
}
geoLoc();
Regarding a welcome message, you have two options. Either send as an activity from your bot (reference this sample) or initiate an event on your page after some initial activity is received (reference this sample).
Lastly, I would recommend getting the code working as-is before tinkering with it. This usually trips me up, so thought I'd pass it along.
Hope of help!
I am trying to grasp how to return a promise from a Redux Action Creator.
I have read these two articles, but are none the wiser. Maybe I'm just daft. ;)
https://redux.js.org/advanced/middleware
https://medium.com/collaborne-engineering/returning-promises-from-redux-action-creators-3035f34fa74b
My component calls the action called startCreateNote()
The action calls the database API
The database operation finishes and resolves an object {newlyCreated-id, reduxStoreObject}
the Action Creator is dispatched
Now I wish to wait for the Redux Action Creator to resolve before proceeding
Show toaster after Action Creator has resolved
See code below.
//ADD_NOTE action generator
export const addNote = (id, note) => ({
type: ADD_NOTE,
id,
note
})
export const startAddNote = (noteData = {}) => {
return (dispatch, getState) => {
//Database API Instruction object
const apiInstruction = { action: DB_ACTION_ADD, payload: noteData, uid: getState().auth.uid }
//Call database API to insert Note into Database
return noteDatabaseAPI(apiInstruction)
.then(({id, reduxStoreObj}) => {
//The database API resolves id of newly created Note & reduxStore object
//THIS IS WHAT I WANT TO ACHIEVE!
dispatch(addNote(id, reduxStoreObj)).then(
toast("New note saved!")
).catch(
toast.error("ERROR - Note was not saved!")
)
}).catch( (err) => {
//This gets triggered with error message:
//database fail! - TypeError: "dispatch(...).then is not a function"
console.log('database fail! -', err)
})
}
}
How can I make my Action Creator return a promise?
From what I understand the (probably) easiest way to make the action creator return a promise is by using one of several asynchronous-operations libraries such as:
redux-saga
redux-thunk
redux-observable
redux-promise
redux-loop
redux-ship
redux-logic
redux-effects
redux-cycles
redux-side-effects
I found more information here: https://decembersoft.com/posts/what-is-the-right-way-to-do-asynchronous-operations-in-redux/#redux-saga
A clean and easy way to do what you're trying to do is via your reducer.
You could start with adding a isNoteSaved variable in your reducer as follows.
case ADD_NOTE_REQUEST:
return {
...state,
isNoteSaved: false
}
case ADD_NOTE_SUCCESS:
return {
...state,
isNoteSaved: true
}
case ADD_NOTE_FAILURE:
return {
...state,
error: action.payload,
isNoteSaved: false
}
Your isNoteSaved variable has an initial state of false.
Then in place of your promise, you could add something like this :
toast() && addNoteReducer.isNoteSaved
It means your function will be executed when isNoteSaved is true.
That's how I do it. Adding promises to action creators adds complexity and makes the code less readable.
In Cypress, it is well-documented that you can alias specific network requests, which you can then "wait" on. This is especially helpful if you want to do something in Cypress after a specific network request has fired and finished.
Example below from Cypress documentation:
cy.server()
cy.route('POST', '**/users').as('postUser') // ALIASING OCCURS HERE
cy.visit('/users')
cy.get('#first-name').type('Julius{enter}')
cy.wait('#postUser')
However, since I'm using GraphQL in my app, aliasing no longer becomes a straightforward affair. This is because all GraphQL queries share one endpoint /graphql.
Despite it not being possible to differentiate between different graphQL queries using the url endpoint alone, it is possible to differentiate graphQL queries using operationName (refer to following image).
Having dug through the documentation, there doesn't appear to be a way to alias graphQL endpoints using operationName from the request body. I'm also returning the operationName (yellow arrow) as a custom property in my response header; however, I haven't managed to find a way to use it to alias specific graphQL queries either.
FAILED METHOD 1: This method attempts to use the purple arrow shown in image.
cy.server();
cy.route({
method: 'POST',
url: '/graphql',
onResponse(reqObj) {
if (reqObj.request.body.operationName === 'editIpo') {
cy.wrap('editIpo').as('graphqlEditIpo');
}
},
});
cy.wait('#graphqlEditIpo');
This method doesn't work since the graphqlEditIpo alias is registered at runtime and as such, the error I receive is as follows.
CypressError: cy.wait() could not find a registered alias for: '#graphqlEditIpo'. Available aliases are: 'ipoInitial, graphql'.
FAILED METHOD 2: This method attempts to use the yellow arrow shown in image.
cy.server();
cy.route({
method: 'POST',
url: '/graphql',
headers: {
'operation-name': 'editIpo',
},
}).as('graphql');
cy.wait('graphql');
This method doesn't work because the headers property in the options object for cy.route is actually meant to accept response headers for stubbed routes per the docs. Here, I'm trying to use it to identify my specific graphQL query, which obviously won't work.
Which leads me to my question: How can I alias specific graphQL queries/mutations in Cypress? Have I missed something?
The intercept API introduced in 6.0.0 supports this via the request handler function. I used it in my code like so:
cy.intercept('POST', '/graphql', req => {
if (req.body.operationName === 'queryName') {
req.alias = 'queryName';
} else if (req.body.operationName === 'mutationName') {
req.alias = 'mutationName';
} else if (...) {
...
}
});
Where queryName and mutationName are the names of your GQL operations. You can add an additional condition for each request that you would like to alias. You can then wait for them like so:
// Wait on single request
cy.wait('#mutationName');
// Wait on multiple requests.
// Useful if several requests are fired at once, for example on page load.
cy.wait(['#queryName, #mutationName',...]);
The docs have a similar example here: https://docs.cypress.io/api/commands/intercept.html#Aliasing-individual-requests.
This works for me!
Cypress.Commands.add('waitForGraph', operationName => {
const GRAPH_URL = '/api/v2/graph/';
cy.route('POST', GRAPH_URL).as("graphqlRequest");
//This will capture every request
cy.wait('#graphqlRequest').then(({ request }) => {
// If the captured request doesn't match the operation name of your query
// it will wait again for the next one until it gets matched.
if (request.body.operationName !== operationName) {
return cy.waitForGraph(operationName)
}
})
})
Just remember to write your queries with unique names as posible, because the operation name relies on it.
If 'waiting' and not 'aliasing' in itself is the main purpose, the easiest way to do this, as I've encountered thus far, is by aliasing the general graphql requests and then making a recursive function call to 'wait' targeting the newly created alias until you find the specific graphql operation you were looking for.
e.g.
Cypress.Commands.add('waitFor', operationName => {
cy.wait('#graphqlRequest').then(({ request }) => {
if (request.body.operationName !== operationName) {
return cy.waitFor(operationName)
}
})
})
This of course have its caveats and may or may not work in your context. But it works for us.
I hope Cypress enables this in a less hacky way in the future.
PS. I want to give credit to where I got the inspiration to this from, but it seemt to be lost in cyberspace.
Since I was having the same issue and I did not find a real solution for this problem I combined different options and created a workaround that solves my problem. Hopefully this can help someone else too.
I do not really 'wait' for the request to be happen but I catch them all, based on **/graphql url and match the operationName in the request. On a match a function will be executed with the data as parameter. In this function the tests can be defined.
graphQLResponse.js
export const onGraphQLResponse = (resolvers, args) => {
resolvers.forEach((n) => {
const operationName = Object.keys(n).shift();
const nextFn = n[operationName];
if (args.request.body.operationName === operationName) {
handleGraphQLResponse(nextFn)(args.response)(operationName);
}
});
};
const handleGraphQLResponse = (next) => {
return (response) => {
const responseBody = Cypress._.get(response, "body");
return async (alias) => {
await Cypress.Blob.blobToBase64String(responseBody)
.then((blobResponse) => atob(blobResponse))
.then((jsonString) => JSON.parse(jsonString))
.then((jsonResponse) => {
Cypress.log({
name: "wait blob",
displayName: `Wait ${alias}`,
consoleProps: () => {
return jsonResponse.data;
}
}).end();
return jsonResponse.data;
})
.then((data) => {
next(data);
});
};
};
};
In a test file
Bind an array with objects where the key is the operationName and the value is the resolve function.
import { onGraphQLResponse } from "./util/graphQLResponse";
describe("Foo and Bar", function() {
it("Should be able to test GraphQL response data", () => {
cy.server();
cy.route({
method: "POST",
url: "**/graphql",
onResponse: onGraphQLResponse.bind(null, [
{"some operationName": testResponse},
{"some other operationName": testOtherResponse}
])
}).as("graphql");
cy.visit("");
function testResponse(result) {
const foo = result.foo;
expect(foo.label).to.equal("Foo label");
}
function testOtherResponse(result) {
const bar = result.bar;
expect(bar.label).to.equal("Bar label");
}
});
}
Credits
Used the blob command from glebbahmutov.com
This is what you're looking for (New in Cypress 5.6.0):
cy.route2('POST', '/graphql', (req) => {
if (req.body.includes('operationName')) {
req.alias = 'gqlMutation'
}
})
// assert that a matching request has been made
cy.wait('#gqlMutation')
Documentation:
https://docs.cypress.io/api/commands/route2.html#Waiting-on-a-request
I hope that this helps!
I used some of these code examples but had to change it slightly to add the onRequest param to the cy.route and also add the date.Now (could add any auto incrementer, open to other solutions on this) to allow multiple calls to the same GraphQL operation name in the same test. Thanks for pointing me in the right direction!
Cypress.Commands.add('waitForGraph', (operationName) => {
const now = Date.now()
let operationNameFromRequest
cy.route({
method: 'POST',
url: '**graphql',
onRequest: (xhr) => {
operationNameFromRequest = xhr.request.body.operationName
},
}).as(`graphqlRequest${now}`)
//This will capture every request
cy.wait(`#graphqlRequest${now}`).then(({ xhr }) => {
// If the captured request doesn't match the operation name of your query
// it will wait again for the next one until it gets matched.
if (operationNameFromRequest !== operationName) {
return cy.waitForGraph(operationName)
}
})
})
to use:
cy.waitForGraph('QueryAllOrganizations').then((xhr) => { ...
This is how I managed to differentiate each GraphQL request. We use cypress-cucumber-preprocessor so we have a common.js file in /cypress/integration/common/ where we can call a before and beforeEach hook which are called before any feature file.
I tried the solutions here, but couldn't come up with something stable since, in our application, many GraphQL requests are triggered at the same time for some actions.
I ended up storing every GraphQL requests in a global object called graphql_accumulator with a timestamp for each occurence.
It was then easier to manage individual request with cypress command should.
common.js:
beforeEach(() => {
for (const query in graphql_accumulator) {
delete graphql_accumulator[query];
}
cy.server();
cy.route({
method: 'POST',
url: '**/graphql',
onResponse(xhr) {
const queryName = xhr.requestBody.get('query').trim().split(/[({ ]/)[1];
if (!(queryName in graphql_accumulator)) graphql_accumulator[queryName] = [];
graphql_accumulator[queryName].push({timeStamp: nowStamp('HHmmssSS'), data: xhr.responseBody.data})
}
});
});
I have to extract the queryName from the FormData since we don't have (yet) the key operationName in the request header, but this would be where you would use this key.
commands.js
Cypress.Commands.add('waitGraphQL', {prevSubject:false}, (queryName) => {
Cypress.log({
displayName: 'wait gql',
consoleProps() {
return {
'graphQL Accumulator': graphql_accumulator
}
}
});
const timeMark = nowStamp('HHmmssSS');
cy.wrap(graphql_accumulator, {log:false}).should('have.property', queryName)
.and("satisfy", responses => responses.some(response => response['timeStamp'] >= timeMark));
});
It's also important to allow cypress to manage GraphQL requests by adding these settings in /cypress/support/index.js:
Cypress.on('window:before:load', win => {
// unfilters incoming GraphQL requests in cypress so we can see them in the UI
// and track them with cy.server; cy.route
win.fetch = null;
win.Blob = null; // Avoid Blob format for GraphQL responses
});
I use it like this:
cy.waitGraphQL('QueryChannelConfigs');
cy.get(button_edit_market).click();
cy.waitGraphQL will wait for the latest target request, the one that will be stored after the call.
Hope this helps.
Somewhere else this method was suggested.
Btw it all becomes a bit easier once you migrate to Cypress v5.x and make use of the new route (route2) method.
Our use case involved multiple GraphQL calls on one page. We had to use a modified version of the responses from above:
Cypress.Commands.add('createGql', operation => {
cy.route({
method: 'POST',
url: '**/graphql',
}).as(operation);
});
Cypress.Commands.add('waitForGql', (operation, nextOperation) => {
cy.wait(`#${operation}`).then(({ request }) => {
if (request.body.operationName !== operation) {
return cy.waitForGql(operation);
}
cy.route({
method: 'POST',
url: '**/graphql',
}).as(nextOperation || 'gqlRequest');
});
});
The issue is that ALL GraphQL requests share the same URL, so once you create a cy.route() for one GraphQL query, Cypress will match all the following GraphQL queries to that. After it matches, we set cy.route() to just a default label of gqlRequest or the next query.
Our test:
cy.get(someSelector)
.should('be.visible')
.type(someText)
.createGql('gqlOperation1')
.waitForGql('gqlOperation1', 'gqlOperation2') // Create next cy.route() for the next query, or it won't match
.get(someSelector2)
.should('be.visible')
.click();
cy.waitForGql('gqlOperation2')
.get(someSelector3)
.should('be.visible')
.click();
I have a problem with redux trying to load initial data with an asynchronous call to my backend API that returns a JSON. Right now, I'm trying to load a bunch of different articles, but I have no idea how to load it asynchronously. Since it is an initial set of data, should I load it synchronously? If yes, then how would I acheive a synchronous call to my API? If not, how would I go about solving this problem asynchronously?
Right now, I have static json data, data/articles.js that creates a default state in store.js.
Thanks
You should use a redux-thunk middleware, which allows you to dispatch async actions and a fetch library (for example) for downloading your initial data.
So:
1) create an action which fetch your data, example:
export function fetchData() {
const options = {
method: 'GET',
headers: {
'Authorization': 'Client-ID xx' // if theres any needed
}
}
return (dispatch) => {
return fetch('yourUrl.json', options)
.then(response => response.json())
.then(data => dispatch(receiveYourData(data)))
.catch(err => console.log(err));
}
}
receiveYourData is a action which will place your data in your state, example:
export function receiveYourData (payload = []) {
return {
type: RECEIVE_DATA,
payload: payload
}
}
Of course you have to write action handler, which after dispatching an action, will place your data in your state.
If you have your setup (similar to above), you should dispatch fetchData in your componentDidMount lifecycle method (its one of the option of course :) ).
If you dont know how to do particular parts, you can refer to this Example.
Also official async example may be helpful :)
I also had this problem. It turned out that you have to add a lot of code for this simple task. So I simplified this process and created a package for async loading of initial state in redux - redux-async-initial-state.
You can check out examples and in your case in the end your store creator will look like this:
// Load initial state function
const loadStore = () => {
return Promise(resolve => {
fetch('/data/articles.js')
.then(response => response.json())
.then(resolve);
});
}
const storeCreator = applyMiddleware(asyncInitialState.middleware(loadStore));
const store = storeCreator(reducer);