How to update redux state of arrays? - javascript

So I'm creating something like "Trello" clone with react redux nodejs and mongoDB and i have some issue.
The problem is when I add a card to a list its not update the redux state, so I will see the card in the list only after a refresh page. (the card added to the DB but not to redux state).
just for more info: boardlists is an array inside the object board from mongo, inside that array there is objects of list, inside each of them there is an array of cards.
here is my code:
REDUCER
const initialState = {
boardLists: [
],
};
export default function (state = initialState, action) {
switch (action.type) {
case FETCH_ITEMS_BEGIN:
return {
...state,
loading: true,
errors: null,
};
case FETCH_ITEMS_SUCCESS:
return {
...state,
loading: false,
boardLists: action.payload.data.boardLists,
};
case FETCH_ITEMS_FAILURE:
return {
...state,
loading: false,
errors: action.payload.errors,
boardLists: [],
};
//handless creation of data
case ADD_LIST:
return {
boardLists: [...state.boardLists, action.payload.list],
};
case ADD_CARD:
return {
boardlists: [...state.boardlists, action.payload.card],
}
ACTIONS
export const fetchItemsBegin = () => ({
type: FETCH_ITEMS_BEGIN,
});
export const fetchItemsSuccess = (data) => ({
type: FETCH_ITEMS_SUCCESS,
payload: { data },
});
export const fetchItemsFailure = (errors) => ({
type: FETCH_ITEMS_FAILURE,
payload: { errors },
});
//dispatched when item needs to be created
export const addList = (list) => {
return {
type: ADD_LIST,
payload: { list },
};
};
// add card
export const addCard = (card) => {
return {
type: ADD_CARD,
payload: { card }
};
};
//dispatched when all the lists from board stored in redux store needs to be read
export const getBoardLists = () => {
return (dispatch) => {
// function starts
dispatch(fetchItemsBegin()); // fetching begins
return http
.get(`${myUrl}/boards/one`) // req data from server
.then(({ data }) => {
console.log(data);
// if data is found
dispatch(fetchItemsSuccess(data)); // success
})
.catch((error) => dispatch(fetchItemsFailure(error))); //errors
};
};
COMPONENT THAT HANDLE THE ADD FUNCTION
handleAddCard = () => {
//add card
const { text } = this.state;
const { listID } = this.props;
const newCard = {
// _id: uuidv4(),
text,
};
cardService.createCard(newCard, listID);
this.props.addCard(newCard);
};
.
.
.
.
.
const mapStateToProps = ({ boardLists, loading, errors }) => ({
boardLists,
loading,
errors,
});
export default connect(mapStateToProps, { addList, addCard, getBoardLists })(ActionButton);

It appears you need to update an object in your lists array, and not add the card item to the list array itself.
In the actions:
// add card
export const addCard = (card, listId) => {
return {
type: ADD_CARD,
payload: { listId, card }
};
};
In the Reducer, you will need to find the list with matching id and add the card to its array e.g.:
case ADD_CARD:
const {listId, card} = action.payload;
return {
...state,
boardLists: state.boardLists.map(list => {
list.cards = list.cards || [];
return list.id === listId ? {...list, cards: [...list.cards, card]} : list
}),
}
This other question on stack overflow could be useful for this part. link

Related

How can I use Redux to only update one instance of a component?

I'm trying to use Redux to update my Card Component to disable and change colors on click. Redux dispatches the action fine, but it updates all Cards not just the one that was clicked. Each Card has an object associated with it that hold the word and a value. The value is the className I want to use to change the color when clicked
Component
const Card = ({ wordObj, updateClass, isDisabled, cardClass }) => {
const showColor = (e) => {
updateClass(wordObj);
console.log(cardClass)
};
return (
<button
onClick={(e) => showColor()}
disabled={isDisabled}
className={cardClass}>
{wordObj.word}
</button>
);
};
const mapStateToProps = (state) => ({
cardClass: state.game.cardClass,
});
export default connect(mapStateToProps, { updateClass })(Card);
Action
export const updateClass = (obj) => (dispatch) => {
console.log(obj)
dispatch({
type: UPDATE_CARD,
payload: obj,
});
};
Reducer
const initialState = {
words: [],
cardClass: 'card',
isDisabled: false,
};
export default function (state = initialState, action) {
const { type, payload } = action;
switch (type) {
case SET_WORDS: {
return {
...state,
words: payload,
};
}
case UPDATE_CARD:
return {
...state,
isDisabled: true,
cardClass: ['card', payload.value].join(' '),
};
default:
return state;
}
}```
All of your card components are consuming the same cardClass field in the state. When you modify it in this line:
cardClass: ['card', payload.value].join(' ')
All cards that are consuming this field have their classes updated. The same occurs to the isDisable field.
You need to create one object for each card in your state. Here is my implementation (was not tested):
const initialState = {
cards: []
};
export default function (state = initialState, action) {
const { type, payload } = action;
switch (type) {
// create a card object for each word
case SET_WORDS: {
return {
...state,
cards: payload.map(word => {
return { word: word, cardClass: "card", isDisabled: false }
})
};
}
case UPDATE_CARD:
// here i'm using the wordObj.word passed as payload
// to identify the card (i recommend to use an id field)
const cardIndex = state.cards.findIndex(card => card.word === payload.word);
// get the current card
const card = state.cards[cardIndex];
// create an updated card object obeying the immutability principle
const updatedCard = { ...card, isDisabled: true, cardClass: ['card', payload.value].join(' '), }
return {
...state,
cards: [
...state.cards.slice(0, cardIndex), // cards before
updatedCard,
...state.cards.slice(cardIndex + 1) // cards after
]
};
default:
return state;
}
}
Your mapStateToProps selects a string, but said string changes on any updateClass and that causes all your cards to update, because the selection of state.game.cardClass produces a different value, which triggers a new render for the connected component.
Maybe what you want, is something that identifies the selection, i.e. an id for each card, and select with that id in the mapStateToProps to avoid reading the change, because what's happening right now is the following:
Card A[className="card A"] == after dispatch ==> mapStateToProps => [className="card B"]
Card B[className="card A"] => dispatch('B') => mapStateToProps => [className="card B"]
B is updating the state of both A and B, and that's why the extra render occurs

Redux: altering different parts of the initial state in Reducer according to Actions

I have the following Reducer:
const initialState = {}
const dishReducer = (state = initialState, action) => {
switch (action.type) {
case 'LOAD_DISHES':
return (action.dishes)
case 'LOAD_DISHES_ERROR':
console.log("load dishes error")
return state
case 'LOAD_DISHES_SUCCESS':
console.log("load dishes success")
return state
default:
return state;
}
};
export default dishReducer;
And the following action(s):
import {database} from '../../config/fbConfig'
export const startLoadingDishes = (dishes) => {
return (dispatch) =>{
return database.ref('products-dishes').once('value').then((snapshot) => {
let dishes = {}
snapshot.forEach((childSnapshot) => {
let parentkey = childSnapshot.key
let dishArray = [];
childSnapshot.forEach((dish) =>{
dishArray.push(dish.val())
});
dishes[childSnapshot.key] = dishArray;
})
dispatch(loadDishes(dishes))
}).then(() => {
dispatch({ type: 'LOAD_DISHES_SUCCESS' });
}).catch(err => {
dispatch({ type: 'LOAD_DISHES_ERROR' }, err);
});
}
}
export const loadDishes = (dishes) => {
return {
type: 'LOAD_DISHES',
dishes
}
}
The 'startLoadingDishes' action is called inside the componentDidLoad() of a certain Component. However, I want to alter the initial state of my dishReducer so that it includes additional information, as follows:
const initialState = {
value : {},
loaded: false,
loading: false,
error: false
}
So now 'action.dishes' returned by reducer [in 'LOAD_DISHES' case] should be put inside the 'value' part of the state, instead of it being the whole state. Also, the 'loaded' part of the state should be set to true if dishes have already been loaded earlier, and so on. I understand this is fairly simple but as I am new to React+Redux, I don't know how to alter the Action/Reducer codes properly (while keeping state immutability). Any help is appreciated.
I originally asked the question, here is how I solved it (not sure if this is the 'best' way though):
New reducer file:
const initialState = {
value : {},
loaded: false,
loading: false,
error: false
}
const dishReducer = (state = initialState, action) => {
switch (action.type) {
case 'LOAD_DISHES':
return {
value: action.dishes,
loading: !state.loading,
loaded: false, //this might need to be set to true
error: false
}
case 'LOAD_DISHES_ERROR':
console.log("load dishes error")
return {
...state, //or: state.value, as the other parts of state are being overwritten below
loaded: false,
loading: false,
error: true
}
case 'LOAD_DISHES_SUCCESS':
console.log("load dishes success")
return {
...state, //better: state.value
loaded: true,
loading: false,
error: false
}
default:
return state;
}
};
export default dishReducer;
No change in actions file.
Now, inside the 'Main' component, I was originally accessing the state as such:
class Main extends Component {
componentDidMount() {
this.props.startLoadingDishes();
}
render() {
return (
//some code
)
}
}
const mapStateToProps = (state) => {
return {
dishes: state.dishes //to access dishes: dishes.value
}
}
export default connect(mapStateToProps, actions)(Main)
The Main component code also stayed the same, with the difference that now I use 'dishes.value' instead of just 'dishes' to access the value of dishes from the state (and dishes.loaded for loaded, and so on). And now the action caller inside componentDidMount is as follows:
componentDidMount() {
if(!this.props.dishes.loaded){
this.props.startLoadingDishes();
console.log("loading dishes from database")
}
}

how to check if the current item is available in the cart list in reactjs

I am working on a reactjs ecommerce app for that I need to check in productDetail Page is the product is already available in cart page. Both of the components have different api endpoint. I am not sure how to do that. I am using react-redux for state management.
CartActions.js
export const getItemsAddedToCart = () => dispatch => {
dispatch({
type: cartActionTypes.GET_CART_LOAD
});
new _rest()
.get(URLConstants.urls.GET_ALL_ITEMS_IN_CART)
.then(response => {
dispatch({
type: cartActionTypes.GET_CART_SUCCESS,
payload: response.data
});
})
.catch(err => {
dispatch({
type: cartActionTypes.GET_CART_ERROR,
error: err
});
});
};
export const IsItemInCart = (pId) => ({
type: cartActionTypes.CHECK_ITEM_IN_CART,
pId
});
These are my actions inside productDetails page i am passing product id inside IsItemIncart. Then In reducer I am trying to look into cart ans update state.
CartReducer.js
const initialState = {
loading: false,
loaded: false,
error: null,
itemsInCart: null,
pagination: null,
totalamount: 0,
checkedItems: [],
paymentStatus: "NIL_TRANSACTION",
order: {
addressId: "NIL",
paymentType: "NIL"
},
isItemInCart: false,
bulkOrderData: []
};
export default function (state = initialState, action) {
switch (action.type) {
case cartActionTypes.GET_CART_LOAD:
return {
...state,
loading: false
};
case cartActionTypes.GET_CART_SUCCESS:
return {
...state,
loaded: true,
loading: false,
totalItemInCart: action.payload._embedded.cartResourceList.length,
itemsInCart: action.payload._embedded.cartResourceList,
totalamount: action.payload._embedded.cartResourceList.reduce(
(acc, item) => {
return item.totalAmount + acc;
},
0
),
pagination: action.payload.page
};
case cartActionTypes.CHECK_ITEM_IN_CART:
const findItem = state.itemsInCart.findIndex(
productId => productId === action.pid
);
return {
...state,
isItemInCart: true,
/*
* Check For Item In Cart Here
* */
};
Here is My reducer I want to check them here which i am not sure how to do that.
In CartAction.js
export const IsItemInCart = (productId) => {
return (dispatch, getState) => {
// you can use your cart reducer name instead of cartData
const cartData = getState().cartData;
// match your productId with your products in cart
const product = cartData.find((singleProductInCart) => {return singleProductInCart.productId === productId});
// then proceed further depending on you get product or not
}
}
Actions in redux are meant to transform the state as you've done with getItemsAddedToCart. IsItemInCart is not how you should use actions as checking whether a particular item is in the cart is a derived property that can be calculated outside the store (in the component or otherwise) like below:
import React from 'react';
import { connect } from 'react-redux';
import { getItemsAddedToCart } from './CartActions';
// these will get mapped to the components props
// using the connect function from react-redux
const mapDispatchToProps = (dispatch) => ({
fetchCartItems: () => dispatch(getItemsAddedToCart())
})
const mapStateToProps = (state, props) => ({
cartList: state.cart.itemsInCart,
});
class ExampleComponent extends React.Component {
constructor(props) {
super(props);
// if has not been fetched yet
if (!this.props.cartList) {
this.props.fetchCartItems();
}
}
checkItemInCart(pID) {
return this.props.cartList.find(
(product) => product.id === pID);
}
....
....
}
export default connect(mapStateToProps, mapDispatchToProps)(ExampleComponent);
Read usage with react.

Delete Item from Array (React-Native + Redux)

I have the checklist with users and when I click on the checkbox user should add to the InputField or delete from InputField, if I check to it again.
For now works only ADD.
import ...
export default class NewEvent extends React.Component {
constructor(props) {
super(props);
this.onSelect = this.onSelect.bind(this);
}
onSelect = id => {
addMembers(id) }
findSelectedContacts = (contacts, membersArray) => {
const newArr = [];
contacts.forEach(item => {
if(membersArray.indexOf(item.id.toString()) > -1) {
newArr.push(` ${item.name}`)
}
});
return newArr;
}
render() {
const { navigation, members, location, contacts } = this.props;
const membersArray = members ? members.split(',') : [];
const selectedArray = this.findSelectedContacts(contacts, membersArray)
const inputFill = selectedArray.join().trim();
return (
<InputField
customStyle={[eventStyles.input]}
icon="addGuest"
placeholder="Add guests"
onGetText={texts => {
this.handlerChangeText(texts)
}}
value={inputFill}
/>
);
}
}
Also, I have reducer, which adds guests to input:
import { handleActions } from 'redux-actions';
import * as types from '../actions/actionTypes';
export const initialState = {
members: '',
};
const addMembers = (members, id) => {
const res = members ? `${members},${id}` : `${id}`;
return res;
}
export default handleActions(
{
[types.ADD_GUEST]: (state, action) => ({
...state,
members: addMembers(state.members, action.payload),
}),
},
initialState
);
Please advise, how I can change my reducer? I need to add or delete the user from InputFiled if I click on the ONE checkbox.
Currently, it appears that you are storing the members list as a comma-separated string. A better option would be to store the list as an actual array, and then convert that to a string when it's needed in that format, e.g. rendering.
The reducer for it might look something like this (trying to follow your existing code style:
export const initialState = {
// initialState changed to array
members: [],
};
const addMember = (members, id) => {
// add id to the end of the list
return members.concat(id);
}
const removeMember = (members, id) => {
// return a new list with all values, except the matched id
return members.filter(memberId => memberId !== id);
}
export default handleActions(
{
[types.ADD_GUEST]: (state, action) => ({
...state,
members: addMember(state.members, action.payload),
}),
[types.REMOVE_GUEST]: (state, action) => ({
...state,
members: removeMember(state.members, action.payload),
}),
},
initialState
);
And if you then need the list as a string, in your component render() method - or preferrably in your react-redux mapStateToProps selector you can convert it to a string:
memberList = state.members.join(',')

React filtering action not working

Given a currentUser with an id, and a database of 'events' created by various users, I am trying to fetch a filtered list of events that include only events created by the current user, in other words, their events.
My plan of attack was to make a piece of the state a 'filter' which would be updated depending on various things, but right now I am having trouble even getting the most basic filter to work.
The error always occurs within the function immediately below this paragraph. I have gotten an error staying "currentUser" is undefined, or that "author_id" is undefined or "cannot read property author_id or undefined."
currentUser is defined on the store when I checked with getState.
filterMyEvents() {
let cid = currentUser.id
let e = this.props.events
let myEventsFilter = (events, id) => {
debugger
(cid === e[id].author_id)
}
myEventsFilter = myEventsFilter.bind(null, this.props.events)
this.props.updateFilter(myEventsFilter);
}
The filtering takes place in Selector.js
export const allEventsByFilter = (events, filter) => {
if (filter) {
return Object.keys(events).filter(filter);
} else {
return events;
}
}
User Container:
const mapStateToProps = (state) => ({
currentUser: state.session.currentUser,
requestEvents: requestEvents,
events: allEventsByFilter(state.events, state.filter)
});
const mapDispatchToProps = (dispatch) => ({
requestEvents: () => { dispatch(requestEvents()) },
updateFilter: (filter) => dispatch(updateFilter(filter))
})
Filter actions:
export const FilterConstants = {
UPDATE_FILTER: "UPDATE_FILTER"
};
export const updateFilter = (filter) => ({
type: FilterConstants.UPDATE_FILTER,
filter
});
Filters reducer
const _defaultFilters = () => {
return true;
}
const FiltersReducer = function(state = null, action){
if (action.type === FilterConstants.UPDATE_FILTER){
// const newFilter = {[action.filter]: action.value};
return action.filter;
} else {
return state;
}
};

Categories

Resources