useEffect is not triggered on redux store get updated - reactjs

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

Related

Reactjs Sort By Price

I want to sort my product after I click the text "Sort by Log To High",
I put "product.sort((a,b) => a.productPrice > b.productPrice ? 1 : -1)" in a onClick function but it does not work. Now it works only if I put in the const displayProduct.
Any tutorial or video may I refer to? Thanks for helping.
export const Product = () =>{
const [product, setProduct] = useState([]);
const [pageNumber, setPageNumber] = useState(0)
const productPerPage = 12
const pagesVisited = pageNumber * productPerPage
const displayProduct = product
.slice(pagesVisited,pagesVisited + productPerPage)
.map(product => {
return(
<div className='imageContainer ' key={product.id}>
<img src={PopularOne} className="image"/>
<div className='productName'>
<Link style={{ textDecoration:'none' }} to="/productsDetails" state={{ product:product }}>{product.productName}</Link>
</div>
<div className='productPrice'>
<h3 >RM{product.productPrice}</h3>
</div>
</div>
)
})
//product.sort((a,b) => a.productPrice > b.productPrice ? 1 : -1)
const pageCount = Math.ceil(product.length/ productPerPage)
const changePage = ({selected}) =>{
setPageNumber(selected)
}
useEffect(() => {
const fetchData = async () => {
try {
const res = await axios.get(`/routes/getProduct`);
console.log(res)
setProduct(res.data);
} catch (err) {
console.log(err);
}
};
fetchData();
}, []);
return(
<div className='product'>
<div>
<button><h3>Sort By Low to High</h3></button>
<h3>Sort By High to Low</h3>
</div>
<div className='productContainer'>
{displayProduct}
</div>
<ReactPaginate
previousLabel={""}
nextLabel={""}
breakLabel="..."
pageRangeDisplayed={5}
pageCount={pageCount}
onPageChange={changePage}
containerClassName={"pagination"}
breakClassName={"break"}
pageClassName={"page-item"} //li
pageLinkClassName={"page-link"} //a
activeLinkClassName={"page-link-active"}
/>
<Footer/>
</div>
)
}
When you use the useState function provided by React it returns 2 things, first is the state variable, and second is the updater function. In your case the state is product and the updater is setProduct.
It doesn't work because you are trying to modify the state variable, just use the updater function, and it will work.
For example:
setProduct(prevState => {
let newState = [...prevState];
newState.sort((a, b) => a.productPrice > b.productPrice ? 1 : -1);
return newState;
});
Updater function provides the previous state, in this case it's named prevState.
Shallow clone the array and store it in the newState
variable.
Mutate the newState array via the sort method.
Return the newState. By returning here we tell React to update the state to the value of newState.

Updating state being one step behind WITHOUT using useEffect?

