React-redux: Nested array is being duplicated - arrays

I'm trying to create an order table (array) that has a nested "products" array in each order.
The order table is rendering as expected, but the products are the same for every order.
OrderTable Component (simplified for clarity)
class OrderTable extends Component {
componentWillMount() {
this.props.fetchPtOrders(this.props.patient_id);
}
renderOrders(orders) {
return orders.map((order) => {
return (
<tr key={order.id}>
<td>
<ProductTable id={order.id}/>
</td>
</tr>
);
});
}
render() {
const { orders, loading, error } = this.props.orderTable;
return (
<div class="container divcon">
<h1>Orders</h1>
<table class="pto">
<tbody>
{this.renderOrders(orders)}
</tbody>
</table>
</div>
);
}
}
export default OrderTable;
<ProductTable id={orders.id}/> arrays the products and is basically a copy of the above (minus the ProductTable component).
I tried debugging using IDs (3000022 and 3000023) and found that everything is being done in batches.
3000022 contains products / 3000023 is empty.
The response from the requests is being used for both IDs, and is overwritten with every iteration. Only the last response is used for every order.
ProductTable Container:
function mapStateToProps(state, ownProps) {
return {
ProductTable: state.order_products.ProductTable,
order_id: ownProps.id
};
}
const mapDispatchToProps = (dispatch) => {
return {
fetchPtOrderProducts: (id) => {
dispatch(fetchPtOrderProducts(id)).then((response) => {
!response.error ? dispatch(fetchOrderProductsSuccess(response.payload.data)) : dispatch(fetchOrderProductsFailure(response.payload.data));
});
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ProductTable);
Product Fetch action:
export function fetchPtOrderProducts(id) {
const request = axios({
method: 'get',
url: `${ROOT_URL}/order_product/search.php?s=${id}`,
headers: []
});
return {
type: FETCH_PTORDER_PRODUCTS,
payload: request
};
}
Product Success action:
export function fetchOrderProductsSuccess(order_products) {
console.log("products fetched")
return {
type: FETCH_ORDER_PRODUCTS_SUCCESS,
payload: order_products
};
}
Product Reducers
case FETCH_ORDER_PRODUCTS:// start fetching products and set loading = true
return { ...state, ProductTable: {order_products:[], error: null, loading: true} };
case FETCH_ORDER_PRODUCTS_SUCCESS:// return list of products and make loading = false
return { ...state, ProductTable: {order_products: action.payload, error:null, loading: false} };
How can I make orders.map() and <ProductTable /> array one ID at a time?
Thanks for you help! Sorry if anything is unclear... I'm a complete newbie.

As far as I can see you are overriding the same field in your state.
You should change ProductTable field to maintain order_id for each product list. It should be a map of order_id to order_products.
Note that my code might contain mistakes because I have no runnable example to edit.
Something like this:
ProductTable Container:
function mapStateToProps(state, ownProps) {
return {
orderProductTable: state.order_products.ProductTable[ownProps.id],
order_id: ownProps.id
};
}
const mapDispatchToProps = (dispatch) => {
return {
fetchPtOrderProducts: (id) => {
// Add `id` argument to fetchOrderProductsSuccess here
dispatch(fetchPtOrderProducts(id)).then((response) => {
!response.error ? dispatch(fetchOrderProductsSuccess(id, response.payload.data)) : dispatch(fetchOrderProductsFailure(response.payload.data));
});
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(OrderProductTable);
Product Fetch action:
export function fetchPtOrderProducts(id) {
const request = axios({
method: 'get',
url: `${ROOT_URL}/order_product/search.php?s=${id}`,
headers: []
});
return {
type: FETCH_PTORDER_PRODUCTS,
payload: {id}
};
}
Product Success action:
export function fetchOrderProductsSuccess(id, order_products) {
console.log("products fetched")
// Add `id` field here
return {
type: FETCH_ORDER_PRODUCTS_SUCCESS,
payload: {id, products: order_products}
};
}
Product Reducers
case FETCH_ORDER_PRODUCTS:// start fetching products and set loading = true
return { ...state, ProductTable: {...(state.ProductTable || {}), [action.payload.id]: {order_products:[], error: null, loading: true}}};
case FETCH_ORDER_PRODUCTS_SUCCESS:// return list of products and make loading = false
return { ...state, ProductTable: {...(state.ProductTable || {}), [action.payload.id]: {order_products: action.payload.products, error:null, loading: false}}}};

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
}
};

fetching data from Sanity returns undefined (Next.js + Sanity)

I have page where I want to add some actualities. These actualities will be first set in the Sanity and then fetched via Next.js .
My Sanity schema
export default{
name:"actuality",
title:"Aktuality",
type:"document",
fields:[
{
name:"headline",
title:"Nadpis",
type:"string"
},
{
name:"publishedAt",
title:"Datum zveřejnění",
type:"datetime"
},
{
name:"body",
title:"Text",
type:"blockContent"
}
],
preview:{
select:{
title:"headline",
}
}
}
Problem is in fetching the data.
If I do this it will work, but will return only first actuality in the Sanity
export const getServerSideProps = async (pageContext: any) => {
const query = `*[ _type == "actuality"][0]`;
const recipe = await client.fetch(query);
console.log(recipe);
if (!recipe) return { props: null, notFound: true };
else
return {
props: {
headline: recipe.headline,
publishedAt: recipe.publishedAt,
body: recipe.body,
},
};
};
But if I remove the [0] it will throw error: "Reason: undefined cannot be serialized as JSON. Please use null or omit this value."
What do I need to change in order to get an array of Actualities?
Wrap the response in a data object to serialize and call {data} in your page props like this:
export const getServerSideProps = async (pageContext: any) => {
const query = `*[ _type == "actuality"]`;
const recipe = await client.fetch(query);
console.log(recipe);
if (!recipe) return { props: null, notFound: true };
else
return {
props: {
data: {
headline: recipe.headline,
publishedAt: recipe.publishedAt,
body: recipe.body,
},
},
};
};
Few things:
it returns an array if you remove [0], you can do return just the data, regardless an array or not.
props: {
data: recipe
}
if you want to return single data with obj vallue as multiple props
props: {...recipe}

React Redux: Update and replace records with another records returns value of 1

React Redux: Update and replace records with another records returns value of 1.
On the server side, I have a json response [{"id":"10", "food_name":"Rice"}]
The code below works fine by displaying a food item called Rice from the database via API Call as showed in the json array above.
Now I have a requirements to replace the displayed food item Rice with Beans.
To this effect, I have a json files which is to be returned via API Call after posting
[{"id":"10", "food_name":"Beans"}]
I have also created a Post button which should send data to the server side and return the response Beans.
Here is my effort as well as my Issue which is caused by reducer.
If Implement the code below in the reducer
case foodConstants.FOOD_SUCCESS_POST:
return {
items: state.items.map(food1 => {
if (food1.id === action.id) {
//return { ...food1, food_name: state.items[0].food_name};
return { ...food1, food_name: 'Beans' };
}
The Code works fine and Rice is replaced with Beans since I set value beans in the reducer.
but since I need to get the records via API Call so if implement
case foodConstants.FOOD_SUCCESS_POST:
return {
items: state.items.map(food1 => {
if (food1.id === action.id) {
return { ...food1, food_name: state.items[0].food_name};
}
Am getting value of 1 replacing Rice instead of Beans. Please where is this value of 1 coming from.
I need to have beans replace record Rice as a value returned from API Call.
My action and service code are okay as I can see the json returned records in the array as per
[{"id":"10", "food_name":"Beans"}]
I think my problem lies in this line of code below which returns value of 1 instaed of Beans.
return { ...food1, food_name: state.items[0].food_name};
Here is the full code
import React from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { foodActions } from 'actions';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
this.props.dispatch(foodActions.getFood());
}
handleFood(id,food_type) {
return (e) => this.props.dispatch(foodActions.postfood(food_id));
}
render() {
const { food1, foods1 } = this.props;
return (
<div>
{foods1.items &&
<ul>
{foods1.items.map((food1, index1) =>
<li key={food1.id}>
{food1.food_name}
<input type="button" value="Post and Update Food Name" onClick={this.handleFood(food1.id)} />
</li>
)}
</ul>
}
</div>
);
}
}
function mapStateToProps(state) {
const { foods1} = state;
const { food1 } = state;
return {
food1, foods1
};
}
const connectedApp = connect(mapStateToProps)(App);
export { connectedApp as App };
Reducer Code
import { foodConstants } from '/constants';
export function foods1(state = {}, action) {
switch (action.type) {
case foodConstants.GETALL_REQUEST:
return {loading: true};
case foodConstants.GETALL_SUCCESS:
return {
loading: false,
error: null,
items: action.foods1,
};
case foodConstants.GETALL_FAILURE:
return {
error: action.error
};
// Post and Update Food Name
case foodConstants.FOOD_REQUEST_POST:
return {...state};
case foodConstants.FOOD_SUCCESS_POST:
return {
items: state.items.map(food1 => {
if (food1.id === action.id) {
return { ...food1, food_name: state.items[0].food_name};
}
return food1;
})
};
case foodConstants.FOOD_FAILURE_POST:
return {
error: action.error
};
default:
return state
}
}
You need to replace value that is coming in action, but you are picking from state
case foodConstants.FOOD_SUCCESS_POST: {
const updatedItems = state.items.map((food1) => {
if (food1.id === action.id) {
return { ...action };
}
return food1;
});
return { ...state, items: updatedItems };
}
Or you can do this as well
case foodConstants.FOOD_SUCCESS_POST: {
let updatedItems = { ...state.items };
const itemIndex = updatedItems.findIndex((food1) => food1.id === action.id);
if(itemIndex > -1){
updatedItems[itemIndex] = {
...updatedItems[itemIndex],
...action,
}
}
return { ...state, items: updatedItems };
}

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.

Redux overwrites model with previous state

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.

Resources