Redux doesn't re-render the components - javascript

I have a component which takes data from mapStateToProps() method. Component's code is:
handleClick = () => {
if (this.props.data.status) {
this.props.changeIpStatus(index, !this.props.data.status);
} else {
this.props.changeIpStatus(index, !this.props.data.status);
}
}
render() {
if (this.props.data.status) {
this.switchClasses = `switcher blocked`;
this.dotClasses = `dot blocked`;
} else {
this.switchClasses = `switcher`;
this.dotClasses = `dot`;
}
return (
<div className="block">
<div onClick={this.handleClick} className={this.switchClasses}>
<div className={this.dotClasses}></div>
</div>
</div>
)
}
}
My Redux connection looks like:
const mapStateToProps = (state) => ({
data: state.ipTable.data.clicks,
})
const mapDispatchToProps = (dispatch) => {
return {
changeIpStatus: (index, status) => {
return dispatch(changeIpStatus(index, status));
},
}
}
export default connect(mapStateToProps, mapDispatchToProps)(BlockSwitcher)
When I click switcher it should re-render because the data is changed. I see that the data is changed through my console log. But it doesn't invoke re-render. Why? My component have mapStateToProps with data that changing and action import is correct (checked).
UPDATE:
This is my reducer:
const initialState = {
data: {}
}
const ipReducer = (state = initialState, action) => {
switch (action.type) {
case `SET_CLICKS`:
return {
...state,
data: action.data
}
case `CHANGE_IP_STATUS`:
let newState = Object.assign({}, state);
newState.data.clicks[action.index].status = action.status;
return newState;
default: return state;
}
}
export default ipReducer;

You can use JSON.parse(JSON.stringify(...)) method but be aware of that if your state includes a non-serializable property then you lose it.
Here is an alternative approach. You can see this method more frequently.
// map the clicks, if index match return the new one with the new status
// if the index does not match just return the current click
const newClicks = state.data.clicks.map((click, index) => {
if (index !== action.index) return click;
return { ...click, status: action.status };
});
// Here, set your new state with your new clicks without mutating the original one
const newState = { ...state, data: { ...state.data, clicks: newClicks } };
return newState;
The second alternative would be like that. Without mapping all the clicks we can use Object.assign for the clicks mutation.
const newClicks = Object.assign([], state.data.clicks, {
[action.index]: { ...state.data.clicks[action.index], status: action.status }
});
const newState = { ...state, data: { ...state.data, clicks: newClicks } };
return newState;

The problem was with deep copy of an object. In JavaScrip for copying object without any reference between them we have to use, for example:
let newState = JSON.parse(JSON.stringify(state));
not this:
let newState = Object.assign({}, state); // <-- this is do not return a standalone new object. Do not use it in your reducer.
Thanks to #kind user!
P.S This is an article with examples why Object.assign() do not work in this case.

Related

Mutating Redux Store Array of Objects in ReactJS

My store consists of an array of objects as such:
const INIT_STATE = {
users:[],
contacts : []
};
And i am attempting to change mutate the store array like this:
const App = (state = INIT_STATE, action) => {
switch (action.type) {
case BLOCK_CONTACT:
state.contacts.map((contact, index) => {
if (contact._id === action.payload) {
state.contacts[index].status = "blocked;
return {
...state
};
}
return {
...state
};
})
return {
...state
};
}
}
But my store value does not change.
When i log the state value after the if statement, i get the correct state values but my state is not being updated. I think it might have something to do with my return statement but at this point i have tried different variations with no luck.
Any help will be appreciated.
Also if your app is in the start of the way, you can read this article about jotai state management (Jotai)
I figured it out. Incase anyone else that might need help on this, here is the code...
const App = (state = INIT_STATE, action) => {
switch (action.type) {
case BLOCK_CONTACT:
var list = state.contacts.map(contact => {
var blockList = {
...contact
};
if (contact._id === action.payload) {
blockList.status = 1;
}
return blockList;
})
return {
...state,
contacts: list
};
}
}

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

Access the state of my redux app using redux hooks

