I'm making an update request on my React Native app to my Firebase with redux in mind.
Here's my redux snippet
export function buyTicket(eventID) {
const { currentUser } = firebase.auth();
return (dispatch) => {
firebase.database().ref(`/Users/${currentUser.uid}/joinedEvent`).update({ [eventID]: true })
.then(() => dispatch({ type: BUY_TICKET_SUCCESS }))
.catch(() => dispatch({ type: BUY_TICKET_FAIL }));
};
};
When the buyTicket function gets called, only the then() method should be expected but both then() and catch() got called.
According to the Firebase docs, update() produces a promise but its optional.
Here's the error I'm getting
This is my reducer
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case PULL_EVENT_DATA:
return action.payload;
case PULL_TRENDING_DATA:
return action.payload;
case BUY_TICKET_SUCCESS:
return {
message: 'Yay, see you there!'
}
case BUY_TICKET_FAIL:
return {
message: 'shit'
}
default:
return state;
}
}
Perhaps the console log might gives a clue?
As Jan pointed out the part I was missing, I was able to relocate the error and make it right.
The error I was making is mutability. I did not consider the concept of mutability when making the app, hence the error.
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case PULL_EVENT_DATA:
return action.payload;
case PULL_TRENDING_DATA:
return action.payload;
case BUY_TICKET_SUCCESS:
return { ...state, message: action.payload}
case BUY_TICKET_FAIL:
return { ...state, message: action.payload}
default:
return state;
}
}
You may find more about mutability here
Related
Say for example when a login function calls an API and it returns an error because of something like invalid credentials. I have noticed that it still goes through the fulfilled case in the extra reducers part. Should I add an if statement to check if response code is 200 or is there a way for the thunk to go through the rejected case?
extraReducers: builder => {
builder.addCase(login.pending, (state, action) => {
state.fetchingError = null;
state.fetchingUser = true;
});
builder.addCase(login.fulfilled, (state, {payload}) => {
console.log(payload, 'hello?');
state.user = payload.data.user;
});
builder.addCase(login.rejected, (state, action) => {
state.fetchingUser = false;
state.fetchingError = action.error;
});
},
You can use rejectWithValue in createAsyncThunk to customize the reject action.
It also takes an argument which will be "action.payload" in the reject action.
In createAsyncThunk:
const updateUser = createAsyncThunk(
'users/update',
async (userData, { rejectWithValue }) => {
const { id, ...fields } = userData
try {
const response = await userAPI.updateById(id, fields)
return response.data.user
} catch (err) {
// Use `err.response.data` as `action.payload` for a `rejected` action,
// by explicitly returning it using the `rejectWithValue()` utility
return rejectWithValue(err.response.data)
}
}
)
https://redux-toolkit.js.org/api/createAsyncThunk#handling-thunk-errors
in my React component, I have this:
componentDidMount() {
const {
dispatchGetCompanies,
dispatchGetDocumentTopics,
dispatchGetDataForCompanyTopicList,
dispatchGetDocumentTypes,
dispatchRequestComplete,
dispatchRequestInProgress,
dispatchRequestError,
} = this.props;
dispatchRequestInProgress()
Promise.all([
dispatchGetCompanies(),
dispatchGetDocumentTopics(),
dispatchGetDocumentTypes(),
dispatchGetDataForCompanyTopicList(),
]).then(() => {
dispatchRequestComplete();
}).catch((error) => {
dispatchRequestError(error);
});
}
const mapDispatchToProps = function (dispatch) {
return {
dispatchGetCompanies: (...args) => dispatch(getCompanies(...args)),
dispatchGetDocumentTopics: (...args) => dispatch(getDocumentTopics(...args)),
dispatchGetDataForCompanyTopicList: (...args) => dispatch(getDataForCompanyTopicList(...args)),
dispatchDeleteCompanyTopic: (...args) => dispatch(deleteCompanyTopic(...args)),
dispatchGetDocumentTypes: (...args) => dispatch(getDocumentTypes(...args)),
dispatchRequestInProgress: (...args) => dispatch(requestInProgress(...args)),
dispatchRequestComplete: (...args) => dispatch(requestComplete(...args)),
dispatchRequestError: (...args) => dispatch(requestError(...args)),
};
};
The action for getDocumentTypes is this:
export function getDocumentTypes() {
return function (dispatch, getState, api) {
return api.get('/document-type-metadata')
.then(({data}) => dispatch(getDocumentTypesSuccess(data)));
};
}
export function getDocumentTypesSuccess(data) {
return {
type: GET_DOCUMENT_TYPES,
payload: data,
};
}
Reducer code is this:
function setDocumentTypes(state, action) {
const {
documentTypeMetadata
} = action.payload;
return state.merge({
documentTypes: List(documentTypeMetadata),
_metadata: Map({
isFetching: false,
}),
});
}
export default function (state = Map({
id: null,
companyTopics: List(),
companies: List(),
topics: List(),
documentTypes: List(),
_metadata: Map({
isFetching: false,
error: false,
}),
}), action) {
switch (action.type) {
case SECTION_IDENTIFICATION_REQUEST_COMPLETE:
return setRequestComplete(state, action);
case SECTION_IDENTIFICATION_REQUEST_IN_PROGRESS:
return setInProgress(state, action);
case SECTION_IDENTIFICATION_REQUEST_ERROR:
return setError(state, action);
case GET_COMPANIES:
return setCompanies(state, action);
case GET_DOCUMENT_TOPICS:
return setTopics(state, action);
case GET_COMPANY_TOPIC_LIST_DATA:
return setCompanyTopicListData(state, action);
case GET_DOCUMENT_TYPES:
return setDocumentTypes(state, action);
default:
return state;
}
}
The dispatchRequestError is similar; it sets the "error" object in the Redux store to whatever error is returned.
The problem I am having is that if the api call to getDocumentTypes returns back an error, I would expect the dispatchRequestError to immediately execute, with all the other promises (dispatchGetDocumentTopics, etc) being rejected. However, after the error is set, the other promises inside the Promise.all() still resolve, causing the error property to become undefined, which causes the error message that I'm showing on the page to appear, then quickly disappear.
What is going wrong?
Thank you
I send a number of files and some other data via redux and axios to my backend. However, when I do so, I get the following error message Unhandled Rejection (TypeError): state.story is not iterable. Although the error occurs in my react js fronted, the data is successfully sent to my backend. When i send normal key vlaue pairs and no array it also works fine.
I now wonder how i can make state.story iterable? Or more general how to approach that issue?
// creating the form data
onClickUpload (e){
let files = this.state.image;
let formData = new FormData();
formData.append('title', "Some_Title")
for (var i = 0; i < files.length; i++) {
formData.append(`story_media[${i}]isTitlePicture`, files[i].isTitlePicture)
formData.append(`story_files[${i}]files`, files[i].file)
}
this.props.addStory(formData);
}
// ADD Story Action
export const addStory = formDataStory => (dispatch, getState) =>
new Promise((resolve, reject) => {
axios.post(apiBase +"/story/createupdatedestroy/", formDataStory, tokenConfig(getState) )
.then(res => {
dispatch(createMessage({ addStory: "Story Added" }));
dispatch({
type: ADD_STORY,
payload: res.data
});
resolve(res);
})
.catch(err => {
reject(err);
dispatch(returnErrors(err.response.data, err.response.status))
}
);
// Add STory Reducer
const initialState = {
story: [],
isFetching: "idle"
};
export default function (state = initialState, action) {
switch (action.type) {
case GET_STORY_REQUEST:
return {
...state,
isFetching: "loading"
};
case GET_STORY_SUCCESS:
return {
...state,
story: action.payload,
isFetching: "success"
};
case GET_STORY_FAILURE:
return {
isFetching: "failure"
};
case GET_SINGLE_STORY:
return {
...state,
story: state.story.filter(story => story.id !== action.payload)
};
case DELETE_STORY:
return {
...state,
story: state.story.filter(story => story.id !== action.payload)
};
case EDIT_STORY:
return {
...state,
story: state.story.filter(story => story.id !== action.payload)
};
case ADD_STORY:
return {
...state,
story: [...state.story, action.payload]
};
default:
return state;
}
}
Issue
One of your reducer cases is not copying the state object correctly. It neglects to copy current state into the new state object.
case GET_STORY_FAILURE:
return {
isFetching: "failure"
};
Solution
Copy existing state into the new state object.
case GET_STORY_FAILURE:
return {
...state,
isFetching: "failure"
};
I am trying to understand how redux middleware works, during my experiment I have noticed that dispatching an action from redux middleware may result in an unexpected behavior.
I will try to explain the problem by simulating file upload as follow:
we have 3 actions:
const setProgress = (progress) => ({ type: SET_UPLOAD_PROGRESS, progress });
const setThumbnail = (thumbnail) => ({ type: SET_THUMBNAIL, thumbnail });
const calculateTotal = () => ({ type: CALCULATE_TOTAL });
Middleware to calculate total:
export const testMiddleware = (store) => (next) => (action) => {
if (action.type === 'CALCULATE_TOTAL') {
return next(action);
}
const result = next(action);
store.dispatch(calculateTotal());
return result;
};
Reducer:
const initialState = {
progress: 0,
total: 0,
thumbnail: ''
};
export function uploadReducer(state = initialState, action) {
switch (action.type) {
case SET_UPLOAD_PROGRESS:
state.progress = action.progress;
return { ...state };
case SET_THUMBNAIL:
state.thumbnail = action.thumbnail;
return { ...state };
case CALCULATE_TOTAL:
state.total += state.progress * 5;
return { ...state };
default:
return state;
}
}
here is the code for simulating file upload:
let cnt = 0;
// simulate upload progress
const setNewProgress = () => {
cnt += 2;
if (cnt > 5) return;
setTimeout(() => {
store.dispatch(setProgress(cnt * 2));
setNewProgress();
}, 1000);
};
setNewProgress();
// simulate thumbnail generating
setTimeout(() => {
store.dispatch(setThumbnail('blob:http://thumbnail.jpg'));
}, 2500);
Here is the sequence of events:
the first action works as intended and sets the progress value:
the problem starts from here; thumbnail suppose to be set by 'setThumbnail' but devtools shows that it has been set by 'calculateTotal', and every dispatch after that is mismatched:
What am I doing wrong here? is it by design? how can I dispatch an action in middleware without causing above problem?
This unexpected behavior may be cause by your uploadReducer not being pure, i.e. it is directly operating on your state (e.g. state.progress = action.progress;). Reducers should only return the new state and not modify existing state injected into your reducer by redux. hence, your reducer needs to look like this:
export function uploadReducer(state = initialState, action) {
switch (action.type) {
case SET_UPLOAD_PROGRESS:
return { ...state, progress: action.progress };
case SET_THUMBNAIL:
return { ...state, thumbnail: action.thumbnail };
case CALCULATE_TOTAL:
return { ...state, total: state.total + state.progress * 5 };
default:
return state;
}
}
how can I dispatch an action in middleware without causing above problem?
Your middleware looks fine (you are correctly preventing recursion and also returning the next() result (which is not needed in your example but still makes sense in a real application). Your actions look good as well (a style remark: You could wrap your action's payload in a payload property, which is a common convention).
In short, I'm trying to plot markers on a map using react-native-maps.
I've gone as far as creating an action to fetch the coordinates and respective ID from the server (see code below).
export const getPlacesOnMap = () => {
return dispatch => {
dispatch(authGetToken())
.then(token => {
return fetch("myApp?auth=" + token);
})
.catch(() => {
alert("No valid token found!");
})
.then(res => {
if (res.ok) {
return res.json();
} else {
throw(new Error());
}
})
.then(parsedRes => {
const places = [];
for (let key in parsedRes) {
places.push({
// ...parsedRes[key], // this fetches all the data
latitude: parsedRes[key].location.latitude,
longitude: parsedRes[key].location.longitude,
id: key
});
} console.log(places)
dispatch(mapPlaces(places));
})
.catch(err => {
alert("Oops! Something went wrong, sorry! :/");
console.log(err);
});
};
};
export const mapPlaces = places => {
return {
type: MAP_PLACES,
places: places
};
};
I don't know if I'm using the right words, but I've essentially tested the code (above) using componentWillMount(), and it successfully returned multiple coordinates as an array of objects.
Now, the problem is I don't know what to do next. As much as I understand, I know the end goal is to create a setState(). But I don't know how to get there.
Would be a great help if someone can point me in the right direction.
You need to create an async action. You can dispatch different actions inside an async action based on whether the async function inside it is resolved or rejected.
export function getPlacesOnMap(token) {
return async function(dispatch) {
dispatch({
type: "FETCHING_PLACES_PENDING"
});
fetch("myApp?auth=" + token)
.then(res => {
dispatch({
type: "FETCHING_PLACES_FULFILLED",
payload: res.json()
});
})
.catch(error => {
dispatch({
type: "FETCHING_PLACES_REJECTED",
payload: error
});
});
};
}
If your authGetToken() function is also a promise, you need to dispatch this action after authGetToken() was resolved.
You can use the action.payload in your "FETCHING_PLACES_FULFILLED" case of your reducer(s) to be able to use the retrieved data.
UPDATE
Your reducer should be like this:
export default function reducer(
state = {
loadingMarkers : false,
markers : [],
error : null,
},
action
) {
switch (action.type) {
case "FETCHING_PLACES_PENDING":
return { ...state, loadingMarkers: true };
case "FETCHING_PLACES_FULFILLED":
return { ...state, loadingMarkers: false, markers: action.payload};
case "FETCHING_PLACES_REJECTED":
return { ...state, loadingMarkers: false, error: action.payload };
default:
return state;
}
}
Now you can connect your component to redux and use your markers when they are fetched.
have a look at this example and connect docs