Redux overwrites model with previous state - angularjs

I am currently making a sample project in AngularJs combined with Redux.
I am struggling to get the mappings from the reducer working.
I have a simple input where users can set a new name together with a drop down to select a 'company'.
<input type="text" ng-model="$ctrl.single.object.name">
<select ng-change="$ctrl.getProperties()"
ng-options="option.description as option.description for option in $ctrl.list.all"
ng-model="$ctrl.single.object.company">
When the user changes the company, new properties need to be fetched in order for the user to set these properties.
function FooController($ngRedux, FooActions, BarActions) {
this.$onInit = function () {
this.unsubscribeCompanies = $ngRedux.connect(this.mapStateToThis, BarActions)(this);
this.fetchCompanyList();
};
this.$onDestroy = function () {
this.unsubscribeCompanies();
};
this.fetchCompanyList = function () {
this.fetchCompanies().payload.then((response) => {
this.fetchCompaniesSuccess(response.data);
}, (error) => {
this.fetchCompaniesError(error.data);
});
};
this.getProperties = function () {
this.fetchCompanyProperties(this.single.object.company).payload.then((response) => {
this.fetchCompanyPropertiesSuccess(response.data);
}, (error) => {
this.fetchCompanyPropertiesError(error.data);
});
};
this.mapStateToThis = function (state) {
return {
list: state.bar.list,
single: state.bar.single
};
};
}
module.exports = {
template: require('./index.html'),
controller: ['$ngRedux', 'FooActions', 'BarActions', FooController]
}
The problem I get is that the name and the selected company are overwritten with empty values when the fetch for properties is successful. I get why the values are overwritten with empty values and I have found a way to get it working.
export const GET_COMPANIES = 'GET_COMPANIES';
export const GET_COMPANIES_SUCCESS = 'GET_COMPANIES_SUCCESS';
export const GET_COMPANIES_ERROR = 'GET_COMPANIES_ERROR';
export const GET_COMPANIES_PROPERTIES = 'GET_COMPANIES_PROPERTIES';
export const GET_COMPANIES_PROPERTIES_SUCCESS = 'GET_COMPANIES_PROPERTIES_SUCCESS';
export const GET_COMPANIES_PROPERTIES_ERROR = 'GET_COMPANIES_PROPERTIES_ERROR';
export default function BarActions($http) {
function fetchCompanies() {
return {
type: GET_COMPANIES,
payload: $http.get('api/companies')
};
}
function fetchCompaniesSuccess(companies) {
return {
type: GET_COMPANIES_SUCCESS,
payload: companies
};
}
function fetchCompaniesError(error) {
return {
type: GET_COMPANIES_ERROR,
payload: error
};
}
function fetchCompanyProperties(company) {
return {
type: GET_COMPANIES_PROPERTIES,
payload: $http.get(`api/company/${company}/properties`)
};
}
function fetchCompanyPropertiesSuccess(properties) {
return {
type: GET_COMPANIES_PROPERTIES_SUCCESS,
payload: properties
};
}
function fetchCompanyPropertiesError(error) {
return {
type: GET_COMPANIES_PROPERTIES_ERROR,
payload: error
};
}
return {
fetchCompanies,
fetchCompaniesSuccess,
fetchCompaniesError,
fetchCompanyProperties,
fetchCompanyPropertiesSuccess,
fetchCompanyPropertiesError
}
}
The way I overwrite the values in the reducer is as follows:
import { GET_COMPANIES, GET_COMPANIES_SUCCESS, GET_COMPANIES_ERROR, GET_COMPANIES_PROPERTIES, GET_COMPANIES_PROPERTIES_ERROR, GET_COMPANIES_PROPERTIES_SUCCESS } from "../actions/bar.actions";
const all = [];
const initialState = {
list: {
all,
filtered: all,
error: null,
loading: false
},
single: {
object: {},
error: null,
loading: false
}
};
export function BarReducer(state = initialState, action) {
switch (action.type) {
case GET_COMPANIES:
return { ...state, list: { all: [], filtered: [], error: null, loading: true } };
case GET_COMPANIES_SUCCESS:
return { ...state, list: { all: action.payload, filtered: action.payload, error: null, loading: false } };
case GET_COMPANIES_ERROR:
return { ...state, list: { all: [], filtered: [], error: action.payload.innerException, loading: false } };
case GET_COMPANIES_PROPERTIES:
return { ...state, single: { ...state.single, object: { ...state.single.object }, error: null, loading: true } };
case GET_COMPANIES_PROPERTIES_SUCCESS:
return { ...state, single: { ...state.single, object: { ...state.single.object, payloadValues: action.payload }, error: null, loading: false } };
case GET_COMPANIES_PROPERTIES_ERROR:
return { ...state, single: { object: null, error: action.payload.innerException, loading: false } };
default:
return state;
}
}
The way I now use the spread operator in order to overwrite the old state feels dirty. I was wondering if there are any rules or guidelines to handle this issue. So far I have searched a while on internet and in specific the Redux website but I did not come cross any other solutions.