Note: I've seen people suggesting useEffect to this issue but I am not updating the state through useEffect here..
The problem I am having is that when a user selects id 7 for example, it triggers a function in App.tsx and filters the todo list data and update the state with the filtered list. But in the browser, it doesn't reflect the updated state immediately. It renders one step behind.
Here is a Demo
How do I fix this issue (without combining App.tsx and TodoSelect.tsx) ?
function App() {
const [todoData, setTodoData] = useState<Todo[]>([]);
const [filteredTodoList, setFilteredTodoList] = useState<Todo[]>([]);
const [selectedTodoUser, setSelectedTodoUser] = useState<string | null>(null);
const filterTodos = () => {
let filteredTodos = todoData.filter(
(todo) => todo.userId.toString() === selectedTodoUser
);
setFilteredTodoList(filteredTodos);
};
useEffect(() => {
const getTodoData = async () => {
console.log("useeffect");
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/todos"
);
setTodoData(response.data);
setFilteredTodoList(response.data);
} catch (error) {
console.log(error);
}
};
getTodoData();
}, []);
const handleSelect = (todoUser: string) => {
setSelectedTodoUser(todoUser);
filterTodos();
};
return (
<div className="main">
<TodoSelect onSelect={handleSelect} />
<h1>Todo List</h1>
<div>
{" "}
{filteredTodoList.map((todo) => (
<div>
<div>User: {todo.userId}</div>
<div>Title: {todo.title}</div>
</div>
))}
</div>
</div>
);
}
In TodoSelect.tsx
export default function TodoSelect({ onSelect }: TodoUsers) {
const users = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"];
return (
<div>
<span>User: </span>
<select
onChange={(e) => {
onSelect(e.target.value);
}}
>
{users.map((item) => (
<option value={item} key={item}>
{item}
</option>
))}
</select>
</div>
);
}
There's actually no need at all for the filteredTodoList since it is easily derived from the todoData state and the selectedTodoUser state. Derived state doesn't belong in state.
See Identify the Minimal but Complete Representation of UI State
Let’s go through each one and figure out which one is state. Ask three
questions about each piece of data:
Is it passed in from a parent via props? If so, it probably isn’t state.
Does it remain unchanged over time? If so, it probably isn’t state.
Can you compute it based on any other state or props in your component? If so, it isn’t state.
Filter the todoData inline when rendering state out to the UI. Don't forget to add a React key to the mapped todos. I'm assuming each todo object has an id property, but use any unique property in your data set.
Example:
function App() {
const [todoData, setTodoData] = useState<Todo[]>([]);
const [selectedTodoUser, setSelectedTodoUser] = useState<string | null>(null);
useEffect(() => {
const getTodoData = async () => {
console.log("useeffect");
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/todos"
);
setTodoData(response.data);
} catch (error) {
console.log(error);
}
};
getTodoData();
}, []);
const handleSelect = (todoUser: string) => {
setSelectedTodoUser(todoUser);
};
return (
<div className="main">
<TodoSelect onSelect={handleSelect} />
<h1>Todo List</h1>
<div>
{filteredTodoList
.filter((todo) => todo.userId.toString() === selectedTodoUser)
.map((todo) => (
<div key={todo.id}>
<div>User: {todo.userId}</div>
<div>Title: {todo.title}</div>
</div>
))
}
</div>
</div>
);
}
State update doesn't happen synchronously! So, selectedTodoUser inside filterTodos function is not what you're expecting it to be because the state hasn't updated yet.
Make the following changes:
Pass todoUser to filterTodos:
const handleSelect = (todoUser: string) => {
setSelectedTodoUser(todoUser);
filterTodos(todoUser);
};
And then inside filterTodos compare using the passed argument and not with the state.
const filterTodos = (todoUser) => {
let filteredTodos = todoData.filter(
(todo) => todo.userId.toString() === todoUser
);
setFilteredTodoList(filteredTodos);
};
You probably won't need the selectedTodoUser state anymore!

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.

React: updating state after deletion