I am migrating my component from a class component to a functional component using hooks. I need to access the states with useSelector by triggering an action when the state mounts. Below is what I have thus far. What am I doing wrong? Also when I log users to the console I get the whole initial state ie { isUpdated: false, users: {}}; instead of just users
reducers.js
const initialState = {
isUpdated: false,
users: {},
};
const generateUsersObject = array => array.reduce((obj, item) => {
const { id } = item;
obj[id] = item;
return obj;
}, {});
export default (state = { ...initialState }, action) => {
switch (action.type) {
case UPDATE_USERS_LIST: {
return {
...state,
users: generateUsersObject(dataSource),
};
}
//...
default:
return state;
}
};
action.js
export const updateUsersList = () => ({
type: UPDATE_USERS_LIST,
});
the component hooks I am using
const users = useSelector(state => state.users);
const isUpdated = useSelector(state => state.isUpdated);
const dispatch = useDispatch();
useEffect(() => {
const { updateUsersList } = actions;
dispatch(updateUsersList());
}, []);
first, it will be easier to help if the index/store etc will be copied as well. (did u used thunk?)
second, your action miss "dispatch" magic word -
export const updateUsersList = () =>
return (dispatch, getState) => dispatch({
type: UPDATE_USERS_LIST
});
it is highly suggested to wrap this code with { try } syntax and be able to catch an error if happened
third, and it might help with the console.log(users) error -
there is no need in { ... } at the reducer,
state = intialState
should be enough. this line it is just for the first run of the store.
and I don't understand where { dataSource } comes from.

How to immutably update/replace React Redux State with a double nested array of objects?

My redux state has a SessionList which is an array of SessionObjects, and each SessionObject has an array of HandObjects.
I use state to update individual SessionObjects by adding new HandObjects, however I want to update the redux store with my updated SessionObject (immutably, if possible).
the index of the SessionObject within SessionList is action.payload.Id (I think? i will show my SessionObject constructor)
**Adding Sessions works just fine, and I only update a Session with the session already in the store
I have tried every link that I can find, but my code is just different enough that I can't seem to update my SessionList correctly.
my reducer, with the initial state
store.js (where my reducers are)
const initialState = {
SessionList: [],
}
...
case "UPDATE_SESSION":
//action.payload is a SessionObject
//action.payload.Id is the Id that i want to update
// i want to set SessionList[action.payload.Id] = action.payload
state = {
...state,
SessionList : state.SessionList.map((item, index) => {
if (index != action.payload.id) {
return item
}
return action.payload
//This code doesn't visibly do anything that I can find
})
// *******FIRST TRY*******
// ...state,
// SessionList: {
// ...state.SessionList,
// SessionList[action.payload.id] : action.payload
//syntax error, but basically what i want to do
// }
// }
// ***********************
};
break;
SessionObject constructor
Session.js
export function SessionObject(_id, _name, _hands) {
this.id = _id, //int
this.name = _name, //string
this.HandList = _hands //array of HandObject objects
}
var defaultSession = new SessionObject(0, "default", []);
*in case i am doing this wrong,
I call
(Session.js)
this.props.navigation.state.params.updateMethod(this.state.Session); //update store
from a stack-navigator child component.
then in the parent component,
I call
(Home.js)
UpdateSession = (session) => {
this.props.updateSession(session);
};
With my dispatcher looking like:
(Home.js)
updateSession: (session) => {
dispatch({
type: "UPDATE_SESSION",
payload: session
});
}
From testing, I am somewhat sure my reducer is getting called correctly.
I want to replace the SessionObject with action.payload in the correct index of the SessionList.
EDIT *
Session.js
addItem = () => {
this.setState((state, props) => ({
Session : {
...state,
HandList: [...state.Session.HandList, new HandObject(state.HandCount)]
},
HandCount : state.HandCount + 1,
}));
this.props.navigation.state.params.updateMethod(this.state.Session); //update store
};
The state in Session.js is a SessionObject
To achieve a result of SessionList[action.payload.Id], you need to initialise SessionList as an object and not an array. Then you can update accordingly.
const initialState = {
SessionList: {},
}
case "UPDATE_SESSION":
state = {
...state,
SessionList: Object.keys(state.SessionList).reduce((acc, id) => {
if (id === action.payload.Id) {
acc[id] = action.payload;
} else {
acc[id] = state.SessionList[id];
}
return acc;
}, {});
};
Update
As requested here, following are the add, update, replace and delete handlers without changing SessionList to an object.
const initialState = {
SessionList: [],
}
Note: action.payload (wherever used) is a session object.
Add
state = {
...state,
SessionList: [...state.SessionList, action.payload];
};
Update
state = {
...state,
SessionList: state.SessionList.map((session) => {
if (session.Id === action.payload.Id) {
return Object.assign({}, session, action.payload);
}
return session;
})
};
Replace
state = {
...state,
SessionList: state.SessionList.map((session) => {
if (session.Id === action.payload.Id) {
return action.payload;
}
return session;
})
};
Delete
state = {
...state,
SessionList: state.SessionList.filter((session) => session.Id !== action.payload.Id)
};