The breakage is likely due to the structure of the reducer. It is concerned with too many different parts of state and has to operate on deep nested objects, making it easy to accidentally mutate state. The guidelines for reducer structure say that splitting reducer state into normalized slices is the best way to go.
Try splitting your one reducer into multiple smaller reducers. For example:
export const all = (initialAll = [], { type, companies }) => {
switch(type) {
case GET_COMPANIES_SUCCESS: return companies;
default: return initialAll;
}
}
export const error = (initialError = '', { type, error }) => {
switch(type) {
case GET_COMPANIES_ERROR: return error;
default: return initialError;
}
}
export const isFetching = (isFetching = false, { type }) => {
switch(type) {
case GET_COMPANIES: return true;
case GET_COMPANIES_SUCCESS: return false;
case GET_COMPANIES_ERROR: return false;
default: return isFetching;
}
}
Then, compose them into one reducer:
import { combineReducers } from 'redux';
export list = combineReducers({
all,
error,
isFetching
});
// ...
export rootReducer = combineReducers({
list,
single,
// ...
})
This way, each reducer is concerned with only one thing or set of things, and its reduction handlers can do simple operations on single-level state instead of complex operations on deep nested state.
Also, in your list substate, it looks like you are storing the same type of collection resources in both all and filtered with potential overlap. This leads to multiple sources of truth for the same data, which opens the door to data inconsistency. Instead, keep an array of filteredIds:
export const filteredIds = (initialIds = [], { type, filteredIds }) => {
switch(type) {
case SET_FILTERED_IDS: return filteredIds;
default: return initialIds;
}
}
Then, use a selector that filters all by the filteredIds to get your filtered items.

One option is to use Immutable, which would change your reducers to:
case GET_COMPANIES:
return state.setIn(['list', 'loading'], true);
// etc
See Using Immutable.JS with Redux for more information about this approach.
Another option is to use Lodash, as shown in this Issue, you can define the following function to make it similar to the immutable one:
import {clone, setWith, curry} from 'lodash/fp';
export const setIn = curry((path, value, obj) =>
setWith(clone, path, value, clone(obj)),
);
Then you can use setIn as follow:
case GET_COMPANIES:
return setIn(['list', 'loading'], true, state);
// etc
The Lodash approach is just working with plain object, so it might be easier to understand than Immutable.

Related

How to access the updated value in my store immediately after updating it?

