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};
Related
i cannot delete a product, although it seems that everything looks fine and i see in console that action of type: REMOVE_SELECTED_PRODUCT occurs, the product is still in array and seems like the state is not updated??
So lets start with productActions.js
export const removeSelectedProduct = (id) => {
return {
type: REMOVE_SELECTED_PRODUCT,
payload: id,
};
};
export const removeProduct = (id) => {
return async (dispatch) => {
dispatch(removeSelectedProduct(id));
try {
console.log(id); // this is for test, and i see in console that proper id is printed
await axios.delete(`https://fakestoreapi.com/products/${id}`);
} catch (err) {
console.log(err);
}
};
};
now the productsReducer.js
const intialState = {
products: [],
};
export const productsReducer = (state = intialState, { type, payload }) => {
switch (type) {
case SET_PRODUCTS:
return { ...state, products: payload };
case ADD_PRODUCT:
return { ...state, products: [...state.products, payload] };
case REMOVE_SELECTED_PRODUCT:
return {
...state,
products: state.products.filter((el) => el.id !== payload),
};
default:
return state;
}
};
And i use it in ProductDetails.js as an button so there is a whole code of this component:
const ProductDetails = ({ removeProduct, product }) => {
const { productId } = useParams();
const { image, title, price, category, description } = product;
const dispatch = useDispatch();
const fetchProductDetail = async (id) => {
const response = await axios
.get(`https://fakestoreapi.com/products/${id}`)
.catch((err) => {
console.log("Err: ", err);
});
dispatch(selectedProduct(response.data));
};
useEffect(() => {
if (productId && productId !== "") fetchProductDetail(productId);
}, [productId]);
return (
<div>
{Object.keys(product).length === 0 ? (
<div>...Loading</div>
) : (
<div>
<img alt={productId} src={image} />
<div>
<h1>{title}</h1>
<h2>
<p>${price}</p>
</h2>
<h3>{category}</h3>
<p>{description}</p>
<button>Add to Cart</button>
<button onClick={() => removeProduct(productId)}>UsuĊ</button>
</div>
</div>
)}
</div>
);
};
const mapStateToProps = (state) => {
return {
products: state.allProducts.products,
product: state.product,
};
};
const mapDispatchToProps = {
removeProduct,
selectedProduct,
};
export default connect(mapStateToProps, mapDispatchToProps)(ProductDetails);
If anyone cant find the problem, i would be glad.
As an i said the acction and the id is printed to the console:
console output on clicking the button
Maybe there is some problem with state, and it dont update the state when i delete? Idk, please help me :(
As product has number type id and you're providing string i.e
2 !== "2"
Do this:
dispatch(removeSelectedProduct(+id));//change string to number
...
...
await axios.delete(`https://fakestoreapi.com/products/${+id}`);//similarly here
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 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've been trying to create this search app where I can display the items in a table and delete items using react redux. However, on the initial load, the app shows a table but there is no data in the table. It's an empty table. If i search for another movie name which have more than one movie for that search term, then 2 tables would be shown but I want to show everything on the same table itself. The delete button is not working as well. Is there something wrong with my action and reducer files?
Action.js
import {
FETCH_MOVIE_PENDING,
FETCH_MOVIE_SUCCESS,
FETCH_MOVIE_ERROR,
DELETE_MOVIE
} from "./types";
const fetchMoviePendig = () => ({
type: FETCH_MOVIE_PENDING
});
const fetchMovieSuccess = json => ({
type: FETCH_MOVIE_SUCCESS,
payload: json
});
const fetchMovieError = error => ({
type: FETCH_MOVIE_ERROR,
payload: error
});
export const fetchMovie = name => {
return async dispatch => {
dispatch(fetchMoviePendig());
try {
const url = `https://jsonmock.hackerrank.com/api/movies/search/?Title=${name}`;
const response = await fetch(url);
const result = await response.json(response);
console.log(result);
dispatch(fetchMovieSuccess(result.data));
} catch (error) {
dispatch(fetchMovieError(error));
}
};
};
export const deleteEvent = id => async dispatch => {
try {
dispatch({
type: DELETE_MOVIE,
payload: id
});
} catch (err) {
console.log(err);
}
};
Reducer
import {
FETCH_MOVIE_PENDING,
FETCH_MOVIE_SUCCESS,
FETCH_MOVIE_ERROR,
DELETE_MOVIE
} from "../action/types";
const initialState = {
data: [],
loading: false,
error: ""
};
const moviesReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_MOVIE_PENDING:
return {
...state,
loading: true
};
case FETCH_MOVIE_SUCCESS:
return {
...state,
loading: false,
data: [...state.data, action.payload]
};
case FETCH_MOVIE_ERROR:
return {
...state,
loading: false,
error: action.payload
};
case DELETE_MOVIE:
return {
...state,
data: state.data.filter(movie => movie.id !== action.payload)
};
default:
return state;
}
};
export default moviesReducer;
App.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { fetchMovie } from "./action/movieActions";
import Input from "./components/Input";
import MovieTable from "./components/MovieTable";
class App extends Component {
state = {
searchInput: "The Rain"
};
componentDidMount() {
this.props.getMovieList(this.state.searchInput);
}
_getMovie = () => {
this.props.getMovieList(this.state.searchInput);
};
_onChangeHandler = e => {
this.setState({
searchInput: e.target.value
});
console.log(this.state.searchInput);
};
render() {
const { data, loading } = this.props.movies;
return (
<div className="center">
<div>
<h2 className="center white-text">Movie Search</h2>
</div>
<div className="container">
<Input
value={this.state.searchInput}
onChange={this._onChangeHandler}
onClick={this._getMovie}
/>
<div className="row">
{loading ? (
<p>Loading</p>
) : (
data.map(item => (
<MovieTable
key={item.imdbID}
year={item.Year}
name={item.Title}
movieId={item.imdbId}
/>
))
)}
</div>
</div>
</div>
);
}
}
const mapStateToProps = state => {
return {
movies: state.movies
};
};
const mapDispatchToProps = dispatch => {
return {
getMovieList: name => dispatch(fetchMovie(name))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
Hello please take a look at the sandbox : https://codesandbox.io/s/prod-wind-4hgq2?file=/src/App.js
I have edited
<MovieTable
data={data.map(d => ({
year: d.Year,
name: d.Title,
movieId: d.imdbId
}))}
/>
and
case FETCH_MOVIE_SUCCESS:
return {
...state,
loading: false,
data: action.payload
};
And ... Currently the delete button has no event, that's why it can't work
I see data having the following pattern:
Object {page: 1, per_page: 10, total: 1, total_pages: 1, data: Array[1]}
page: 1
per_page: 10
total: 1
total_pages: 1
data: Array[1]
0: Object
Title: "Sin in the Rain"
Year: 2006
imdbID: "tt1072449"
And you are accessing wrong properties in the component render logic, can you fix that.
Duplicate table is created the way you have written the logic.
Pass the data to MovieTable component and let it render and create the table
and fill it.
In reducer (FETCH_MOVIE_SUCCESS) you need don't need to append data you have to
replace or use the current movie data only.
I'm building a simple CRUD note app and I'm having issues getting the child components to update after simple POST and DELETE api calls to create and delete notes.
Here's the parent component with a simple form and a child component <NotepadsShowView /> to render the submitted notes.
class AuthenticatedHomeView extends Component {
_handleSubmit(e) {
e.preventDefault()
const { dispatch } = this.props
const data = {
title: this.refs.title.value,
description: this.refs.description.value,
private: this.refs.private.checked
}
dispatch(Actions.createNotepad(this.props.currentUser.id, data))
this._resetForm()
}
_resetForm() {
this.refs.title.value = ''
this.refs.description.value = ''
this.refs.private.checked = true
}
render() {
return (
<div>
<form onSubmit={::this._handleSubmit}>
{/* form removed for clarity */}
</form>
<NotepadsShowView/>
</div>
)
}
}
and the NotepadsShowView child component:
class NotepadsShowView extends Component {
componentWillMount() {
const { dispatch, currentUser } = this.props
dispatch(Actions.fetchNotepads(currentUser.id))
}
_renderEachOwnedNotepad() {
const { ownedNotepads } = this.props
return ownedNotepads.map((notepad, i) => {
return <NotepadShowView key={notepad.id} {...notepad} {...this.props}/>
})
}
render() {
const { isFetchingNotepads } = this.props
const notepads = this._renderEachOwnedNotepad()
if (isFetchingNotepads || notepads.length == 0) return null
return (
<ul className="notepads-container">
{notepads}
</ul>
)
}
}
const mapStateToProps = (state) => ({
isFetchingNotepads: state.notepads.fetching,
currentUser: state.session.currentUser,
ownedNotepads: state.notepads.ownedNotepads,
sharedNotepads: state.notepads.sharedNotepads
})
export default connect(mapStateToProps)(NotepadsShowView)
Here is the action creator:
const Actions = {
createNotepad: (userId, data) => {
return dispatch => {
httpPost(`/api/v1/users/${userId}/notepads`, {data: data})
.then(data => {
dispatch({
type: CONSTANTS.NOTEPADS_CREATED,
notepad: data
})
})
.catch(error => {
error.response.json()
.then(json => {
dispatch({
type: CONSTANTS.NOTEPADS_CREATE_ERROR,
errors: json.errors,
})
})
})
}
},
fetchNotepads: (userId) => {
return dispatch => {
dispatch({
type: CONSTANTS.NOTEPADS_FETCHING
})
httpGet(`/api/v1/users/${userId}/notepads`, {id: userId})
.then(data => {
dispatch({
type: CONSTANTS.NOTEPADS_RECEIVED,
notepads: data.notepads
})
})
.catch(error => {
error.response.json()
.then(json => {
dispatch({
type: CONSTANTS.NOTEPADS_ERRORS,
errors: json.error
})
})
})
}
},
deleteNotepad: (userId, notepadId) => {
return dispatch => {
httpDelete(`api/v1/users/${userId}/notepads/${notepadId}`, {id: notepadId})
.then(data => {
dispatch({
type: CONSTANTS.NOTEPADS_OWNED_DELETE,
id: notepadId
})
})
}
},
}
Here is the reducer:
const initialState = {
ownedNotepads: [],
fetching: true,
}
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case CONSTANTS.NOTEPADS_FETCHING:
return {
...state,
fetching: true,
}
case CONSTANTS.NOTEPADS_RECEIVED:
return {
...state,
fetching: false,
ownedNotepads: action.notepads
}
case CONSTANTS.NOTEPADS_CREATED:
return {
...state,
ownedNotepads: [
...state.ownedNotepads,
{
id: action.id,
title: action.title,
description: action.description,
private: action.private
}
]
}
case CONSTANTS.NOTEPADS_OWNED_DELETE:
const index = state.ownedNotepads.findIndex(note => note.id === action.id)
return {
...state,
ownedNotepads: [
...state.ownedNotepads,
state.ownedNotepads.slice(0, index),
state.ownedNotepads.slice(index + 1)
]
}
default:
return state
}
}
A user submits a new notepad which triggers an POST api call. Server returns the new notepad and the reducer adds the notepad to the Redux state. No issues here. However, when a notepad is created the notepad props are undefined and no new notepads are being shown in the child UI components. They don't know of the update and I assume it's because I'm not handling the state update.
I am using componentWillMount (cWM) above to fetch the updated notepads state before the initial render. I'm assuming I should use componentWillReceiveProps but I understand there will be an infinite loop if I dispatch the fetchNotepads action because the dispatch in cWM will run again.
My question is how do I update the component props when the Redux state changes? Do I have to use component state? What about the lifecycle methods?