ReactJS x Redux: Reducer not returning state values

Hi I'm new at React and Redux.
I'm met with a problem with the reducer while trying to fetch a user object from the database. But it seems like it is not returning the state to the correct place?
On my front end editProfile.js:
import { a_fetchUser } from '../../../actions/resident/actions_user';
class EditProfile extends Component {
componentDidMount() {
this.props.fetchProfile({ iduser: this.props.auth.user.iduser });
console.log(this.props.store.get('isProcessing')); // returns false
console.log(this.props.store.get('retrievedUser')); // returns empty object {} when it's supposed to return data
}
// code simplified...
const mapStateToProps = state => ({
store: state.r_fetch_user,
auth: state.authReducer
});
const mapDispatchToProps = (dispatch, store) => ({
fetchProfile: (user) => {
dispatch(a_fetchUser(user));
}
});
export const EditProfileContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(EditProfile);
}
Action actions_user.js:
import axios from 'axios';
const startFetchUser = () => ({
type: 'START_FETCH_USER',
});
const endFetchUser = response => ({
type: 'END_FETCH_USER',
response,
});
export const a_fetchUser = (user) => (dispatch) => {
dispatch(startFetchUser());
return axios.post('/rdb/getUser/', user)
.then((res) => {
console.log(res);
dispatch(endFetchUser(res));
})
.catch((err) => {
console.log(err);
dispatch(endFetchUser({ status: 'error' }));
});
};
Reducer userReducer.js:
import Immutable from 'immutable';
export const fetchUserState = Immutable.Map({
isProcessing: false,
feedbackType: null,
feedbackMsg: null,
retrievedUser: {},
});
export const r_fetch_user = (state = fetchUserState, action) => {
switch (action.type) {
case 'START_FETCH_USER':
console.log('start'); // printed
return state.set('isProcessing', true);
case 'END_FETCH_USER':
if (action.response.data.status === 'success') {
console.log(action.response.data.data[0]); // data retrieved from database successfully
return state.set('isProcessing', false).set('retrievedUser', action.response.data.data[0]);
} else {
return state.set('isProcessing', false).set('retrievedUser', {});
}
default:
return state;
}
};
My aim is to retrieve the object retrievedUser from the store. I've tried to console.log(this.props.store) on the front end and it did return a Map of the initial state, fetchUserState.
I've also tried to state.set (without returning) and it was successful so I came to a conclusion that there was something wrong with the return statement?
Additional details:
Using MERN stack.
this looks wrong:
const mapDispatchToProps = (dispatch, store) => ({
fetchProfile: (user) => {
dispatch(a_fetchUser(user));
}
});
What you need to do is to use bindActionCreators with, you can see example here and here:
function mapDispatchToProps(dispatch) {
return bindActionCreators(actionCreators, dispatch)
}
or you can also change the syntax to:
const mapDispatchToProps = (dispatch) => ({
fetchProfile: a_fetchUser(user);
});
I am not sure what exactly your state.set() method does (in reducer) but if its mutating the state, then your reducer will not remain PURE function since its changing the original state obj. So please update below reducer method to start returning new state obj which should not mutate existing state obj:
export const r_fetch_user = (state = fetchUserState, action) => {
switch (action.type) {
case 'START_FETCH_USER':
console.log('start'); // printed
return state.set('isProcessing', true);
case 'END_FETCH_USER':
if (action.response.data.status === 'success') {
console.log(action.response.data.data[0]); // data retrieved from database successfully
return state.set('isProcessing', false).set('retrievedUser', action.response.data.data[0]);
} else {
return state.set('isProcessing', false).set('retrievedUser', {});
}
default:
return state;
}
};

Categories

Resources