I have a React application that is currently using Redux for state management.
What I am trying to achieve: Click a Buy Now button - dispatch a action that makes a request to the server to add the item (increment the cart item count based on server response), check the state to see if the cart item count is greater than 0 & do something if it is.
For some reason, I have to click the button twice in order for the cartItemCount to reflect 1?
My current implementation looks like the below (I have tried to pull out all the unrelated code due to the file being quite large):
CourseSpecificScreen.tsx
const mapStateToProps = (state: RootState) => {
return {
courseSpecificReducer: state.courseSpecificReducer,
authState: state.authReducer,
currencyState: state.currencyReducer,
cartReducer: state.cartReducer,
courseCategoriesState: state.courseCategoriesReducer,
};
};
const mapDispatchTopProps = (dispatch: Dispatch<AnyAction>) => {
return bindActionCreators(ActionCreators, dispatch);
};
const connector = connect(mapStateToProps, mapDispatchTopProps);
type CourseSpecificScreenNavigationProp = CompositeNavigationProp<
StackNavigationProp<ExploreRouteStackParamList, "CourseSpecificScreen">,
CompositeNavigationProp<
StackNavigationProp<AppRouteHeaderParamList>,
StackNavigationProp<AuthRouteStackParamList>
>
>;
type CourseSpecificScreenRouteProp = RouteProp<
ExploreRouteStackParamList,
"CourseSpecificScreen"
>;
type Props = PropsFromRedux & {
navigation: CourseSpecificScreenNavigationProp;
route: CourseSpecificScreenRouteProp;
};
type State = {
cartItemCount: number;
};
class CourseSpecificScreen extends Component<Props, State> {
pruchaseItem = async () => {
const {
courseSpecificReducer,
clearCartAndAddItem,
navigation,
cartReducer,
getCartItemCount,
} = this.props;
const paymentMethod = paymentMethodForDevice();
await clearCartAndAddItem(
paymentMethod,
courseSpecificReducer.productData.code as string,
1,
navigation
)
if(cartReducer.cartItemCount > 0) {
// do some stuff
}
};
render() {
return (
<Button
btnStyle={[this.getStyles().smallButtonBuyCourse]}
labelStyle={[this.getStyles().buttonStickyLabelStyle]}
label={translate(
productData.isBundle && productData.isBundle === true
? "CategorySpecificScreen_buyThisBundle"
: "CategorySpecificScreen_buyThisCourse",
)}
onPress={this.purchaseItem}
disabled={false}
/>
)
};
CourseSpecificScreen.contextType = LocalizationContext;
export default connector(CourseSpecificScreen);
ThunkActions.ts
export const clearCartAndAddItem = (
paymentMethod: string,
productCode: string,
quantity: number,
navigation: any,
): AppThunk => {
return async (dispatch) => {
dispatch(cartActions.updateCartLoadingStatus(true));
const response = await cartServices.clearCart();
const {httpStatusCode} = response as APIResponse;
switch (httpStatusCode) {
case httpStatusCodes.SUCCESS_OK:
case httpStatusCodes.SUCCESS_CREATED:
case httpStatusCodes.SUCCESS_NO_CONTENT:
dispatch(cartActions.updateCartLoadingStatus(false));
dispatch(cartActions.updateCartItemCount(0))
globalConfig.setCartItemCount(0);
dispatch(addItemToCart(paymentMethod, productCode, quantity, navigation));
break;
case httpStatusCodes.CLIENT_ERROR_UNAUTHORIZED:
case httpStatusCodes.SERVER_ERROR_INTERNAL_SERVER_ERROR:
dispatch(cartActions.updateCartLoadingStatus(false));
let alertMessage = "Error, please try again later.";
if (response?.message) alertMessage = response?.message;
Alert.alert("Alert", alertMessage, [
{
text: "Ok",
},
]);
break;
default: {
dispatch(cartActions.updateCartLoadingStatus(false));
}
}
};
};
export const addItemToCart = (
paymentMethod: string,
productCode: string,
quantity: number,
navigation: any,
): AppThunk => {
return async (dispatch) => {
dispatch(cartActions.updateCartLoadingStatus(true));
const response = await cartServices.addItemToCart(productCode, quantity, paymentMethod);
const {httpStatusCode, data, error, message} = response as APIResponse;
console.log('add_item_to_cart_response:', response);
switch (httpStatusCode) {
case httpStatusCodes.SUCCESS_OK:
case httpStatusCodes.SUCCESS_CREATED:
dispatch(cartActions.updateCartLoadingStatus(false));
dispatch(cartActions.updateCartItemCount(quantity));
globalConfig.setCartItemCount(quantity);
break;
case httpStatusCodes.CLIENT_ERROR_UNAUTHORIZED:
dispatch(cartActions.updateCartLoadingStatus(false));
break;
case httpStatusCodes.SERVER_ERROR_INTERNAL_SERVER_ERROR:
case httpStatusCodes.CLIENT_ERROR_BAD_REQUEST:
dispatch(cartActions.updateCartLoadingStatus(false));
Alert.alert("Alert", (message)? message : "Error, it looks like you already have access to this course.", [
{
text: "Ok",
},
]);
break;
default: {
dispatch(cartActions.updateCartLoadingStatus(false));
}
}
};
};
Reducers.ts
const initialState: CartInitialState = {
isLoading: true,
cartToken: "",
responseStatus: apiResponseStatuses.IDLE,
cartItemCount: 0,
isMessageVisible: false,
message: "",
};
export default function cartReducer(
state = initialState,
action: CartActionTypes,
): CartInitialState {
switch (action.type) {
case UPDATE_LOADING_STATUS:
return {
...state,
isLoading: action.isLoading,
};
case UPDATE_CART_TOKEN:
return {
...state,
cartToken: action.cartToken,
};
case UPDATE_RESPONSE_STATUS:
return {
...state,
responseStatus: action.responseStatus,
};
case UPDATE_CART_ITEM_COUNT_TOKEN:
return {
...state,
cartItemCount: action.cartItemCount,
};
case CLEAR_DATA_ON_LOGOUT:
return {
...state,
isLoading: true,
cartToken: "",
responseStatus: apiResponseStatuses.IDLE,
cartItemCount: 0,
isMessageVisible: false,
message: "",
};
default: {
return state;
}
}
}
In the pruchaseItem() function of CourseSpecificScreen.tsx, I would like to dispatch a action that adds the item to the cart and immediately afterwards check if the cartItemCount has been updated & if it has, do something... This functionality works as expected, but only after clicking the Buy Now button twice.
I have ruled out the possibility of the issue being the API request failing the first time.
I have been stuck on this issue for several days now so any help or advice would be greatly appreciated. Let me know if I need to include more information
In my case, I was storing a reference of the old cartReducer state before it was being updated.
I got this working by updating my purchaseItem() function to look like the below:
pruchaseItem = async () => {
const {
courseSpecificReducer,
clearCartAndAddItem,
navigation
} = this.props;
const paymentMethod = paymentMethodForDevice();
await clearCartAndAddItem(
paymentMethod,
courseSpecificReducer.productData.code as string,
1,
navigation
)
const { cartReducer } = this.props;
if(cartReducer.cartItemCount > 0) {
// do some stuff
}
};

how to save array object data in redux store

i try to store multiple object in redux store on my react native app, but only one object is save,
i'm new at redux, i try a lot of solutions found on StackOverflow but no one works :/
result i have in my store:
"hives": {"hive_id": 12944, "hive_name": null}
result i want (or something like that) :
"hives": [
1: {"hive_id": 123, "hive_name": "HelloHive"},
2: {"hive_id": 12944, "hive_name": null}]
store:
const middleware = [thunk]
export const store = createStore(persistedReducer, applyMiddleware(...middleware));
export const persistor = persistStore(store);
reducer :
const INIT_STATE = {
hives: [],
}
const hiveReducer = (state = INIT_STATE, action) => {
switch (action.type) {
case SET_HIVES:
return {
...state,
hives: action.payload,
};
[...]
action creator:
export const setHives = hives => {
return {
type: SET_HIVES,
payload: hives,
};
};
action:
export const getHives = () => {
return dispatch => {
axios.get(GET_HIVE_URL, HEADER).then(res => {
const status = res.data.status;
const hives = res.data.hives;
if (status == 'hiveFound') {
for (let i = 0; i < hives.length; i++) {
console.log(hives[i]);
dispatch(setHives(hives[i]));
}
}
});
};
};
and my API send me:
"hives": [
{
"hive_id": 123,
"hive_name": "HelloHive"
},
{
"hive_id": 12944,
"hive_name": null
}
]
and console.log(hives[i]) return :
LOG {"hive_id": 123, "hive_name": "HelloHive"}
LOG {"hive_id": 12944, "hive_name": null}
thanks you
First of all, in your reducer you don't need to use ...state spread operator, since hives seems to be the only one variable in your state there. And second, you are iterating over each element of hives, therefore you are inputting them one by one thus overwriting the previous one. You are not appending it to array. Here's how you need to change your action:
export const getHives = () => {
return dispatch => {
axios.get(GET_HIVE_URL, HEADER).then(res => {
const status = res.data.status;
const hives = res.data.hives;
if (status == 'hiveFound') {
dispatch(setHives(hives));
}
});
};
};
This way it will write the whole array into that variable in redux.
You can try this below so you can store the whole array. assuming you already have the actions.
InitialState
export default {
hives:[]
}
HivesReducer
export default function counter(state = initialState.hives, action) {
switch (action.type) {
case Types.SET_HIVES:
return [...state, action.payload];
default:
return state;
}
}
In your reducer try this :
case SET_HIVES:
return {
...state,
hives: [...state.hives,action.payload],
};
[...]
hope it helps. feel free for doubts

Composing a redux reducer for nested objects in an array

I'm new to Redux and am having some difficulty composing a working reducer for my situation.
My current state looks like this
export const userData = {
userID: '12345678',
userDetails: {
firstName: 'Joe',
surname: 'Bloggs'
},
currentGames: [
{
gameId: 'G-00000001',
gameSelections: [
{
subgameId: '',
selection: ''
}
]
}
]
};
My action looks like this
function selectWinner (gameId, subgameId, selection) {
return {
type: SELECT_WINNER,
gameId,
subgameId,
selection
}
}
The aim is to be able to add/update the objects in the gameSelections array.
There may be more than one Object in the currentGames array also.
I've heard I should use .map but I'm not really sure how.
You're on the right track for using .map() to iterate over the array of objects. It also looks like your action-creator has all the necessary parameters to update your reducer state.
Your reducer can look something like this:
const userReducer = (state=userData, action) => {
switch(action.type){
case SELECT_WINNER:
return {
...state,
currentGames: [...state.currentGames].map((game) => {
if(game.gameId == action.gameId){
return {
...game,
gameSelections: [...game.gameSelections].map((gameSelection) => {
if(gameSelection.subgameId == action.subgameId){
return {
...gameSelection,
selection: action.selection
}
} else {
return gameSelection
}
})
}
} else {
return game
}
})
}
default:
return state
}
}
Kind of messy, but would get the job-done with a deeply nested state.
Add item to array:
case'ADD_ITEM':
return {
...state,
some_arr: [...state.some_arr, action.payload]
}
update spicific item in array:
case 'UPDATE_ITEM':
return {
...state,
some_arr: state. some_arr.map(
(item, index) => index === specific_index
? {...item, ...action.payload}
: content
)
}
Deep cloning of the state is required.
useful link-https://redux.js.org/recipes/structuring-reducers/immutable-update-patterns
const reducer = (state = userData, action) => {
switch (action.type) {
case CASENAME:
return {
userID: state.userID,
userDetails: {
...state.userdetails
},
currentGames: [
{
gameId: action.gameId,
gameSelections: [
{
subgameId: action.subgameId,
selection: action.selection
}
]
}
]
};
}
}

React/Redux updating a certain value in an array of objects

I am just learning redux and this is my first time using it in a project. I am trying to update a certain value in an array of objects. The structure of my object is:
students: {
loading: false,
error: null,
data: [{
id: 1,
name: "Bob",
email: 'whatever#gmail.com',
status: 'out'
}]
}
Below are my actions for this and the data it gets back is the id of the student that it needs to update. These work fine.
export const studentCheckInStart = student => ({
type: "STUDENT_CHECK_IN_START",
student
})
export const studentCheckIn = (id) => {
return dispatch => {
dispatch(studentCheckInStart())
return axios.put('http://localhost:8080/studentList/'+id)
.then((response) => {
dispatch(studentCheckInSuccess(response.data))
}).catch(err => {
dispatch(studentCheckInError(err))
})
}
}
export const studentCheckInSuccess = (data) => {
return {
type: STUDENT_CHECK_IN_SUCCESS,
payload: data
}
}
export const studentCheckInError = (error) => {
return {
type: STUDENT_CHECK_IN_ERROR,
error
}
}
Where I'm having the issue is in the reducer
case "STUDENT_CHECK_IN_SUCCESS":
let updatedStudent = state.students.data.findIndex((student) => {
return student.id === action.payload
})
console.log(updatedStudent)
return {
...state,
students: {
...state.students[updatedStudent],
data: {
status:'in'
}
}
};
break;
case "STUDENT_CHECK_IN_START":
return {
...state,
students: {
...state.students,
loading: true
}
}
break;
case "STUDENT_CHECK_IN_ERROR":
return {
...state,
students: {
...state.students,
error: action.payload,
loading: false
}
}
break;
I'm trying to target the specific student object using the id to find the index of the student I want to target. Then change just the status of that object to "in". I know what I have in the STUDENT_CHECK_IN_SUCCESS is incorrect, I'm just not sure how to do it.
Your state seems a little bit complex. Why do you need loading or error in your students object? What other parts do you have in your state beside students? This is one possible way I can think of at this situation (just the related part) :
let updatedStudent = state.students.data.findIndex(
student => student.id === action.payload
);
const newData = [ ...state.students.data ];
newData[ updatedStudent ] = { ...newData[ updatedStudent ], status: "in" }
return { ...state, students: { ...state.students, data: newData } };
I will edit my answer if I think a better way.
It looks like your action doesn't really need all that payload, just an id of the student who checked in. So if you change that, I think you could return this from your reducer action:
return {
...state,
students: {
...state.students,
data: state.students.data.map(s => {
if (s.id === action.id) {
return { ...s, status: 'in' };
}
return s;
}
}
};
The idea is that you need to return everything unchanged except the data array. By using map, we can return a modified version of the data array where the student whose id matches the one supplied in the action will have their status changed to in, but the rest of the students in the data array remain unchanged.

How to split reducer under 1 key?

I want to separate a reducer into N more combining together 1 key.
Say I have an initial state:
const STATE_INITIAL = {
nested_component: {
field1: 1,
field2: 2
},
upper_field: 3
}
Then I have a reducer:
function reducer(state=STATE_INITIAL, action){
switch(action){
case ACTION_UPPER_FIELD:
return ...
case ACTION_GRID1:
return ...
case ACTION_GRID2:
return ...
default:
return state;
}
}
Why I want to do it?
I want to have a component that I can reuse throughout the project. It would always come with its initial state and would have its reducer that I'd like to connect to the rest of the application.
My solution
One way I can think of is stacking cases for grid actions, providing it with state.gridand its own initial state and combing the result with the state:
const STATE_INITIAL = {
nested_component: {},
upper_field: 3
};
function reducer(state=STATE_INITIAL, action){
switch(action){
case ACTION_UPPER_FIELD:
return ...
case ACTION_GRID1:
case ACTION_GRID2:
return reducerGrid(state.grid, action);
default:
return state;
}
}
const STATE_INITIAL_GRID = {
field1: 1,
field2: 2
};
function reducerGrid(state = STATE_INITIAL_GRID, action) {
switch(action){
case ACTION_GRID1:
return ...
case ACTION_GRID2:
return ...
default:
return state;
}
}
Is there a standardized approach or is my solution fine? The things I don't like about it is the default in the reducerGrid seems redundant now and I am also not satisfied with having to repeat the actions in both reducers.
My 2nd Solution
function reducer(state=STATE_INITIAL, action){
const stateGrid = reducerGrid(state.grid, action)
let stateNew = state;
if(stateGrid !== state.grid){
stateNew = {...state, grid: ...stateGrid}
}
switch(action){
case ACTION_UPPER_FIELD:
return {...stateNew, ... };
default:
return stateNew;
}
}
3rd Solution
function reducer(state=STATE_INITIAL, action){
const stateNew = {...state, grid: ...reducerGrid(state.grid, action)};
switch(action){
case ACTION_UPPER_FIELD:
return ...
default:
return stateNew;
}
}
I have finally found a solution I am satisfied with.
Using this method:
import R from 'ramda';
function splitReducers(reducers, rest) {
return (state, action) => {
const reducersPrepared = R.mapObjIndexed((reducer, key) => {
return reducer(R.defaultTo({}, state)[key], action);
})(reducers);
const getUndefinedIfEmpty = R.ifElse(
R.isEmpty,
() => undefined,
R.identity
);
const stateWithoutSplitKeys = R.pipe(
R.omit(R.keys(reducers)),
getUndefinedIfEmpty
)(state);
return R.merge(
reducersPrepared,
rest(stateWithoutSplitKeys, action)
);
}
}
I can write my state tree in the following way:
Ports: splitReducers({
grid: reducerGrid,
}, reducer);
This will result in the object with keys split:
{
Ports: {
grid: {...},
isSaving: true,
isVisible: false
}
}
After applying the method the root-reducer is showing more of its state at the first glance:
export const rootReducer = combineReducers({
pageAllocation: combineReducers({
tabNetwork: combineReducers({
popupNetworkTemplates: reducerPopupNetworkTemplates,
gridPorts: splitReducers({ // <----- HERE IT IS
grid: reducerGridPortsOnly
}, reducerPorts),
}),
tabStorage: () => ({}),
activeTab: reducerPortsActiveTab
}),

Resources