Redux store is updated but UI is not - reactjs

I'm having an issue with my vote score on comments. I can see in Redux Devtool that the value has changed but I need to force reload to update the UI.
Not sure why this is. I get my comments as an object with a key of the parent elements id as a key and an array inside of it.
This is then converted inside of mapStateToProps.
Heres an image showing different stages of comments.
Anyone have any idea why this is.
Cheers, Petter
Action
export function pushVoteComment(option, postId) {
const request = API.commentPostVote(option, postId)
return dispatch => {
request.then(({ data }) => {
dispatch({ type: COMMENTS_POST_VOTE, payload: data, meta: postId })
})
}
}
Reducer
const comments = (state = {}, action) => {
switch (action.type) {
case COMMENTS_GET_COMMENTS:
return {
...state,
[action.meta]: action.payload,
}
case COMMENTS_POST_VOTE:
console.log('An vote request was sent returning ', action.payload)
return { ...state, [action.payload.id]: action.payload }
default:
return state
}
}
PostDetailes ( its used here to render a PostComment )
renderComments() {
const { comments, post } = this.props
console.log('This post has these comments: ', comments)
return _.map(comments, comment => {
return (
<div key={comment.id} className="post-container">
{post ? (
<PostComment
key={comment.id}
postId={comment.id}
body={comment.body}
author={comment.author}
voteScore={comment.voteScore}
timestamp={comment.timestamp}
/>
) : (
''
)}
</div>
)
})
}
const mapStateToProps = (state, ownProps) => {
const { posts, comments } = state
return {
comments: comments[ownProps.match.params.postId],
post: posts.filter(
item => item.id === ownProps.match.params.postId && item.deleted !== true
)[0],
}
}
PostComment
<i
className="fa fa-chevron-up"
aria-hidden="true"
onClick={() => pushVoteComment('upVote', postId)}
/>
<span className="vote-amount">{voteScore}</span>
<i
className="fa fa-chevron-down"
onClick={() => pushVoteComment('downVote', postId)}
/>
export default connect(null, { pushVoteComment })(PostComment)
PS:
The reason it is built with a {parentId: [{comment1}, {comment2}]}
Is that I use it when showing all posts to see a number of comments.
return ({comments.length})
const mapStateToProps = (state, ownProps) => {
return {
comments: state.comments[ownProps.postId]
? state.comments[ownProps.postId]
: [],
}
}
Redux dev tool
Looks like this when I press the votebutton for the first time:
Then when I press again I get this:

The issue here is that it's changing the state, not thinking about the fact that I have my comment stored as
{
[postId]: [array of comments]
}
So in order to resolve it, I ended up rewriting my reducer doing it like this.
case COMMENTS_POST_VOTE:
const { parentId } = action.payload // get commentId
const commentList = [...state[parentId]] // get array of comments, but copy it
const commentIndex = commentList.findIndex(el => (el.id === payload.id)) // get index of comment
commentList[commentIndex] = action.payload // update the commentList
const updatedPost = {...state, [parentId]: commentList} // return new state
return updatedPost

Related

Redux-tookit "mutate" the object by overwrting a field vs return an immutably-updated value

I'm new to react and redux-toolkit. When I went through the redux-toolkit usage guide(https://redux-toolkit.js.org/usage/usage-guide) I found out that I could either "mutate" the object or return an immutably-updated value. However, I got confused when they actually behave differently.
This is my todoSlice
const todoSlice = createSlice({
name: 'todo',
initialState:{
todoArray: [],
},
reducers: {
addTodo(state, action) {
state.todoArray.push({ id: new Date().toISOString(), content: action.payload });
},
removeTodoMutate(state, action) {
state.todoArray = state.todoArray.filter((todo) => todo.id !== action.payload);
},
removeTodoWithImmutably(state, action) {//NOT WORKING
return state.todoArray.filter((todo) => todo.id !== action.payload);
}
},});
This is my TodoList
const TodoList = () => {
const listOfTodos = useSelector(state => state.todo.todoArray);
return (
<div>
{ listOfTodos && listOfTodos.length > 0 && (listOfTodos.map((todo) => (
<TodoItem key={todo.id} id={todo.id} todoContent={todo.content} />
)))}
{!listOfTodos || listOfTodos.length === 0 && (<h1>No Items</h1>)}
</div>
);};
This is my TodoItem
const TodoItem = (props) => {
const dispatch = useDispatch();
const deleteClickHandler = () => {
dispatch(removeTodoWithImmutably(props.id));
}
return (
<div>
<h2>
{props.todoContent}
<button onClick={deleteClickHandler}>DELETE</button>
</h2>
</div>);};
The problems were two removeTodo reducers in the todoSlice, the removeTodoMutate would work normaly, the removeTodoWithImmutably would clear out the entire TodoList once I trigger the action. When I checked the redux devtool the correct action was triggered, and the state was changed correctly.
Could anyone please explain to me why this would happened? Thank you.
Your removeTodoWithImmutably is changing the state shape itself.
Before, your state has the shape { todoArray: [ ... ] }, but then you return a new state of the shape [ ... ].
So, instead of
return state.todoArray.filter((todo) => todo.id !== action.payload);
do
return { todoArray: state.todoArray.filter((todo) => todo.id !== action.payload) };
Generally, give https://redux-toolkit.js.org/usage/immer-reducers a read.