I'm trying to update elements after deletion, without refreshing a page. Currently, if delete a record, need to refresh a page to see the result. As I understand, need to update useState, but I do not understand how to do it. If I loop useEffect it works but slowly, but I think it's not the best idea to loop get response.
Get all records from a database.
const PostsGetUtill = () => {
const [posts, setPosts] = useState([]);
const fetchPosts = () => {
axios.get("api/v1.0/post/get").then(response => {
console.log(response.data);
setPosts(response.data);
}).catch(function (error) {
if (error.response) {
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
console.log(error.request);
} else {
console.log('Error', error.message);
}
console.log(error.config);
});
};
useEffect(() => {
fetchPosts();
}, []); // }, [fetchPosts]); <--- working well with loop
return (
<section className="container-post">
<PostMansonry posts={posts} columns={3} />
</section>
);
};
export default PostsGetUtill;
Sort and map records
export default function PostMansonry({ posts, columns }) {
return (
<section className="masonry" style={{ gridTemplateColumns: `repeat(${columns}, minmax(275px, 1fr))` }}>
{posts.sort((a, b) => a.zonedDateTime < b.zonedDateTime ? 1 : -1).map((posts, index) =>
<MasonryPost {...{ posts, index, key: index }} />)
}
</section>
)
}
Put data to the card
export default function MasonryPost({ posts, index }) {
return (
<div key={index} className="masonry-post">
<div className="card">
<div className="card-body">
<h5 className="card-title">{posts.title}</h5>
<p className="card-text">{posts.description}</p>
<p className="card-text"><small className="text-muted"> {posts.zonedDateTime}</small></p>
<div><button type="button" onClick={(e) => PostsDeleteUtill(posts.post_Id)} className="btn btn-danger">Delete</button></div>
</div>
</div>
</div>
)
}
Deleting
const PostsDeleteUtill = async (post_Id) => {
axios.delete(`api/v1.0/post/delete/${post_Id}`).then(response => {
console.log(response);
}).catch((error) => {
if (error.response) {
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
console.log(error.request);
} else {
console.log('Error', error.message);
}
console.log('error config', error.config);
});
};
export default PostsDeleteUtill;
Basically what you need to do is, in your PostsDeleteUtill function, in the promise return of your axios.delete, you need to update your posts state, which is set in PostsGetUtill.
In order to do that, you have 2 options:
Use a global state (React Context, Redux, etc)
Pass your setPosts handle all the way to your PostsDeleteUtill
I think option 1 is a bit cleaner for your specific case, but if you don't need global state anywhere else in your project, maybe it is fine to have a not so clean solution instead of implementing the whole global state structure for only one thing.
Option 1 pseudo code:
Your PostsGetUtill component would use a global state instead of local state:
const PostsGetUtill = () => {
// Remove this:
// const [posts, setPosts] = useState([]);
const fetchPosts = () => {
axios.get("api/v1.0/post/get").then(response => {
console.log(response.data);
// Instead of a local "setPosts" you would have a global
// "setPosts" (in Redux, this would be a dispatch)
dispatch({type: "PUT_POSTS", action: response.data})
}).catch(function (error) {
// No changes here...
});
};
// This runs only the first time you load this component
useEffect(() => {
fetchPosts();
}, []);
// Use your global state here as well:
return (
<section className="container-post">
<PostMansonry posts={globalState.posts} columns={3} />
</section>
);
};
export default PostsGetUtill;
In your PostsDeleteUtill function:
const PostsDeleteUtill = async (post_Id) => {
axios.delete(`api/v1.0/post/delete/${post_Id}`).then(response => {
// Update global state here. Probably filter the data to remove
// the deleted record
const updatedPosts = globalState.posts.filter(post => post.id !== response.data.id)
}).catch((error) => {
// No changes here
});
};
export default PostsDeleteUtill;
Option 2 pseudo code:
In your PostsGetUtill component, create and pass on a handleRemovePost:
// Your existing code ...
const handleRemovePost = (postID) => {
const filteredPosts = posts.filter(post => post.id !=== postID)
setPosts(filteredPosts)
}
return (
<section className="container-post">
<PostMansonry posts={posts} columns={3} handleRemovePost={handleRemovePost} />
</section>
);
In your PostMansonry, pass on again your handleRemovePost
export default function PostMansonry({ posts, columns, handleRemovePost }) {
return (
// Your existing code ...
<MasonryPost {...{ posts, index, key: index, handleRemovePost }} />)
)
}
Again in your MasonryPost
export default function MasonryPost({ posts, index, handleRemovePost }) {
return (
// Your existing code ...
<button type="button" onClick={(e) => PostsDeleteUtill(posts.post_Id, handleRemovePost)} className="btn btn-danger">Delete</button>
)
}
And finally:
const PostsDeleteUtill = async (post_Id, handleRemovePost) => {
axios.delete(`api/v1.0/post/delete/${post_Id}`).then(response => {
handleRemovePost(response);
})
};
PS: Please note that I only added a pseudo-code as a reference, trying to point out specific parts of the code that needs to be updated. If you need more information about global state, you can check React Context and Redux

Redux store is updated but UI is not

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

Resources