I have a async action
function fetch(url) {
dispatch => {
request(url).then(
resp => dispatch({type: FETCH_SUCCESS, resp}),
error => dispatch({type: FETCH_FALUIRE, error})
);
}
}
UI has a page to list all items, when click one of them, it dispatchs a action with url of that item.
when I click item1 first, then I click item2 quickly, if the response of item2 returns earlier than item1. the data of item2 in store will be overwritten by item1.
fetch('http://remote/items/item1'); //first
fetch('http://remote/items/item2'); // then
How can I ignore the result of the item1? or how can I make dispatch of item always trigger after item1
what I can think of is, dispatch the url before send request.
function fetch(url) {
dispatch => {
dispatch({type: FETCH_START, url});
request(url).then(
resp => dispatch({type: FETCH_SUCCESS, resp, url}),
error => dispatch({type: FETCH_FALUIRE, error, url})
);
}
}
in the reducer, I check the url to decide if I should ignore the response.
function reduceFetch(state = initialState, action) {
switch(action.type) {
case FETCH_START:
return {initialState, action.url};
case FETCH_SUCCESS:
if (state.uri === action.url) {
return {state, resp: action.resp};
} else {
return state;
}
...
}
}
Thanks for your help!
Your solution should work. Atleast if you ignore something crazy like
Request item1
Request item2
Request item1 again.
Third request is comming back.
First request failed. Or first request is comming back and item1 was changed in between the requests.
Alternatively you could make your fetch function to dispatch events only on the latest call.
const fetchLatest = (called => url => {
const snapshot = ++called
return dispatch => request(url).then(
resp => snapshot === called && dispatch(...),
error => snapshot === called && dispatch(...)
)
})(0)
Related
I'm currently using redux / redux-thunk to fetch a user using api-sauce like so
let authToken = await AsyncStorage.getItem('#TSQ:auth_token')
if (authToken) {
store.dispatch(fetchUser(authToken))
console.log('show login screen')
// dont worry, if the token is invalid, just send us to onboarding (api determines this)
loggedInView()
} else {
Onboarding ()
}
....
export const fetchUser = authToken => async dispatch => {
console.log('dispatching auth token')
console.log('here goes request')
let res = await api.get(`/auth/${authToken}`);
if (res.ok) {
console.log('have the user')
dispatch(
setUser(res.data)
)
} else {
dispatch({
type: 'SET_USER_DEFAULT'
})
}
}
When this code is ran, the user is still loading and the console.logs are not in order
`dispatching auth token`
`here goes request`
`show login screen`
Why is this happening?
This is because the actual call to store.dispatch(fetchUser(authToken)) is synchronous - the dispatch() method is not asynchronous, so the logging "show login screen" will occur immediately after execution of the fetchUser() method.
If you want loggedInView() to be executed after a response is returned from your network request (ie the call to the async method api.get()), then you could consider refactoring your code in the following way:
if (authToken) {
store.dispatch(fetchUser(authToken))
// Remove navigation from here
} else {
Onboarding ()
}
And then:
export const fetchUser = authToken => async dispatch => {
console.log('dispatching auth token')
console.log('here goes request')
let res = await api.get(`/auth/${authToken}`);
if (res.ok) {
console.log('have the user')
// Occurs after network request is complete
console.log('show login screen')
// Add navigation here to go to logged in view now that request is complete
loggedInView()
dispatch(
setUser(res.data)
)
} else {
dispatch({
type: 'SET_USER_DEFAULT'
})
}
Hope this helps!
I changed the signature of an action creator to make a call to getState before trying to dispatch fetch, but now fetch is not getting called.
StartingPoint: I have an async action that makes an api call using fetch and then dispatches a success or error action once it's done, as below. I call this action from a container like so and it works fine:
dispatch(actions.getData()); //from a container
export function getData(){
return (dispatch : any) => {
return fetch(
'http://localhost:8000/api',{}
).then(
response => response.json()
).then(
json => dispatch(successAction(json)),
err => dispatch(notify("SERVER_ERROR"))
);
}
}
The problem is that I need to call getState in the action, so that I can have an option about which port to query. Therefore, I changed the getData action to what you see below. However, when I call the action creator like this dispatch(actions.getData());, it's not making a network call, although the console.log statement is running.
Question: how can the getData function be written to allow for making a call to getState before running the fetch? (and, related, what is the purpose of wrapping it in the dispatch return)?
export const getData = () => (dispatch: any, getState: any) => {
let state = getState();
let url = //code omitted - getting port from state object
console.log("this log statement runs");
return (dispatch : any) => {
return fetch(
url,{}
).then(
response => response.json()
).then(
json => dispatch(successAction(json)),
err => dispatch(notify("SERVER_ERROR"))
);
}
}
added Promise support
const addPromiseSupportToDispatch = (store: any) => {
const rawDispatch = store.dispatch;
return (action: any) => {
if (typeof action.then === 'function') {
return action.then(rawDispatch);
}
return rawDispatch(action);
};
};
store.dispatch = addPromiseSupportToDispatch(store);
You inserted an additional return I think. This should be the right code block
export const getData = () => (dispatch: any, getState: any) => {
let state = getState();
let url = //code omitted - getting port from state object
console.log("this log statement runs");
return fetch(
url,{}
).then(
response => response.json()
).then(
json => dispatch(successAction(json)),
err => dispatch(notify("SERVER_ERROR"))
);
}
EDIT
If I had to use your original code:
export function getData(){
return (dispatch : any, getState: any) => { // <= second parameter provided by redux-thunk
let url = getState().url; //can call getState here
return fetch(
'http://localhost:8000/api',{}
).then(
response => response.json()
).then(
json => dispatch(successAction(json)),
err => dispatch(notify("SERVER_ERROR"))
);
}
}
I have an action, that uses a redux thunk, that looks like so:
export function fetchData(query) {
return dispatch => {
return fetch(`http://myapi?query=${query}` ,{mode: 'cors'})
.then(response => response.json())
.then(json => { dispatch(someOtherAction(json)) })
}
}
}
and then my someOtherAction actually updates state:
export function someOtherAction(data) {
return {
action: types.SOME_ACTION,
data
}
}
But i want it to be possible for the fetchData action creator to be reusable so that different parts of my app can fetch data from myapi and then have different parts of the state based on that.
I'm wondering what is the best way to reuse this action? Is it acceptable to pass a second parameter in to my fetchData action creator that stipulates which action is called on a successful fetch:
export function fetchData(query, nextAction) {
return dispatch => {
return fetch(`http://myapi?query=${query}` ,{mode: 'cors'})
.then(response => response.json())
.then(json => { dispatch(nextAction(json)) })
}
}
}
Or is there an accepted way of doing this sort of thing?
I use a middleware for that. I have defined the fetch call in there, then in my actions I send the URL to fetch and the actions to dispatch when completed. This would be a typical fetch action:
const POSTS_LOAD = 'myapp/POST_L';
const POST_SUCCESS = 'myapp/POST_S';
const POST_FAIL = 'myapp/POST_F';
export function fetchLatestPosts(page) {
return {
actions: [POSTS_LOAD, POST_SUCCESS, POST_FAIL],
promise: {
url: '/some/path/to/posts',
params: { ... },
headers: { ... },
},
};
}
When calling that action, the POST_LOAD action will be dispatch automatically by the middleware just before the fetch request it's executed. If everything goes well the POST_SUCCESS action will be dispatched with the json response, if something goes wrong the POST_FAIL action will be dispatched by the middleware.
All the magic it's in the middleware! And it's something similar to this:
export default function fetchMiddleware() {
return ({ dispatch, getState }) => {
return next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
const { promise, actions, ...rest } = action;
if (!promise) {
return next(action);
}
const [REQUEST, SUCCESS, FAILURE] = actions;
next({ ...rest, type: REQUEST }); // <-- dispatch the LOAD action
const actionPromise = fetch(promise.url, promise); // <-- Make sure to add the domain
actionPromise
.then(response => response.json())
.then(json => next({ ...rest, json, type: SUCCESS })) // <-- Dispatch the success action
.catch(error => next({ ...rest, error, type: FAILURE })); // <-- Dispatch the failure action
return actionPromise;
};
};
}
This way I have all my requests on a single place and I can define the actions to run after the request it's completed.
------------EDIT----------------
In order to get the data on the reducer, you need to use the action name you defined on the original action creator. The following example shows how to handle the POST_SUCCESS action from the middleware to get the posts data from the json response.
export function reducer(state = {}, action) {
switch(action.type) {
case POST_SUCCESS: // <-- Action name
return {
...state,
posts: action.json.posts, // <-- Getting the data from the action
}
default:
return state;
}
}
I hope this helps!
I'm very bad when it comes to thinking of a title question, sorry for that.
My Problem:
I'm unit testing my async redux actions like it's suggested in the docs. I mock the API calls with nock and check for the dispatched actions with redux-mock-store. It works great so far, but I have one test that fails even though it clearly does work. The dispatched action neither does show up in the array returned by store.getActions() nor is the state changed in store.getState(). I'm sure that it does happen because I can see it when I test manually and observe it with Redux Dev Tools.
The only thing that is different in this action dispatch is that it is called in a promise in a catch of another promise. (I know that sounds confusing, just look at the code!)
What my code looks like:
The action:
export const login = (email, password) => {
return dispatch => {
dispatch(requestSession());
return httpPost(sessionUrl, {
session: {
email,
password
}
})
.then(data => {
dispatch(setUser(data.user));
dispatch(push('/admin'));
})
.catch(error => {
error.response.json()
.then(data => {
dispatch(setError(data.error))
})
});
};
}
This httpPost method is just a wrapper around fetch that throws if the status code is not in the 200-299 range and already parses the json to an object if it doesn't fail. I can add it here if it seems relevant, but I don't want to make this longer then it already is.
The action that doesn't show up is dispatch(setError(data.error)).
The test:
it('should create a SET_SESSION_ERROR action', () => {
nock(/example\.com/)
.post(sessionPath, {
session: {
email: fakeUser.email,
password: ''
}
})
.reply(422, {
error: "Invalid email or password"
})
const store = mockStore({
session: {
isFetching: false,
user: null,
error: null
}
});
return store.dispatch(actions.login(
fakeUser.email,
""))
.then(() => {
expect(store.getActions()).toInclude({
type: 'SET_SESSION_ERROR',
error: 'Invalid email or password'
})
})
});
Thanks for even reading.
Edit:
The setErroraction:
const setError = (error) => ({
type: 'SET_SESSION_ERROR',
error,
});
The httpPostmethod:
export const httpPost = (url, data) => (
fetch(url, {
method: 'POST',
headers: createHeaders(),
body: JSON.stringify(data),
})
.then(checkStatus)
.then(response => response.json())
);
const checkStatus = (response) => {
if (response.status >= 200 && response.status < 300) {
return response;
}
const error = new Error(response.statusText);
error.response = response;
throw error;
};
Because of you are using nested async function in catch method - you need to return the promise:
.catch(error => {
return error.response.json()
.then(data => {
dispatch(setError(data.error))
})
});
Otherwise, dispatch will be called after your assertion.
See primitive examples:
https://jsfiddle.net/d5fynntw/ - Without returning
https://jsfiddle.net/9b1z73xs/ - With returning
How to cancel a HTTPRequest in Angular 2?
I know how to reject the request promise only.
return new Promise((resolve, reject) => {
this.currentLoading.set(url, {resolve, reject});
this.http.get(url, {headers: reqHeaders})
.subscribe(
(res) => {
res = res.json();
this.currentLoading.delete(url);
this.cache.set(url, res);
resolve(res);
}
);
});
You can use the following simple solution:
if ( this.subscription ) {
this.subscription.unsubscribe();
}
this.subscription = this.http.get( 'awesomeApi' )
.subscribe((res)=> {
// your awesome code..
})
You can call unsubscribe
let sub = this.http.get(url, {headers: reqHeaders})
.subscribe(
(res) => {
res = res.json();
this.currentLoading.delete(url);
this.cache.set(url, res);
resolve(res);
}
);
sub.unsubscribe();
More info here: http://www.syntaxsuccess.com/viewarticle/angular-2.0-and-http
You can use SwitchMap on the observable which will cancel any previous request's responses and only request the latest:
https://www.learnrxjs.io/operators/transformation/switchmap.html
A little late for the party, but here is my take:
import { Injectable } from '#angular/core'
import { Http } from '#angular/http'
import { Observable } from 'rxjs/Observable'
import { Subscriber } from 'rxjs/Subscriber'
#Injectable ()
export class SomeHttpServiceService {
private subscriber: Subscriber<any>
constructor(private http: Http){ }
public cancelableRequest() {
let o = new Observable(obs => subscriber = obs)
return this.http.get('someurl').takeUntil(o)
.toPromise() //I dont like observables
.then(res => {
o.unsubscribe
return res
})
}
public cancelRequest() {
subscriber.error('whatever')
}
}
This allows you to manually cancel a request. I sometimes end up with an observable or promise that will make changes to a result on the page. If the request was initiated automatically (user didn't type anyting in a field for x millis) being able to abort the request is nice (user is suddenly typing something again)...
takeUntil should also work with a simple timeout (Observable.timer) if that is what you are looking for
https://www.learnrxjs.io/learn-rxjs/operators/filtering/takeuntil
Use switchMap [docs], which will cancel all in-flight requests and use only the latest.
get(endpoint: string): Observable<any> {
const headers: Observable<{url: string, headers: HttpHeaders}> = this.getConfig();
return headers.pipe(
switchMap(obj => this.http.get(`${obj.url}${endpoint}`, { headers: obj.headers, params: params }) ),
shareReplay(1)
);
}
shareReplay will emit the latest value for any late subscribers.
This is a great thread, and I have a little more info to provide. I have an API call that could potentially go on for a very long time. So I needed the previous request to cancel with a timeout. I just figured out today that I can add a timeout operator to the pipe function. Once the timeout completes its count, that will cancel the previous HTTP request.
Example...
return this.exampleHttpRequest()
.pipe(
timeout(3000),
catchError(err => console.log(error)
)