useEffect is not triggered on redux store get updated

export default function Cart(props) {
const dispatch = useDispatch();
const branch = useSelector((state) => get(state, "vendor.currentBranch"));
const orderInput = useSelector((state) => get(state, "order.orderInputs"));
const [orderResult, setOrderResult] = useState(null);
let orderItemLength = orderInput.length
useEffect(() => {
let payload = {
branch_uuid: get(branch, "uuid"),
menu_items: orderInput,
};
dispatch(calculateOrder(payload))
.then((result) => {
setOrderResult(result);
})
.catch(() => {});
}, [orderInput]);
const deleteItem = (index) => {
remove(orderInput, (oi, i) => index === i);
dispatch({
type: ADD_ORDER_INPUT,
payload: orderInput,
});
};
return (
<div className={styles.cartbody} id="scrollstyle-4">
{map(get(orderResult, "orderItems", []), (oi, i) => {
return (
<div className={styles.cartProductBox}>
<div className={styles.productName}>
<p className={styles.textRed}>
<span className={styles.qunatity}>
{get(oi, "quantity")}
</span>
{get(oi, "item_name")}
<Popconfirm
placement="rightBottom"
title={"Are you sure you want to remove this item?"}
onConfirm={() => {
deleteItem(orderInput,i);
}}
okText="Yes"
cancelText="No"
>
<DeleteOutlined />
</Popconfirm>
</p>
<span className={styles.subItem}>
{map(get(oi, "orderItemAddons", []), (oia, i) => {
return `${i === 0 ? "" : ","} ${get(
oia,
"addon_type_name"
)} `;
})}
</span>
</div>
<div>
<div>
<span className={styles.qunatity}>
$ {round(get(oi, "item_amount"), 2)}
</span>
</div>
<div style={{ marginTop: 10 }}>
{get(oi, "orderItemAddons", []).length > 0 && (
<span className={styles.addonPrice}>
$ {round(get(oi, "addon_amount"), 2)}
</span>
)}
</div>
</div>
</div>
);
})}
</div>
);
}
This is my reducer code
import {
ADD_SERVICE,
ADD_ORDER_INPUT,
ADD_CALCULATED_ORDER,
} from "../../constants/ActionTypes";
const orderReducer = (
state = { serviceType: null, orderInputs: [] },
action
) => {
switch (action.type) {
case ADD_SERVICE:
return { ...state, serviceType: action.payload };
case ADD_ORDER_INPUT:
return { ...state, orderInputs: action.payload };
case ADD_CALCULATED_ORDER:
return { ...state, orderInputs: action.payload };
default:
return { ...state };
}
};
export default orderReducer;
In the above code when I add an order item from another component useEffect gets triggered but when I remove orderItem (as you can see in deleteItem() function) useEffect didn't get triggered on my redux store get updated, my useEffect trigger dependency is OrderInput variable as shown in code.
I also try to set dependency of useEffect to length of the array of order OrderInput
Please help me to know what I am doing wrong?
Faced the same problem & resolved thanks to Chris Answer.
Providing some elaboration, hopefully it helps the next guy:
was'nt working intially, useEffect was not trigger when redux store changed.
// Redux: Subscribe to user Store
const userStateRedux = useSelector((state: RootState) =>state.user);
useEffect(() => {
console.log('hello',userStateRedux);
}, [userStateRedux]);
Redux needs to reference another object to detect a change state
(perform an Immutable update in ur reducer)
In your reducer.js
//Mutable Update: Still referencing ur initial State
state.user=action.user;
state.token=action.token;
//Immutable Update: Referencing a new Object <- USE THIS
state={
user:action.user,
token:action.token
};

