I am deleteing an object then immediately retrieving a list of available objects, and am hitting a race-esk problem.
DELETE requests are subject to a CORS Pre-Flight OPTIONS request, while the GET request is not. This means my intended
DELETE /things/21
GET /things/
becomes:
OPTIONS /things/21
GET /things
DELETE /things/21
And the result of the GET includes object 21.
I want to avoid adding artificial delays; is there any other way to ensure the DELETE happens first?
(The requests are triggered from completely different components of my react app)
Edit:
I have a component Things which renders a summary list of things, and a component Thing which renders a page of detail.
Thing includes a Delete button, which fires a delete and navigates to Things. By "fires a delete" I mean: triggers an ajax DELETE to /things/21 and deletes thing 21 from my local redux store.
Things has a componentWillMount which triggers a GET to retrieve the list of available things, when they arrive my redux reducer adds them all to its store.
Edit: example:
Redux action creators
export const deleteThing = thingId => ({
type: 'DELETE_THING',
payload: thingId
});
export const retrieveThings = () => ({
type: 'FETCH_THINGS'
});
"Reducer" responsible for API requests
export default store => next => action => {
switch (action.type) {
case 'DELETE_THING':
return deleteObj(store.dispatch, action.type, `/things/{action.payload}`, action.payload);
case 'FETCH_THINGS':
return getObj(store.dispatch, action.type, '/things/');
}
}
const getObj = (dispatch, action, url) => {
return sendRequest(dispatch, () => fetch(url)
.then(processResponse(dispatch, action))
.catch(handleError(dispatch, action))
);
};
const deleteObj = (dispatch, action, url, payload) => {
return sendRequest(dispatch, () => fetch(url, {
method: 'DELETE',
headers
})
.then(results => {
if (results.status >= 400) return handleError(dispatch, action)(results);
// DELETE doesn't return anything but reducers want to know what was deleted, so pass the payload
return dispatch({
type: `${action}_SUCCESS`,
payload
});
})
.catch(handleError(dispatch, action))
);
}
// Wraps a fetch request, triggering redux events on send/receive (regardless of outcome)
const sendRequest = (dispatch, fn) => {
dispatch({type: 'SENDING_REQUEST'});
const always = () => dispatch({type: 'RECEIVED_RESPONSE'});
return fn().then(always, always);
}
Reducer/store for Things
export default (state = initialState, action) => {
case 'DELETE_THING_SUCCESS':
return state.deleteIn(['byId'], action.payload);
}
React Components
class Thing {
render () {
return (
<div>
<h1>{props.thing.id}</h1>
<button onClick={this.props.deleteThing}>Delete</button>
</div>
);
}
deleteThing () {
this.props.triggerActionToSend
// Pretend this is `connect`ed to a redux store
this.props.deleteThing(this.props.id);
// AJAX to delete from server
fetch({
url: '/thing/' + this.props.id,
method: 'delete'
});
// Redirect to main list
this.props.navigate('/things/');
}
}
// Pretend this is a `connect`ed component that receives a `things` prop from a redux store
class Things {
componentWillMount () {
this.props.retrieveThings();
}
render () {
return (
<ul>
{this.props.things.map(x => <li>x.id</li>)}
</ul>
);
}
);
const App = () => (
<div>
<Route path="/things" component={Things} />
<Route path="/thing/:thingId" component={Thing} />
</div>
);
Change your navigation call to wait for fetch promise to resolve. Should probably do same with deleting from local store but that isn't the issue at hand
// AJAX to delete from server
fetch({
url: '/thing/' + this.props.id,
method: 'delete'
}).then(_=> this.props.navigate('/things/'));
Related
I use react 18.2.0, redux 4.2.0 and redux thunk
I want to wait until my dispatch is done, so I can take the data it got from the server and render a component with that data
I try to do the following in my component:
import store from '../redux/store';
const featureInfoA = useSelector(featureInfo)
const featureErrorA = useSelector(featureError)
const featureStatusA = useSelector(featureStatus)
const clickFeatureFromList = (id) => {
//start dispatching
dispatch(fetchFeatureInfoById({ id, category }))
.then((e) => {
console.log('BINGO THEN ', featureInfoA, featureErrorA, featureStatusA);
})
}
//check store when a specific state changed and then get its data and render a component
store.subscribe(() => {
const state = store.getState()
if (state.features.status == 'succeeded' || state.features.status == 'failed') {
console.log('BINGO SUBSCRIBE ', featureInfoA);
accordionDetailsRootGlobal.render(<ContextInfo featureData={featureInfoA} />);
}
})
The issue is that in my console I see:
BINGO SUBSCRIBE a multiple times
and then the DATA from store that is inside the store
and then the BINGO THEN a null idle that is in the then after calling dispatch
I think that the log of the store should be first
Why I see "BINGO SUBSCRIBE a" that is inside the store.subscribe multiple times?
Why I never see the final data that came from the server? "BINGO THEN a null idle" contains the initial state, even though "DATA from store" brought back data.
By the way, this my store, for features
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit'
const FEATURE_URL = 'http://localhost:3500/map'
//status : idle, loading, succeeded, failed
const initialState = {
info:'a',
status:'idle',
error:null
}
export const fetchFeatureInfoById = createAsyncThunk('feature/fetchInfoById', (originalData) => {
console.log('fetchFeatureInfoById originalData ', originalData);
fetch(FEATURE_URL + '/feature/' + originalData.id + '/' + originalData.category)
.then((response) => response.json())
.then((data) => {
console.log('DATA from store ', data);
return data;
})
.catch((error) => {
return error.message;
});
})
export const featuresSlice = createSlice({
name: 'features',
initialState,
reducers: {
featureInfoById: {
reducer(state, action) {
//do something with the id
console.log('feature state ', ' -- state : ',state.status, ' -- action : ', action);
},
prepare(category, id) {
return{
payload:{
category,
id
}
}
}
}
},
extraReducers(builder) {
builder
.addCase(fetchFeatureInfoById.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchFeatureInfoById.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
.addCase(fetchFeatureInfoById.fulfilled, (state, action) => {
console.log('fulfilled data store ', state, action);
state.status = 'succeeded'
state.info = action.palyload
})
}
})
export const featureInfo = (state) => state.features.info;
export const featureError = (state) => state.features.error;
export const featureStatus = (state) => state.features.status;
export const {featureInfoById} = featuresSlice.actions
export default featuresSlice.reducer
I just want to get the data after the dispatch is done, to render a component with them. Please help me understand what I am doing wrong and how can I fix that.
Why I see "BINGO SUBSCRIBE a" that is inside the store.subscribe
multiple times?
The docs mention:
Adds a change listener. It will be called any time an action is
dispatched, and some part of the state tree may potentially have
changed.
So basically each time any action is dispatched and any part of the state tree might have been changed it will be triggered. This refers to your entire store (not just featuresSlice).
Another potential reason is that you are subscribing each time the component is mounted, but never unsubscribing (so you might have multiple subscriptions at the same time). If you really want to keep this, a fix would be to subscribe inside an useEffect unsubscribe on unmount (something like):
useEffect(() => {
const unsubscribeFn = store.subscribe(() => {...})
return unsubscribeFn
}
Why I never see the final data that came from the server? "BINGO THEN
a null idle" contains the initial state, even though "DATA from store"
brought back data.
This is most likely because your fetchFeatureInfoById is not returning anything (please note that the return data; part is inside a callback, but the main function doesn't return anything) so in your extra reducer action.payload has no value.
So what you want to do in order to be able to properly set the slice state from the slice extra reducer is returning the fetch result from the fetchFeatureInfoById function. So basically your function would look something like:
export const fetchFeatureInfoById = createAsyncThunk('feature/fetchInfoById', (originalData) => {
console.log('fetchFeatureInfoById originalData ', originalData);
// Add the return keyword before the fetch from the next line
return fetch(FEATURE_URL + '/feature/' + originalData.id + '/' + originalData.category)
.then((response) => response.json())
.then((data) => {
console.log('DATA from store ', data);
return data;
})
.catch((error) => {
return error.message;
});
})
PS: Without a reproducible example is not 100% this will fix all your problems, but it should at the very least point you in the right direction.
Extra suggestions:
(related to first answer): I think you can at least use an useEffect? It would be way more efficient to only trigger the an effect when accordionDetailsRootGlobal changes or featureInfoA (so basically have an useEffect with this 2 dependencies and render the ContextInfo when one of this 2 change, rather than rendering on every store update). Something like:
useEffect(() => {
if (featureInfoA)
accordionDetailsRootGlobal.render(<ContextInfo featureData={featureInfoA} />)
}, [accordionDetailsRootGlobal, featureInfoA])
inside fetchFeatureInfoById on the catch you don't throw any error. This will prevent .addCase(fetchFeatureInfoById.rejected ... from being called (since, even if your fetch fails, the function won't throw any error). So you should probably remove the catch part, or throw an error inside it.
You don't need to do the store.subscribe thing.
The selector is supposed to update and it will cause a render of your component. So, initially featureInfoA is undefined but after the promise is resolved it will update the store which will trigger a render in your component
import store from '../redux/store';
const featureInfoA = useSelector(featureInfo)
const featureErrorA = useSelector(featureError)
const featureStatusA = useSelector(featureStatus)
const clickFeatureFromList =(id) => {
//start dispatching
dispatch(fetchFeatureInfoById({id, category}))
}
// add some logic here to check if featureInfoA has value
if(featureErrorA) return <div>Error</div>
if(featureStatusA === "loading") return <div>Loading...</div>
if(featureInfoA) return <div>{featureInfoA}</div>
I Think this might be a better approach to deal with dispatch when you want to observe your data after you request successfully handled
const clickFeatureFromList =async (id) => {
//start dispatching
const result = await dispatch(fetchFeatureInfoById({ id, category }))
if (fetchFeatureInfoById.fulfilled.match(result)) {
const {payload} = result
console.log('BINGO THEN ', featureInfoA, featureErrorA, featureStatusA);
}
}
Here I am setting a flag as true(initially flag=false) and I want that flag in another reducer and stop and untill the flag is true
filterReduer.js
const filterReducer = (state = intialState, action) => {
switch(action..type){
case actionTypes.FILTER_SUCCESS:
return {
...state,
filtereData: action.data.messages,
flag: true,
};
default state;
}
}
OtherAction.js
export const mySolutions = (userId) => {
return async (dispatch, getState) => {
let flag = await getState().filter.flag; // I am getting flag info from different reducer
let data = getState().filter.channelMetaData;
console.log("data", data);
dispatch(mySolutionStarts());
}
}
My flag is false unable to wait untill flag is true
My intenton is when flag is set true the api call and data is updated to state and where I can use the state info for further call but unable to wait
Timeout is not a good idea where flag vaue is updated based on api call
is there any different approcah ?
First, in case you pasted your code here, you should change
switch(action..type){
// ...
filtereData: action.data.messages,
// ...
}
to
switch(action.type){
// ...
filterData: action.data.messages,
// ...
}
As for dispatching an action on successful API response: I have to assume a lot, but let's hope that you have an initial action with the API call. If so, you should simply dispatch the second action within the same function that dispatches the first. Something like:
const myAsyncAction = () => async (dispatch, getState) => {
dispatch({ type: FIRST_ACTION }); // perhaps you want to set the flag here
// async API call here
if (apiRes.status === 200) { // if successful response
dispatch({ type: SECOND_ACTION }) // set flag again
}
}
As far as I know, you can dispatch as many different actions as you like in there. Don't forget to import the actions if needed.
I don't know what does cause this, it sends new request almost every half a second. I was thinking it's because I call my action in render method but it's not, tried to call it in componentDidMount, the same result.
Here is the code:
Action:
export const getComments = () => dispatch => {
dispatch({
type: GET_COMMENTS
})
fetch(`${API_URL}/comments`,
{ method: 'GET', headers: {
'content-type': 'application/json'
}})
.then((res) => res.json())
.then((data) => dispatch({
type: GET_COMMENTS_SUCCESS,
payload: data
}))
.catch((err) => dispatch({
type: GET_COMMENTS_FAILED,
payload: err
}))
}
Since I need post id loaded before I call the comment action I put it in render method:
componentDidMount() {
const { match: { params }, post} = this.props
this.props.getPost(params.id);
}
render() {
const { post, comments } = this.props;
{post && this.props.getComments()}
return <div>
...
Here is the route:
router.get("/comments", (req, res) => {
Comment.find({})
.populate("author")
.exec((err, comments) => {
if (err) throw err;
else {
res.json(comments);
}
});
});
Your getComments() function is running during render. The dispatch used in the action is causing a re-render, causing getComments() to fire again, producing an infinite loop.
Instead of fetching comments in the render() function, you should instead fetch them in the componentDidMount lifecycle hook, then in the render function simply display the comments from props;
getComments() is invoking the http request, so it should be moved to componentDidMount lifecycle hoook.
This should work:
componentDidMount() {
const { match: { params } = this.props
this.props.getPost(params.id);
this.props.getComments()
}
render() {
const { post, comments } = this.props;
{post && comments}
return <div>
...
When the component has mounted, the params are retrieved from props.match and the Post and Comments are fetched. Then with redux, post and comments data is dispatched, and can be accessed in the connected component's render method.
I'm trying to use redux thunk to make asynchronous actions and it is updating my store the way I want it to. I have data for my charts within this.props by using:
const mapStateToProps = (state, ownProps) => {
const key = state.key ? state.key : {}
const data = state.data[key] ? state.data[key] : {}
return {
data,
key
}
}
export default connect(mapStateToProps)(LineChart)
Where data is an object within the store and each time I make an XHR call to get another piece of data it goes into the store.
This is the the async and the sync action
export function addData (key, data, payload) {
return {
type: ADD_DATA,
key,
data,
payload
}
}
export function getData (key, payload) {
return function (dispatch, getState) {
services.getData(payload)
.then(response => {
dispatch(addData(key, response.data, payload))
})
.catch(error => {
console.error('axios error', error)
})
}
}
And the reducer:
const addData = (state, action) => {
const key = action.key
const data = action.data.results
const payload = action.payload
return {
...state,
payload,
key,
data: {
...state.data,
[key]: data
}
}
}
In the tutorial I have been following along with (code on github), this seems like enough that when a piece of data that already exists within the store, at say like, data['some-key'] redux will not request the data again. I'm not entirely sure on how it's prevented but in the course, it is. I however am definitely making network calls again for keys that already exist in my store
What is the way to prevent XHR for data that already exists in my store?
Redux itself does nothing about requesting data, or only requesting it if it's not cached. However, you can write logic for that. Here's a fake-ish example of what a typical "only request data if not cached" thunk might look like:
function fetchDataIfNeeded(someId) {
return (dispatch, getState) => {
const state = getState();
const items = selectItems(state);
if(!dataExists(items, someId)) {
fetchData(someId)
.then(response => {
dispatch(loadData(response.data));
});
}
}
}
I also have a gist with examples of common thunk patterns that you might find useful.
if I have an async action with api call, which could either be an action returns a function:
export function asyncAction(itemId) {
return (dispatch) => {
dispatch(requestStarted());
return sendRequest(itemId).then(
(result) => dispatch(requestSuccess()),
(error) => dispatch(requestFail())
);
};
}
or one returns an object and uses middleware to intercept it and do stuff:
export function asyncAction(itemId) {
return {
type: [ITEM_REQUEST, ITEM_REQUEST_SUCCESS, ITEM_REQUEST_FAILURE],
promise: sendRequest(itemId),
userId
};
}
// same middleware found in https://github.com/rackt/redux/issues/99
export default function promiseMiddleware() {
return (next) => (action) => {
const { promise, ...rest } = action;
if (!promise) {
return next(action);
}
next({ ...rest, readyState: 'request' );
return promise.then(
(result) => next({ ...rest, result, readyState: 'success' }),
(error) => next({ ...rest, error, readyState: 'failure' })
);
};
}
Now my question is: How do I rollback to the state before the asyncAction is dispatched, which essentially means two steps back in the state(success/failure => request) w/ an api call to undo last api call.
For example, after delete a todo item(which is an async action), a popup snackbar shows with an undo option, after click it the deleted todo item will be added back to UI along with an api call to add it back to db.
I've tried redux-undo but I feel it's not intended to solve problems like this.
Or I should forget about 'undo' and just dispatch a brand new addTodo action when user clicks undo option?
Thanks in advance :-)
Redux Optimist might be what you need.