Reducer checking action.type with indexOf - redux

I wrote a redux reducer that helps me group error messages from other reducers.
I'm wondering if this could have any side effect since I don't see anyone doing it. I'm also wondering if there is a better way to do it that I couldn't think of.
This is what I wrote:
const errors = (state = {}, action = {}) => {
let new_state = Object.assign({}, state);
// if action type contains ERROR and action error is present
if (action.type.indexOf("ERROR") != "-1" && action.error) {
let error_id = Utils.hashCode(action.error);
// if error already in the array
if (new_state[error_id]) {
new_state[error_id].count++;
}
// otherwise add the message to the list
else {
new_state[error_id] = {message: action.error, count: 1};
}
}
// regular switch stmt
switch (action.type) {
case ERRORS_RESET: new_state = {}; break;
}
return new_state;
}
My store looks something like this now:
{
reducer1: {
something: [],
error: "Some error message",
},
reducer2: {
something: [],
error: false,
},
reducer3: {
some_other_obj: {},
error: "Another error message",
},
errors: [
{message: "Some error message, count: 1}
{message: "Another error message", count: 2}
]
}

The overall concept of listening for "SOMETHING_ERROR" actions is fine, but there's a couple issues with your implementation.
First, your if statement has a direct mutation of the existing state. Per the Structuring Reducers - Immutable Update Patterns section of the Redux docs, you need to make sure you copy every level of nesting. Right now you're copying the first level of the state, but not the nested object.
Second, you're always copying the state, even when there's nothing that's actually changed. That's generally going to cause unnecessary re-renders in your UI.

Related

Redux Toolkit - assign entire array to state

How can I assign an entire array to my intialState object using RTK?
Doing state = payload or state = [...state, ...payload] doesn't update anything.
Example:
const slice = createSlice({
name: 'usersLikedPosts',
initialState: [],
reducers: {
getUsersLikedPosts: (state, { payload }) => {
if (payload.length > 0) {
state = payload
}
},
},
})
payload looks like this:
[
0: {
uid: '1',
title: 'testpost'
}
]
update
Doing this works but I don't know if this is a correct approach. Can anyone comment?
payload.forEach((item) => state.push(item))
immer can only observe modifications to the object that was initially passed into your function through the state argument. It is not possible to observe from outside the function if that variable was reassigned, as it only exists in the scope within the function.
You can, however, just return a new value instead of modifying the old one, if you like that better. (And in this case, it is probably a bit more performant than doing a bunch of .push calls)
So
return [...state, ...payload]
should do what you want.

How do I pass a new key as an array with immer?

Say I have some initial state like
const initialState = {
loading: false,
updating: false,
saving: false,
data: {},
error: null
};
And I want add to data as the result of an action but the data I want to add is going to be an array. How do I go about this?
I've tried
export default produce((draft, action) => {
switch (action.type) {
case UPDATE_STATE.SUCCESS:
draft.data.new_Array.push(action.payload);
draft.loading = false;
break;
default:
}
}, initialState);
but this errors.
If I start the initial state as
const initialState = {
loading: false,
updating: false,
saving: false,
data: {
newArray: []
},
error: null
};
any update to the state before I make the array key overwrites the initial state and removes the key. ie
export default produce((draft, action) => {
switch (action.type) {
case OTHER_UPDATE_STATE.SUCCESS:
draft.data = payload.action;
draft.loading = false;
break;
default:
}
}, initialState);
can anyone help?
There's one thing I noticed you'll run into trouble, draft.data.new_Array.push(action.payload);
Make sure you don't modify the existing data structure. The reason is that redux relies on the memory reference of an object, if the object reference doesn't change, it might fool the redux that nothing actually happened in the past.
In your case, i have a feeling that nothing will ever get triggered.
One way to modify the reference is to create an new object, ex.
return [...data, newElementObject]

(DynamoDB) ConditionExpression behaves unpredictably with an '=' operator. How could I debug it better?

My objects in Dynamodb look roughly like this:
{
userId: "GEFOeE8EsaWmq4NQ3oh7tbeVkLx1",
url: 'objectURL',
object: {}
}
I have this simple piece of code for deleting an object, when the user that owns the object requests a delete. The user argument here is a parsed JWT, by the way.
export async function deleteObject(user, url) {
let params = {
TableName: OBJECTS_TABLE,
Key: {
url: url,
},
ConditionExpression: `userId = :uid`,
ExpressionAttributeValues: {
":uid": {
S: user.sub
}
}
};
let deleteResult = await dynamoDb.delete(params).promise();
return deleteResult;
}
The problem is that it doesn't work, and I've made sure that the problem stems from the ConditionExpression by changing = to <>. I simply get this:
ConditionalCheckFailedException: The conditional request failed
I'm sure solving the problem wouldn't be difficult, but I barely have any information
Questions:
Why is the condition expression failing? Everything looks alright, and it should work. Right?
How could I debug this issue better?
The await/async is not supported by AWS SDK at the moment. Please refer this similar issue.
The SDK currently relies on CLS to trace the call context. It doesn't
work with async/await functionality right now. You can see the
discussion here.
It should work if you remove the await. Example below:-
let deleteResult = dynamodb.deleteItem(params).promise();
deleteResult.then(function (data) {
console.error("Delete item result :", JSON.stringify(data,
null, 2));
}).catch(function (err) {
console.error("Delete item result error :", JSON.stringify(err,
null, 2));
});
I figured it out. ExpressionAttributeValues can be directed used, without mentioning the datatype. The Javascript SDK does that automatically.
export async function deleteObject(user, url) {
let params = {
TableName: OBJECTS_TABLE,
Key: {
url: url,
},
ConditionExpression: `userId = :uid`,
ExpressionAttributeValues: {
":uid": user.sub
}
};
let deleteResult = await dynamoDb.delete(params).promise();
return deleteResult;
}

seamless-immutable is not updating state consistently

I'm working with seamless-immutable and redux, and I'm getting a strange error when updating the state. Here's my code, without the bits like the action return or combineReducers. Just the junk that's running/causing the error.
Initial State
{
things: {
fetching: false,
rows: []
}
}
Action Handler
export default {
[DEALERS_REQUEST]: (state, action) => {
return Immutable({ ...state, fetching: true });
},
[DEALERS_RECEIVE]: (state, action) => {
return Immutable({ ...state, rows: action.payload, fetching: false });
},
Middleware with thunk
export const thingsFetch = (data) => {
return (dispatch, getState) => {
dispatch(thingsRequest());
dispatch(thingsReceive(data));
}
}
Now, what's weird is, if I run these two actions together, everything is fine.
If I only dispatch thingsRequest(), I get a "cannot push to immutable object" error.
I've tried using methods like set, update, replace, merge, but they usually return with "this.merge is not a function".
Am I doing something wrong procedurally or should I contact the module dev to report an issue?
This issue on this was that, on the case of an empty array, the component was trying to write back to the Immutable object with it's own error message.
To get around this, I pass the prop as mutable. There's also some redux-immutable modules that replace the traditional connect function to all the app to pass mutable props to components while maintaining immutability in the state.

redux-promise with Axios, and how do deal with errors?

So, I see on an error, redux-promise hands me back error: true, along with the payload, but that is once it hits the reducer... to me, decoupling the request AND error condition is a bit odd, and seems inappropriate. What is an effective way to also deal with error condition when using axios w/ reduc-promise (middleware).. here is the gist of what i have..
in action/
const request = axios(SOME_URL);
return {
type: GET_ME_STUFF,
payload: request
}
in reducer/
const startState = {
whatever: [],
error: false
}
case GET_ME_STUFF:
return {...state, startState, {stuff:action.payload.data, error: action.error? true : false}}
etc... then I can deal with the error.. so, my api call is now split into two seperate areas and that seems wrong.... there must be something I am missing here. I would think in the /actions I can pass in a callback that handles a new action etc.. or something, but not split it.
I've had to go through a similar situation. The challenge is that you likely won't be able to evaluate the results of the promise until it is at the reducer. You could handle your exceptions there but it's not the best pattern. From what I've read reducers are meant only to return appropriate pieces of state based on action.type and do nothing else.
So, enter an additional middleware, redux-thunk. Instead of returning an object, it returns a function, and it can coexist with promise.
It's explained quite well at http://danmaz74.me/2015/08/19/from-flux-to-redux-async-actions-the-easy-way/ [archived here]. Essentially, you can evaluate the promise here and dispatch through the other action creators before the promise result hits the reducers.
In your actions file, add additional action creators that would handle the success and error (and any other) states.
function getStuffSuccess(response) {
return {
type: GET_ME_STUFF_SUCCESS,
payload: response
}
}
function getStuffError(err) {
return {
type: GET_ME_STUFF_ERROR,
payload: err
}
}
export function getStuff() {
return function(dispatch) {
axios.get(SOME_URL)
.then((response) => {
dispatch(getStuffSuccess(response))
})
.catch((err) => {
dispatch(getStuffError(err))
})
}
}
return null
This is roughly to how you might translate your pseudocode to what is explained at the link. This handles evaluating the promise directly in your action creator and firing off the appropriate actions and payloads to your reducers which follows the convention of action -> reducer -> state -> component update cycle. I'm still pretty new to React/Redux myself but I hope this helps.
The accepted answer doesn't make use of redux-promise. Since the question is actually about handling errors using redux-promise I provide another answer.
In the reducer you should inspect the existence of the error attribute on the action object:
// This is the reducer
export default function(previousState = null, action) {
if (action.error) {
action.type = 'HANDLE_XHR_ERROR'; // change the type
}
switch(action.type) {
...
And change the type of the action, triggering a state change for an error handling component that you have set up for this.
You can read a bit more about it here on github.
It looks like you can catch the error where you make the dispatch, then make an separate error dispatch if it happens. It's a bit of a hack but it works.
store.dispatch (function (dispatch) {
dispatch ({
type:'FOO',
payload:axios.get(url)
})
.catch (function(err) {
dispatch ({
type:"FOO" + "_REJECTED",
payload:err
});
});
});
and in the reducer
const reducer = (state=initialState, action) => {
switch (action.type) {
case "FOO_PENDING": {
return {...state, fetching: true};
}
case "FOO_REJECTED": {
return {...state, fetching: false, error: action.payload};
}
case "FOO_FULFILLED": {
return {
...state,
fetching: false,
fetched: true,
data: action.payload,
};
}
}
return state;
};
Still using redux-promises you can do something like this which I think is an elegant way to deal with this problem.
First, set a property in the redux state that will hold any ajax errors that may occurred.
ajaxError: {},
Second, setup a reducer to handle ajax errors:
export default function ajaxErrorsReducer(state = initialState.ajaxError, action) {
if (action.error) {
const { response } = action.payload;
return {
status: response.status,
statusText: response.statusText,
message: response.data.message,
stack: response.data.stack,
};
}
return state;
}
Finally, create a very simple react component that will render errors if there are any (I am using the react-s-alert library to show nice alerts):
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import Alert from 'react-s-alert';
class AjaxErrorsHandler extends Component {
constructor(props, context) {
super(props, context);
this.STATUS_GATE_WAY_TIMEOUT = 504;
this.STATUS_SERVICE_UNAVAILABLE = 503;
}
componentWillReceiveProps(nextProps) {
if (this.props.ajaxError !== nextProps.ajaxError) {
this.showErrors(nextProps.ajaxError);
}
}
showErrors(ajaxError) {
if (!ajaxError.status) {
return;
}
Alert.error(this.getErrorComponent(ajaxError), {
position: 'top-right',
effect: 'jelly',
timeout: 'none',
});
}
getErrorComponent(ajaxError) {
let customMessage;
if (
ajaxError.status === this.STATUS_GATE_WAY_TIMEOUT ||
ajaxError.status === this.STATUS_SERVICE_UNAVAILABLE
) {
customMessage = 'The server is unavailable. It will be restored very shortly';
}
return (
<div>
<h3>{ajaxError.statusText}</h3>
<h5>{customMessage ? customMessage : ajaxError.message}</h5>
</div>
);
}
render() {
return (
<div />
);
}
}
AjaxErrorsHandler.defaultProps = {
ajaxError: {},
};
AjaxErrorsHandler.propTypes = {
ajaxError: PropTypes.object.isRequired,
};
function mapStateToProps(reduxState) {
return {
ajaxError: reduxState.ajaxError,
};
}
export default connect(mapStateToProps, null)(AjaxErrorsHandler);
You can include this component in your App component.
This might not be the best approach but it works for me. I pass the 'this' of my component as var context. Then when i get response back i just execute the methods defined in my components context. In my component i have successHdl and errorHdl. From there i can trigger more redux actions as normal. I checked all the previous answers and seem too daunting for such a trivial task.
export function updateJob(payload, context){
const request = axios.put(UPDATE_SOMETHING, payload).then(function (response) {
context.successHdl(response);
})
.catch(function (error) {
context.errorHdl(error);
});;
return {
type: UPDATE_SOMETHING,
payload: payload,
}
}
Don't use redux-promise. It overcomplicates something that's actually super simple to do yourself.
Instead read the redux docs: http://redux.js.org/docs/advanced/AsyncActions.html
It'll give you a much better understanding of how to handle this kind of interactions and you'll learn how to write something (better than) redux-promise yourself.

Resources