Error rendering react-redux functional component when data is deleted from firestore (useFirestoreConnect)

My ArticleList component is successfully getting & displaying the user's list of articles from firestore when I first load the app. The user can click a "Remove Article" button, which successfully removes the article from the subcollection in firestore, but it causes an error in the rendering of the react component, which seems to still be trying to render the article that was just removed and is now null. Is there something else I can do to make my react component continuously listen to the firestore data? If possible, I'd like to keep this a functional component and use hooks rather than making it a class, but I'm still learning how to use react hooks and therefore struggling a bit.
ArticleList component:
const ArticleList = (props) => {
const firestore = useFirestore();
const userId = props.auth.uid;
useFirestoreConnect([
{
collection: 'users',
doc: userId,
subcollections: [{collection: 'articles'}],
storeAs: userId + '::articles'
}
]);
const myArticles = useSelector(state => state.firestore.data[`${userId}::articles`]);
const dispatch = useDispatch();
const removeArticle = useCallback(
articleId => dispatch(removeArticleFromFirebase({ firestore }, articleId)),
[firestore]
);
if (props.auth.uid) {
return(
<div>
<h3>My Articles</h3>
<p>Currently signed in: {props.auth.email}</p>
<br/>
{myArticles ? (
Object.keys(myArticles).map(articleId => {
let article = myArticles[articleId];
let articleInformation = '';
if (articleId === props.currentPaperId) {
articleInformation =
<div>
<p>{article.year}</p>
<p>{article.description}</p>
<a target="_blank" href={article.downloadUrl}><button className='waves-effect waves-light btn-small'>See article</button></a>
<button className='waves-effect waves-light btn-small' onClick={() => {removeArticle(articleId);}}>Remove from My Articles</button>
</div>;
}
let authorName = '';
if (article.author) {
authorName = ` by ${article.author}`;
}
if (article) {
return <span key={articleId}>
<li onClick={() => {dispatch(selectArticle(articleId));}}>
<em>{article.title}</em>{authorName}
</li>{articleInformation}
</span>;
} else {
return null;
}
})
) : (
<h4>No articles yet</h4>
)
}
</div>
);
} else {
return null;
}
};
const mapStateToProps = (state) => {
return {
currentPaperId: state.currentPaperId,
auth: state.firebase.auth
};
};
export default compose(connect(mapStateToProps))(ArticleList);
And the removeArticleFromFirebase action:
export const removeArticleFromFirebase = ({ firestore }, id) => {
return (dispatch, getState) => {
const userId = getState().firebase.auth.uid;
firestore
.collection('users')
.doc(userId)
.collection('articles')
.doc(id)
.delete()
.then(() => {
console.log('Deleted article from firestore: ', id);
dispatch({ type: 'REMOVE_ARTICLE', id });
})
.catch(err => {
console.log('Error: ', err);
});
};
}
I've tried adding useState and useEffect in the ArticleList as follows (and tried having the component's return statement map through myArticlesState instead of myArticles), but no success:
const [myArticlesState, setMyArticlesState] = useState(myArticles);
useEffect(() => {
setMyArticlesState(myArticles);
}, [myArticles]);
Note: I do not currently have this article list in overall app state/redux store/props at all. This is something I was thinking of trying next, but I decided to post my question first in case I can just use hooks in this component. No other components/parts of the app need access to this particular list.
Console errors:
error image 1
error image 2
Github repo: https://github.com/jpremmel/yarp2.0
It's kind of difficult to see what's going on but it appears as though you are trying to use a property on an object that does not exist. Therefore, checking for those properties should help resolve this.
Can you try the follow code as your ArticleList?
const ArticleList = (props) => {
const firestore = useFirestore();
const userId = props.auth.uid;
useFirestoreConnect([{
collection: 'users',
doc: userId,
subcollections: [{ collection: 'articles' }],
storeAs: userId + '::articles'
}]);
const myArticles = useSelector(state => state.firestore.data[`${userId}::articles`]);
const dispatch = useDispatch();
const removeArticle = useCallback(articleId => dispatch(removeArticleFromFirebase({ firestore }, articleId)), [firestore]);
if (props.auth.uid) {
return (
<div>
<h3>My Articles</h3>
<p>Currently signed in: {props.auth.email}</p>
<br />
{myArticles ? (
Object.keys(myArticles).map(articleId => {
let article = myArticles[articleId];
let articleInformation = '';
if (article) {
if (
articleId === props.currentPaperId &&
article.hasOwnProperty('year') &&
article.hasOwnProperty('description') &&
article.hasOwnProperty('downloadUrl')
) {
articleInformation =
<div>
<p>{article.year}</p>
<p>{article.description}</p>
<a target="_blank" href={article.downloadUrl}><button className='waves-effect waves-light btn-small'>See article</button></a>
<button className='waves-effect waves-light btn-small' onClick={() => { removeArticle(articleId); }}>Remove from My Articles</button>
</div>;
}
let authorName = '';
if (article.hasOwnProperty('author') && article.author) {
authorName = ` by ${article.author}`;
}
if (article.hasOwnProperty('title') && article.title) {
return <span key={articleId}>
<li onClick={() => { dispatch(selectArticle(articleId)); }}>
<em>{article.title}</em>{authorName}
</li>{articleInformation}
</span>;
} else {
return null;
}
}
})
) : (
<h4>No articles yet</h4>
)
}
</div>
);
} else {
return null;
}
};
const mapStateToProps = (state) => {
return {
currentPaperId: state.currentPaperId,
auth: state.firebase.auth
};
};
export default compose(connect(mapStateToProps))(ArticleList);
Can you show us the error? I think it's about the state not being an array after you delete your data just initialize your state with an empty array like this :
Const= [articlesdata,setArticlesData]=useState([])
And leave the useEffect as it is

