I have a .map function in a component:
let recentItemsMarkup = loading ? (
<p>Items are loading...</p>
) : (
items.map(item => (
<ShoppingItem
key={item._id}
id={item._id}
name={item.name}
createdAt={item.date}
/>
))
);
When I post an item, sometimes -not always- it duplicates on the view, but not in Database. DB is working properly, but somehow, after I post an item, it is not always sets the items properly,
here are the action and the reducer:
//post item
export const postItem = newItem => dispatch => {
dispatch({ type: LOADING_UI });
axios
.post("http://localhost:5000/api/items", newItem)
.then(res => {
dispatch({
type: POST_ITEM,
payload: res.data
});
})
.catch(err => {
dispatch({
type: SET_ERRORS,
payload: err.response.data
});
});
};
and the reducer:
const initialState = {
items: [],
item: {},
loading: false
};
export default (state = initialState, action) => {
switch (action.type) {
case LOADING_ITEMS:
return {
...state,
loading: true
};
case GET_ITEMS:
return {
...state,
items: action.payload,
loading: false
};
case POST_ITEM:
return {
...state,
items: [action.payload, ...state.items]
};
case DELETE_ITEM:
return {
...state,
items: state.items.filter(item => item._id !== action.payload)
};
default:
return state;
}
};
I checked the Ids and Database, everything is ok, ids are unique vs. Why this happening?
screenshot
and also Shopping Item component:
class ShoppingItem extends Component {
render() {
const { authenticated } = this.props.user;
const { name, createdAt, classes, id } = this.props;
const deleteButton = authenticated ? (
<DeleteItem id={id} />
) : null;
return (
<Card className={classes.card}>
<CardContent>
<Typography variant="body1" color="textPrimary">
{this.props.id}
</Typography>
<Typography variant="body1" color="textPrimary">
{name}
</Typography>
{deleteButton}
<Typography color="textSecondary">
{dayjs(createdAt).format("h:mm a, MMM DD YYYY")}
</Typography>
</CardContent>
</Card>
);
}
}
ShoppingItem.propTypes = {
name: PropTypes.string.isRequired,
};
const mapStateToProps = state => ({
user: state.user
});
const actionsToProps = {
deleteItem
};
export default connect(
mapStateToProps,
actionsToProps
)(withStyles(styles)(ShoppingItem));
It seems like your backend returns an array of items together with the new one, so in that case you'd just set them on the state, instead of adding to existing items:
case POST_ITEM:
return {
...state,
items: action.payload
};
It is because of
case POST_ITEM:
return {
...state,
items: [action.payload, ...state.items]
};
just send the item which needs to add & then handle insertion at backend.
Ok, I solved the problem,
My failure is, I added "getItems" to "postItem" function which causes duplication because when I post an item it already refreshes the page and loads the Items from componentDidMount method. So it seems I didn't understand the logic very well, which is when a state or props change, the page refreshes automatically.
Related
I'm using React Redux and want to be able to change the title and description of a post, using the onChange method. When only using React the way you would do this is that you keep an useState which you change whenever a change occurs, but I can't seem to get it to work with using redux in react. Instead of the state changing the original title, and description remains and cannot be changed.
From what I have read the basic idea is to have a listener on the input (onChange, usually) and have that fire a redux action. You then have the action tell the reducer to make the change to the store.
I have tried doing this, but could make it work correctly. What am I doing wrong and how do you solve it? I'm also wondering how do I specify that I want to change either title or description when using onChange, or do I simply send everything in post each time a change occurs?
This is what the redux state looks like when entering a post:
{
auth: {
isSignedIn: true,
user: {
id: '624481f22566374c138cf974',
username: 'obiwan',}
},
posts: {
'62448632b87b223847eaafde': {
_id: '62448632b87b223847eaafde',
title: 'hellothere',
desc: 'its been a long time since I heard that name...',
username: 'vorbrodt',
email: 'example#gmail.com',
categories: [],
createdAt: '2022-03-30T16:32:50.158Z',
updatedAt: '2022-03-30T16:32:50.158Z',
__v: 0
}
},
}
Here is where the onChange happens.
Post.js
import { getPostById, editPost } from "../actions";
const Post = ({ getPostById, editPost, username }) => {
const [updateMode, setUpdateMode] = useState(false);
let { id } = useParams();
let post = useSelector((state) => state.posts[id]);
const handleInputChange = (e) => {
try {
editPost(e.target.value);
} catch (err) {}
};
return (
<div className="post">
<div className="post-wrapper">
{updateMode ? (
<input
type="text"
value={post.title}
className="post-title-input"
autoFocus
onChange={(e) => handleInputChange(e)}
/>
) : (
<h1 className="post-title">
{post.title}
</h1>
)}
<div className="desc-area">
{updateMode ? (
<textarea
className="post-desc-input"
value={post.desc}
onChange={(e) => handleInputChange(e)}
/>
) : (
<p className="post-desc">{post.desc}</p>
)}
</div>
</div>
</div>
);
};
const mapStateToProps = (state) => {
return { username: state.auth.user.username };
};
export default connect(mapStateToProps, { getPostById, editPost })(Post);
Here is the action creator:
//edit post in redux state
const editPost = (postValues) => (dispatch) => {
dispatch({ type: EDIT_POST, payload: postValues });
};
And here is the reducer which is suppose to change the state.
postReducer.js
import _ from "lodash";
import { GET_POSTS, GET_POST, CREATE_POST, EDIT_POST } from "../actions/types";
function postReducer(state = {}, action) {
switch (action.type) {
case GET_POSTS:
return { ...state, ..._.mapKeys(action.payload, "_id") };
case GET_POST:
return { ...state, [action.payload._id]: action.payload };
case CREATE_POST:
return { ...state, [action.payload._id]: action.payload };
case EDIT_POST:
//here the change should occur, not sure how to specify if title or desc should
//change
return { ...state, [action.payload._id]: action.payload };
default:
return state;
}
}
export default postReducer;
Hey there something like this should be of help
const handleInputChange = (e, key, id) => {
try {
editPost({ [key]: e.target.value, id });
} catch (err) {}
};
Usage
<textarea
className="post-desc-input"
value={post.desc}
onChange={(e) => handleInputChange(e, "title", post.id)}
/>
action
const editPost = (postValues) => (dispatch) => {
dispatch({ type: EDIT_POST, payload: postValues });
};
Reducer
case EDIT_POST:
//here we destructure the id and return the data without the id cause we //need it below
const {id, ...newData} = action.payload
const indexToUpdate = state.posts.find(post => post.id === id)
const newPostsData = [...state.posts]
//Here we update the actual object and its property that is in the state at //the specific value
newPostsData[indexToUpdate] = {...newPostData[indexToUpdate], {...newData}
return { ...state, posts: newPostsData};
EDIT:
I fixed the problem in the reducer...changed this:
case ADD_LIST_ITEM:
return {
...state,
lists: {
...state.lists.map(list =>
list._id === payload.id
? { ...list, listItems: payload.data }
: list
)
},
loading: false
};
to this:
case ADD_LIST_ITEM:
return {
...state,
lists: [
...state.lists.map(list =>
list._id === payload.id
? { ...list, listItems: payload.data }
: list
)
],
loading: false
};
Stupid error on my part.
I have a MERN todo application using redux for state management and useEffect() for UI updates (all functional instead of class-based components). However, when I change state in the redux store, the UI does not update. This seems to only happen during an update triggered by a post request from the front end to the backend, where I pass data to an action, which is handled in a reducer (a js file rather than the useReducer() hook in this app). My backend will update properly, but the UI will crash.
What happens is, I input, say, a new list item in a given todo list, and the error I get is:
Uncaught TypeError: list.lists.map is not a function
at Dashboard (Dashboard.jsx:32)
I'm not sure where to use an additional useEffect(), if needed, or if there's a problem in my reducer...here's the relevant flow (removed all className declarations and irrelevant parts):
/* Dashboard.jsx */
// imports //
const Dashboard = ({ auth: { user }, list, getLists }) => {
useEffect(() => {
getLists();
}, [getLists]);
return (
<>
<p>Lists...</p>
{list.lists &&
list.lists.map(list => <List key={list._id} list={list} />)}
</>
);
};
Dashboard.propTypes = {
getLists: PropTypes.func.isRequired,
list: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
list: state.list
});
export default connect(mapStateToProps, { getLists })(Dashboard);
/* List.jsx */
// imports
const List = ({ list, addListItem, getLists }) => {
const [text, setText] = useState('');
useEffect(() => {
getLists();
}, []);
const handleAddItem = e => {
e.preventDefault();
addListItem(list._id, { text });
setText('');
};
return (
<div>
{list.listItems &&
list.listItems.map((item, index) => (
<ListItem
key={index}
item={item}
listId={list._id}
itemIndex={index}
/>
))}
<div>
<form onSubmit={handleAddItem}>
<input
type="text"
name="text"
placeholder="add a to-do item"
value={text}
onChange={e => setText(e.target.value)}
/>
<input type="submit" value="add" />
</form>
</div>
</div>
);
};
List.propTypes = {
addListItem: PropTypes.func.isRequired,
getLists: PropTypes.func.isRequired
};
export default connect(null, {
addListItem,
getLists
})(List);
/* list.actions.js */
// imports
export const addListItem = (listId, text) => async dispatch => {
try {
const res = await api.post(`/lists/${listId}`, text); // returns all list items after adding new item
dispatch({
type: ADD_LIST_ITEM,
payload: { id: listId, data: res.data }
});
} catch (err) {
dispatch({
type: LIST_ERROR,
payload: { message: err.response.statusText, status: err.response.status }
});
}
};
/* list.reducer.js */
// imports
const initialState = {
lists: [],
list: null,
loading: true,
error: {}
};
const list = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case GET_LISTS:
return { ...state, lists: payload, loading: false };
case LIST_ERROR:
return { ...state, error: payload, loading: false };
case ADD_LIST_ITEM:
return {
...state,
lists: {
...state.lists.map(list =>
list._id === payload.id
? { ...list, listItems: payload.data }
: list
)
},
loading: false
};
default:
return state;
}
};
export default list;
I assume when creating your app's store, you are passing list as rootReducer,
Meaning your app's main state is exactly the state list is managing.
So if you need to access property lists of the state, you need to do it like this:
const mapStateToProps = state => ({
lists: state.lists /// state in here is exactly the state of list reducer
});
Now, in Dashboard lists is that array that you manipulate in list reducer.
Also, you have defined a property also named list in list reducer. It is initially defined to be null, also in the reducer, you never change it:
const initialState = {
lists: [],
list: null, /// none of actions ever change this, meaning it's currently useless.
loading: true,
error: {}
};
I have textfields for 5 rows (each row having textfields of same name as it is iterated for 5 times) and i want to do validation that the textfield should not be empty and and i'm using redux for generationg that error. But If i click on one of the textbox and left it empty, all the 5 textboxes are showing error.
Form
const ItemInfo = ({
errors, // errors generated from redux getting from parent Component
classes,
index,
getItem,
validateInput,
}) => {
const [item, handleItem] = React.useState({});
const [err, setErr] = React.useState({});
React.useEffect(() => {
if (errors) setErr(errors);
}, [errors]);
React.useEffect(() => {
const { rate, quantity, discount } = item;
if (item.item && rate && quantity && discount) {
//Send item details to parent PurchaseForm
//console.log(index, item);
getItem(item);
}
}, [item]);
const handleInput = (event) => {
handleItem((items) => ({
...items,
[event.target.name]: event.target.value,
}));
};
return (
<tr xs={12}>
<td>{index}</td>
<td>
<TextField
id="item"
name="item"
type="item"
className={classes.itemField}
helperText={err.item}
error={err.item ? true : false}
value={item.item || ""}
onChange={(event) => handleInput(event)}
onBlur={(e) => validateInput(e)}
onFocus={(e) => removeError(e)}
fullWidth
/>
</td>
<td>
// Similarly other form components
</td>
<td>
<Button color="red" onClick={() => delItem()}>
×
</Button>
</td>
</tr>
);
};
export default withStyles(styles)(ItemInfo);
Reducer
import {
SET_ERRORS,
CLEAR_ERRORS,
SET_AUTHENTICATED,
LOADING,
} from "./types";
const initialState = {
authenticated: false,
loading: false,
errors: null,
};
export default function (state = initialState, action) {
switch (action.type) {
case SET_AUTHENTICATED:
return {
...state,
authenticated: action.payload,
};
case LOADING:
return {
...state,
loading: action.payload,
errors: null,
};
case SET_ERRORS:
return {
...state,
loading: false,
errors: action.payload,
};
case CLEAR_ERRORS:
return {
...state,
loading: false,
errors: null,
};
default:
return state;
}
}
Difficulty I'm facing
Please suggest me how to design reducer such that error appears only on the textbox where it should be.
Yes, i got the way to get the desired output. I maintained an array of error in redux state and store the error according to the index of the row and then showing the error of particular index of the row.
I have this component (associated with the route /product/:id in App.js)
import React, { useEffect} from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Spinner from '../components/Spinner';
import { getProductDetails } from '../actions/productActions';
const ProductDetailsScreen = ({ match }) => {
const { product, loading, error } = useSelector(
(state) => state.productDetails
);
console.log(product);// < ------- This outputs the previously display product, then undefined, then the targeted product
const dispatch = useDispatch();
useEffect(() => {
dispatch(getProductDetails(match.params.id));
}, [match]);
return (
<div>
<div className='container'>
{loading ? (
<Spinner />
) : error ? (
<h1>{error}</h1>
) : (
<div>
<h1>{product.name}</h1>
<span>
<strong>Price: </strong>${product.price}
</span>
</div>
)}
</div>
<div>
);
};
export default ProductDetailsScreen;
In another component I have this
<Link to={`/product/${_id}`}>View Details</Link>
which is supposed to go to ProductDetailsScreen component, and fill the screen with the product's details based on the _id passed. However, although the redux state is populated correctly from the backend with the product's details whose _id is passed, the components' elements aren't filled with the product's details as it is supposed to be, although I am checking if the product is done loading and there is no error. The component seems to be rendered 3 times based on the console.log(product). The first time it outputs the previously displayed product (I think I need to clear the state), then undefined, and then the target product!
Why? What am I doing wrong?
EDIT1:
the reducer
export const productDetailsReducer = (state = { product: {} }, action) => {
const { type, payload } = action;
switch (type) {
case PRODUCT_GET_REQUEST:
return { loading: true };
case PRODUCT_GET_SUCCESS:
return { loading: false, success: true, product: payload };
case PRODUCT_GET_FAIL:
return {
loading: false,
error: payload,
};
case PRODUCT_GET_RESET:
return { product: {} };
default:
return state;
}
};
and the action
export const getProductDetails = (productId) => async (dispatch) => {
try {
dispatch({
type: PRODUCT_GET_REQUEST,
});
const { data } = await axios.get(`/api/products/${productId}`);
dispatch({
type: PRODUCT_GET_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: PRODUCT_GET_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
Please try below.
Reducer
export const productDetailsReducer = (state = { product: {} }, action) => {
const { type, payload } = action;
switch (type) {
case PRODUCT_GET_REQUEST:
return { ...state, loading: true };
case PRODUCT_GET_SUCCESS:
return { ...state, loading: false, success: true, product: payload };
case PRODUCT_GET_FAIL:
return {
...state,
loading: false,
error: payload
};
case PRODUCT_GET_RESET:
return { ...state, product: {} };
default:
return state;
}
};
I am following a tutorial trying to learn Redux. I got the first action working, which is a simple GET API call, but am stuck on the next action I'm trying to create. The code looks like the following:
In the Component:
class ShoppingList extends Component {
componentDidMount() {
this.props.getItems();
}
handleClick = id => {
console.log("component " + id);
this.props.deleteItem(id);
};
render() {
const { items } = this.props.item;
return (
<Container>
<ListGroup>
<TransitionGroup className="shoppingList">
{items.map(({ id, name }) => (
<CSSTransition key={id} timeout={500} classNames="fade">
<ListGroupItem>
<Button
className="button1"
color="danger"
size="sm"
onClick={e => this.handleClick(id, e)}
>
×
</Button>
{name}
</ListGroupItem>
</CSSTransition>
))}
</TransitionGroup>
</ListGroup>
</Container>
);
}
}
ShoppingList.propTypes = {
getItems: PropTypes.func.isRequired,
item: PropTypes.object.isRequired,
deleteItem: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
item: state.item
});
export default connect(mapStateToProps, { getItems, deleteItem })(ShoppingList);
In my reducer:
const initialState = {
items: [
{ id: 3, name: "Eggs" },
{ id: 4, name: "Milk" },
{ id: 5, name: "Steak" },
{ id: 6, name: "Water" }
]
};
export default function(state = initialState, action) {
switch (action.type) {
case GET_ITEMS:
return {
...state
};
case DELETE_ITEM:
console.log("reducer");
return {
...state,
items: state.items.filter(item => item.id !== action.id)
};
default:
return state;
}
}
In my actions file:
export const getItems = () => {
return {
type: GET_ITEMS
};
};
export const deleteItem = id => {
console.log("actions");
return {
type: DELETE_ITEM,
payload: id
};
};
However, when I click on the button to try to delete an item from the list, nothing happens. I can see in the Redux console that the action is being dispatched, however it seems to have no effect. Any suggestions?
You have in deleteItem action { type, payload }. Instead you can have { type, id } or using payload in the reducer return statement.
I would do the following - so you are passing the id with the action instead of payload:
export const deleteItem = id => {
console.log("actions");
return {
type: DELETE_ITEM,
id
};
};
Or the best option for later purposes - keep payload just adding id as property:
// action
export const deleteItem = id => {
console.log("actions");
return {
type: DELETE_ITEM,
payload: { id }
};
};
// reducer
case DELETE_ITEM:
// here destructuring the property from payload
const { id } = action.payload;
return {
...state,
items: state.items.filter(item => item.id !== id)
};
I hope this helps!