react-redux props is not updated

I'm trying to build a simple todo-app using react-redux. Problem is when I'm trying to update data, it would not update in view. My code is given below:
actions
export const listTodo = () => { type: actionTypes.LIST_TODO }
export const updateTodo = ( payload, index ) => { type: actionTypes.UPDATE_TODO, payload, index }
Reducers
const INITIAL_STATE = {
all: [{
name: 'init',
status: false,
lastUpdate: new Date().toISOString()
}]
}
const listTodo = ( state, action ) => {...state, all: state.all }
const updateTodo = ( state, action ) => {
const listTodo = {...state, all: state.all }; // init data
// find data
let todo = listTodo.all.filter( (todo, index) => index === action.index );
// update data
todo.name = action.payload.name;
todo.status = action.payload.status;
todo.lastUpdate = new Date().toISOString();
listTodo.all[ action.index ] = todo;
// return data
return {
...state,
all: listTodo.all
}
}
export default ( state = INITIAL_STATE, action) => {
switch( action.type ) {
case LIST_TODO:
return listTodo( state, action );
case UPDATE_TODO:
return updateTodo( state, action );
default:
return state;
}
}
In below code (Components/list.js), I just fetch all todo-list, and then print all list using ShowList.
Components/list.js
import ShowList from '../container/showList';
class TodoList extends Component {
renderTodoList() {
return this.props.all.map( (todo, index) => {
return (
<ShowList key={index} index={index} todo={todo} />
);
});
}
render() {
return <ul> { this.renderTodoList() } </ul>
}
}
const mapStateToProps = ( state ) => { all: state.todo.all };
export default connect( mapStateToProps ) ( TodoList );
In below code (container/showList.js), todo list is shown using <li /> and also have a checkbox, when user click on checkbox, handleCheckbox will trigger, and will update todo-list. I believe data is updated correctly, but it is not updated on html. In browser, todo-list remain same as before.
container/showList.js
class ShowList extends Component {
handleCheckbox = ( ) => {
const { todo, index } = this.props;
todo.status = !todo.status;
todo.lastUpdate = new Date().toISOString();
this.props.onUpdateTodo( todo, index );
}
render() {
const { todo, index } = this.props;
return (
<li> <input type="checkbox" onChange={ this.handleCheckbox } checked={todo.status} /> {todo.name} # {todo.status.toString()} # { todo.lastUpdate } </li>
)
}
}
const mapDispatchToProps = ( dispatch ) => onUpdateTodo: ( todo, index ) => dispatch( actions.updateTodo( todo, index ) )
export default connect(null, mapDispatchToProps) (ShowList);
How can I solve this problem? Thanks in Advance.
Your problem was in your reducers file. Whenever you executed updateToDo() you were not actually updating the the existing todos, you would just add a new property to your state with the new changes. This created layers and layers of properties without actually updating the first-layer. And since your components were only connected to the first-layer, it would never get the updated state.
I've updated a codesandbox for your reference: https://codesandbox.io/s/lively-flower-mwh79
You can update your reducers to something like this and then your code works completely fine:
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case "LIST_TODO":
return listTodo(state, action);
case "UPDATE_TODO":
return {
...state,
all: state.all.map((todo, index) => {
if (index == action.index) {
return {
...todo,
status: todo.status,
lastUpdate: new Date().toISOString()
};
} else {
return todo;
}
})
};
default:
return state;
}
};
Your problem is in this line,
export const updateTodo = ( payload, index ) => { type: actionTypes.UPDATE_TODO, payload, index }
Redux Action will only take 2 parameters, type and payload respectively.
Here you are passing 3 parameters which is wrong. Remove your index parameter, then your action becomes like this,
export const updateTodo = ( payload, index ) => { type: actionTypes.UPDATE_TODO, payload } //payload = your updated todo list
Pass only update todo list to your action,
this.props.onUpdateTodo( todo );
Finally in your reducer only do this,
return Object.assign(state,action.todo) // This will merge your old state with updated todo list and eventually you will get a updated list.
See more obout Object.assign here

How to change a value inside array of object in Redux

I have stored an array of object in Redux State and inside each object there is a key named price. Now when I increment the quantity button I need to access the object that has the key inside redux and change the price value. I was able to do that but it's not working properly the price is being changed but a new object is being added in the state of Redux you can see it in the screenshot below. hope I was able to explain the problem clearly. if not please let know so I can explain more.
Cart Component
increment(e, item){
let qty = e.target.previousElementSibling.textContent;
qty++;
e.target.previousElementSibling.textContent = qty;
this.props.changePrice(item);
}
<div>
<input onClick={(e) =>this.decrement(e)} type="submit" value="-"/>
<span>1</span>
<input onClick={(e) => this.increment(e, item)} type="submit" value="+"/>
</div>
function mapStateToProps(state){
return({
itemlist: state.rootReducer
})
}
function mapDispatchToProps(dispatch) {
return({
removeItem: (item)=>{
dispatch({type: 'removeCart', payload: item})
},
changePrice: (item)=>{
dispatch({type: 'changePrice', payload: item})
}
})
}
export default connect(mapStateToProps, mapDispatchToProps)(Cart);
Reducer Component
const changePrice = (itemArray, item)=>{
let newObject = {};
let filteredObject = itemArray.filter(reduxItem => reduxItem.id === item.id);
let newprice = filteredObject[0].price + filteredObject[0].price;
filteredObject[0].price = newprice;
newObject = filteredObject;
const something = ([...itemArray, newObject]);
return something;
}
const reducer = (state = [], action) =>{
switch (action.type) {
case 'Add':
return [...state, action.payload]
case 'removeCart':
const targetItemIndex = state.indexOf(action.payload);
return state.filter((item, index) => index !== targetItemIndex)
case 'changePrice':
return changePrice(state, action.payload)
default:
return state;
}
}
export default reducer;
filteredObject is an array. You override the newObject to be an array in this statement newObject = filteredObject. So the newObject is an array ( in [...itemArray, newObject] ) rather than an object. Keep things simple without unnecessary complexity.You can use Array.map. So do this instead
const changePrice = (itemArray, item) => {
return itemArray.map(reduxItem => {
if(reduxItem.id === item.id){
reduxItem.price = reduxItem.price + reduxItem.price
}
return reduxItem
});
};
See this for more info https://redux.js.org/recipes/structuring-reducers/immutable-update-patterns#inserting-and-removing-items-in-arrays
Hope this helps!
Instead of mutating the state.
// use this
const newState = Object.assign({},state);
We can create a new state and now if you do this, this works fine.
This avoids mutating state.

Resources