redux props receiving data twice - reactjs

I am using redux to get the async data and response. In the below component when i post recipe and from server i get response through redux the success modal popup twice. reducer is running only once i have checked eveything, only component has problem. the problem could be with the lifecycle method.
import React, { Component } from 'react';
import 'antd/dist/antd.css';
import {withRouter} from 'react-router-dom';
import {connect} from 'react-redux';
import * as actionCreators from '../../actions/recipe-action/index';
import { Modal, Button } from "antd";
import Spinner from '../../UI/spinner';
class PostRecipe extends Component {
state = {
url: '',
visible: false,
}
showModal = () => {
this.setState({ visible: true });
};
onChangeHandler = (e) => {
this.setState({[e.target.name]: e.target.value});
}
handleOk = e => {
this.props.recipe(this.state.url);
this.setState({url: ""});
this.setState({ visible: false });
};
handleCancel = e => {
this.setState({ visible: false });
};
render() {
const { postRecipes } = this.props;
if(postRecipes.loading) {
return <Spinner />;
}else if(postRecipes.success.ok) {
// this success model popup twice after uploading the recipe
Modal.success({
content: "Recipe Uploaded"
});
}else if(postRecipes.failure.error) {
Modal.error({
title: "Error while uploading recipe",
});
}
return (
<div>
<div>
<Button type="primary" onClick={this.showModal}>
Add Recipe
</Button>
<Modal
title="Add Recipe"
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
>
<input
style={{ width: "100%", padding: "5px", fontSize: "15px" }}
type="text"
placeholder="enter the url"
name="url"
value={this.state.url}
onChange={this.onChangeHandler}
/>
</Modal>
</div>
</div>
);
}
}
const mapStateToProps = ({ postRecipeReducers }) => {
const { recipe: { post: postRecipes } } = postRecipeReducers;
return {
postRecipes
}
};
const mapStateToDispatch = dispatch => {
return {
recipe: (url) => dispatch(actionCreators.postRecipes(url))
}
}
export default withRouter(connect(mapStateToProps, mapStateToDispatch)(PostRecipe));
// my action creators
import {POST_RECIPE_LOADING, POST_RECIPE_SUCCESS, POST_RECIPE_FAILURE, POST_RECIPE_RESET} from '../types';
import {GET_RECIPE_LOADING, GET_RECIPE_SUCCESS, GET_RECIPE_FAILURE, GET_RECIPE_RESET} from '../types';
import Parse from 'parse';
export const postRecipes = (url) => async(dispatch) => {
try {
dispatch({type: POST_RECIPE_LOADING, payload: null});
const {data} = await Parse.Cloud.run('post_recipe', {url: url});
dispatch({type: POST_RECIPE_SUCCESS, payload: data});
} catch(e) {
dispatch({type: POST_RECIPE_FAILURE, payload: {message: e.message}})
}
}
export const getRecipes = () => async (dispatch) => {
try {
dispatch({type: GET_RECIPE_LOADING, payload: null});
const {data} = await Parse.Cloud.run('get_recipe');
dispatch({type: GET_RECIPE_SUCCESS, payload: data});
} catch(e) {
dispatch({type: GET_RECIPE_FAILURE, payload: {message: e.message}})
}
};

Try this:
handleOk = e => {
this.props.recipe(this.state.url);
this.setState({url: "", visible: false});
};
state variable of class is a object with two keys: url and visible. You have to set both at once.

I would try implementing a constructor function to make sure that you have this bound to your local state.
In this code block,
handleOk = e => {
this.props.recipe(this.state.url);
this.setState({url: ""});
this.setState({ visible: false });
};
you could set the whole state in one line like this,
handleOk = e => {
this.props.recipe(this.state.url);
this.setState({url: "", visible: false});
}
I don't know that this will fix your problem. Just a bit of house keeping.

Related

Context & Reducer not returning State

Could someone please let me know why the state isn't being updated from the reducer? The useEffect(()=>{}) isn't being triggered when the state is being returned from the reducer. I have validated the correct information is being passed to the return, but nothing can be seen from the LoginScreen.
Context Script
import React, { createContext, useReducer } from "react";
import userReducer from "./UserReducer";
export const UserContext = createContext();
const initialState = {
userData: [],
isLoggedIn: false,
isAdmin: false,
isEmployee: false,
errorMessage: [{ success: false, statusCode: 0, error: null }],
};
const UserContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(userReducer, initialState);
const registerUser = (user) =>
dispatch({ type: "REGISTER_USER", payload: user });
const loginUser = (user) => dispatch({ type: "LOGIN_USER", payload: user });
const deleteUser = (user) => dispatch({ type: "DELETE_USER", payload: user });
const updateUser = (user) => dispatch({ type: "UPDATE_USER", payload: user });
const contextValues = {
...state,
registerUser,
loginUser,
deleteUser,
updateUser,
};
return (
<UserContext.Provider value={contextValues}>
{children}
</UserContext.Provider>
);
};
export default UserContextProvider;
Reducer Script
import axios from "axios";
axios.defaults.withCredentials = true;
const userReducer = (state = {}, action) => {
let config = {
header: {
"Content-Type": "application/json",
},
};
switch (action.type) {
case "REGISTER_USER":
break;
case "LOGIN_USER":
console.log(state);
const email = action.payload.email;
const password = action.payload.password;
axios
.post("/api/user/login", { email, password }, config)
.then((response) => {
if (response.data.success) {
// localStorage.setItem("authToken", response.data.authToken);
state.userData = response.data.user;
state.isLoggedIn = true;
if (response.data.user.role === 9) {
state.isAdmin = true;
state.isEmployee = true;
} else {
state.isAdmin = false;
state.isEmployee = false;
}
}
})
.catch((error) => {
state.errorMessage = {
success: error.response.data.success,
statusCode: error.response.status,
message: error.response.data.error,
};
});
return {
...state,
userData: [state.userData],
isLoggedIn: state.isLoggedIn,
isAdmin: state.isAdmin,
isEmployee: state.isEmployee,
errorMessage: [state.errorMessage],
};
default:
return state;
}
};
export default userReducer;
Login Form
import { useState, useEffect, useContext } from "react";
import { Link } from "react-router-dom";
import {
Button,
Form,
Grid,
Message,
Segment,
Image,
Container,
} from "semantic-ui-react";
//Custom Imports
import "./LoginScreen.css";
import Logo from "../../../img/logo.png";
//Context
import { UserContext } from "../../context/UserContext";
const LoginScreen = ({ history }) => {
const { userData, loginUser, isLoggedIn, errorMessage, clearErrorMessage } =
useContext(UserContext);
const [user, setUser] = useState({ email: "", password: "" });
const [error, setError] = useState("");
useEffect(() => {
console.log(errorMessage);
if (localStorage.getItem("authToken")) {
history.push("/dashboard");
}
}, [history]);
useEffect(() => {
if (isLoggedIn) {
console.log(userData);
console.log("User is Logged in");
// history.push("/");
}
if (!errorMessage.success && errorMessage.error != null) {
console.log(errorMessage);
setError(errorMessage.message);
setTimeout(() => {
setError("");
}, 5000);
}
}, [userData, errorMessage, isLoggedIn]);
return (
<Container className="login-container">
<Grid
textAlign="center"
style={{ height: "100vh" }}
verticalAlign="middle"
>
<Grid.Column style={{ maxWidth: 450 }}>
<Image src={Logo} className="login-logo" />
<Form size="large" onSubmit={() => loginUser(user)}>
<Segment stacked>
<Form.Input
fluid
icon="user"
iconPosition="left"
placeholder="Email Address"
value={user.email}
onChange={(e) => setUser({ ...user, email: e.target.value })}
/>
<Form.Input
fluid
icon="lock"
iconPosition="left"
placeholder="Password"
value={user.password}
type="password"
onChange={(e) => setUser({ ...user, password: e.target.value })}
/>
{error && <span>{error}</span>}
<Button color="blue" fluid size="large" type="submit">
Login
</Button>
</Segment>
</Form>
<Message>
Don't have an account? <Link to="/register">Sign Up</Link>
</Message>
</Grid.Column>
</Grid>
</Container>
);
};
export default LoginScreen;
Refactor your login function like this
const loginUser({ email, password }) => {
let config = {
header: {
"Content-Type": "application/json",
},
};
axios
.post("/api/user/login", { email, password }, config)
.then((response) => {
if (response.data.success) {
dispatch({ type: 'LOGIN_SUCCESS', payload: response.data });
}
})
.catch((error) => {
dispatch({ type: 'LOGIN_FAILED', payload: error });
});
}
and then your reducer
...
switch(action.type) {
...
case 'LOGIN_SUCCESS':
// return here a new object
// do not mutate the state (state.something = something) is not allowed
...
case 'LOGIN_FAILED':
// handle error
}
Prerequisite Reducer Concepts
Redux and useReducer use reducer like (previousState, action) => newState.
The reducer should be a 'pure' function as in this document. The promises, api calls should not be use inside reducers.
The problem:
Because you call api/promise inside the reducer. The reducer function returns the value before the promise finish. So when the promise finishes, nothing happen.
// A will be return before B, C are going to call
case "LOGIN_USER":
promiseFn()
.then(/* B */ ...)
.catch(/* C */ ...)
// A
return {
...
}
Solution:
Separate the non-pure calls from the reducer. And put them in the other code blocks (like inside hooks, event handlers...).

How to use React Redux Hooks to load spinners

I am trying to load spinner using react-redux hooks (useSelector and useDispatch). I am able to fetch data but not loader (in my case showLoader and hideLoader)
Expectation: when I click the refresh button I want to load spinner (in background it will refresh the data). Before clicking the button I am able to fetch data using useEffect hook.
//ActionCreators.js
export const EVENT_LOG = "EVENT_LOG";
export const EVENT_FAILURE = "EVENT_FAILURE";
export const SHOW_LOADER = "SHOW_LOADER";
export const HIDE_LOADER = "HIDE_LOADER";
//Actions.js
import {
EVENT_LOG,
EVENT_FAILURE,
SHOW_LOADER,
HIDE_LOADER,
} from "./actionCreators";
import { readList } from "./APIUtilsNew";
export const readLogs = (path) => {
return (dispatch) => {
readList(path)
.then((data) =>
dispatch(
{
type: EVENT_LOG,
payload: data,
},
console.log("EventLog Actions: ", data)
)
)
.catch((error) => {
dispatch({
type: EVENT_FAILURE,
payload: error,
});
throw error;
});
};
};
export const showLoader = () => (dispatch) => {
dispatch({
type: SHOW_LOADER,
});
};
export const hideLoader = () => (dispatch) => {
dispatch({
type: HIDE_LOADER,
});
};
//Reducers.js
import {
EVENT_LOG,
EVENT_FAILURE,
HIDE_LOADER,
SHOW_LOADER,
} from "../../actionCreators/index";
export const initialState = {
loading: false,
eventData: [],
eventError: false,
};
const eventReducer = (state = initialState, action) => {
switch (action.type) {
case EVENT_LOG:
return {
...state,
eventData: action.payload,
};
case EVENT_FAILURE:
return {
...state,
eventError: action.payload,
};
case HIDE_LOADER:
return {
...state,
loading: false,
};
case SHOW_LOADER:
return {
...state,
loading: true,
};
default:
return state;
}
};
export default eventReducer;
//React Component
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { readLogs, showLoader, hideLoader } from "./eventActions";
import { FormattedMessage } from "react-intl";
import { XGrid } from "#material-ui/x-grid";
import { CSVLink } from "react-csv";
import IconBtn from "./IconBtn";
import MaterialTheme from "./MaterialTheme";
import { ThemeProvider as MuiThemeProvider } from "#material-ui/core/styles";
import Refresh from "./Refresh";
export default function EventsLog() {
const dispatch = useDispatch();
const eventLogs = useSelector(
(state) => state.eventReducer.eventData.data || []
);
const show = useSelector((state) => state.eventReducer.loading);
const hide = useSelector((state) => state.eventReducer.loading);
useEffect(() => {
dispatch(readLogs("/events"));
}, [dispatch]);
const update = () => {
dispatch(showLoader());
dispatch(hideLoader());
};
let rows = eventLogs.map((obj, index) => {
return (rows = {
id: index + 1,
Time: obj.time,
dateTime: obj.dateTime,
ID: obj.deviceId
});
});
const columns = [
{
field: "Time",
flex: 1,
type: "dateTime",
renderHeader: () => <FormattedMessage id={"time"} />
},
{
field: "dateTime",
flex: 1,
type: "dateTime",
renderHeader: () => <FormattedMessage id={"dateTime"} />
},
{
field: "ID",
flex: 1,
renderHeader: () => <FormattedMessage id={"id"} />
}
];
return (
<div>
<h1>
<FormattedMessage id="event.eventLog" />
<span>
<IconBtn iconLabel="refresh" />
</span>
<CSVLink data={rows} filename={"Log.csv"}>
<IconBtn iconLabel="cloud_download" onClick={update} />
</CSVLink>
</h1>
<div style={{ height: "90%", width: "100%" }}>
<MuiThemeProvider theme={MaterialTheme}>
<Refresh />
<XGrid
pageSize={50}
rowsPerPageOptions={[25, 50, 100]}
rows={rows}
columns={columns}
pagination={true}
hideFooterSelectedRowCount={true}
/>
</MuiThemeProvider>
</div>
</div>
);
}
This is the component where my spinner resides. I want to fetch this component while loading spinner
//Refresh Component
import React from "react";
export default function Refresh() {
return <div>Spinner....</div>;
}
I saw few examples online, where I found everything is in class components
// component Example
class FullPageLoader extends Component {
state = { }
render() {
const {loading} = this.props;
if(!loading) return null;
return (
<div class="loader-container">
<div className="loader">
<img src={LoaderGif} />
</div>
</div>
);
}
}
const mapStateToProps = state => ({ loading: state.application.loading })
export default connect(mapStateToProps)(FullPageLoader);
// Another Component
updateProfile = () =>{
this.props.dispatch( showLoader() )
Axios.post(`https://jsonplaceholder.typicode.com/users`, { user : { name : 'Test User' } })
.then(res => {
console.log( res );
this.props.dispatch( hideLoader() )
})
/* setTimeout(() => {
this.props.dispatch( hideLoader() )
}, 2000); */
}
<Button bsStyle="info" pullRight fill onClick={this.updateProfile} >
Update Profile
</Button>
Can somebody help me how to convert the above class to functional based component and instead of using mapStateToProps to hooks (or) please tell me how to load the spinner using react-redux hooks. I appreciate the help!
More easier way is to show and hide the loader in the action itself. Before the promise, setLoader as true. And in then and catch you can hide loader.
export const readLogs = (path) => {
return (dispatch) => {
showLoader();
readList(path)
.then((data) => {
hideLoader();
dispatch(
{
type: EVENT_LOG,
payload: data,
},
console.log("EventLog Actions: ", data)
)
})
.catch((error) => {
hideLoader();
dispatch({
type: EVENT_FAILURE,
payload: error,
});
throw error;
});
};
};
if it has to be done in the component itself, You can add a delay rather than calling them immediately. There doesn't seem to be any action that is happening here.
const update = () => {
dispatch(showLoader());
setTimeout(() => {
dispatch(hideLoader());
}, 1000);
};

React-Redux: Action isn't properly being triggered

I'm trying to learn the MERN stack, and I'm going through this tutorial on YouTube. I'm getting stuck in Ep. 7. The issue I think I'm having is my ADD_ITEM action is never triggered, and so the state is never updated, and I have to reload the page in order to see any items added. The DELETE_ITEM action works properly, so I suspect there may be an issue with the ADD_ITEM action being called from a form in a modal, but I'm unsure.
Picture of my Redux DevTools after refreshing the page, deleting 2 items, and trying to add 1:
itemReducer.js
import { GET_ITEMS, ADD_ITEM, DELETE_ITEM, ITEMS_LOADING } from '../actions/types';
const initialState = {
items: [],
loading: false
};
export default function(state = initialState, action) {
console.log(action.type);
switch(action.type) {
case GET_ITEMS:
return { ...state, items: action.payload, loading: false };
case ADD_ITEM:
return { ...state, items: [action.payload, ...state] };
case DELETE_ITEM:
return { ...state,
items: state.items.filter(item => item._id !== action.payload)
};
case ITEMS_LOADING:
return {
...state,
loading: true
};
default:
return state;
}
}
ItemModal.js
import React, { Component } from 'react';
import { Button, Modal, ModalHeader, ModalBody, Form, FormGroup, Label, Input } from 'reactstrap';
import { connect } from 'react-redux';
import { addItem } from '../actions/itemActions';
class ItemModal extends Component {
state = {
modal: false,
name: ''
}
toggle = () => {
this.setState({
modal: !this.state.modal
});
}
onChange = (e) => {
this.setState({ [e.target.name]: e.target.value })
}
onSubmit = (e) => {
e.preventDefault();
const newItem = {
name: this.state.name
}
this.props.addItem(newItem);
this.toggle();
}
render() {
return(
<div>
<Button
color="dark"
style={{marginBotton: '2rem'}}
onClick={this.toggle}
>Add Item</Button>
<Modal
isOpen={this.state.modal}
toggle={this.toggle}
>
<ModalHeader
toggle={this.toggle}
>
Add To Shopping List
</ModalHeader>
<ModalBody>
<Form onSubmit={this.onSubmit}>
<FormGroup>
<Label for="item">Item</Label>
<Input
type="text"
name="name"
id="item"
placeholder="Add shopping item"
onChange={this.onChange}
/>
</FormGroup>
<Button
color="dark"
style={{marginTop: '2rem'}}
block
>Submit</Button>
</Form>
</ModalBody>
</Modal>
</div>
)
}
}
const mapStateToProps = state => ({
item: state.item
});
export default connect(mapStateToProps, { addItem })(ItemModal);
itemActions.js
import axios from 'axios';
import { GET_ITEMS, ADD_ITEM, DELETE_ITEM, ITEMS_LOADING } from './types';
export const getItems = () => dispatch => {
dispatch(setItemsLoading());
axios.get('/api/items').then(res => dispatch({
type: GET_ITEMS,
payload: res.data
}))
};
export const deleteItem = (id) => dispatch => {
axios.delete(`/api/items/${id}`).then(res => dispatch({
type: DELETE_ITEM,
payload: id
}))
};
export const addItem = (item) => dispatch => {
axios.post('/api/items', item).then(res => dispatch({
type: ADD_ITEM,
payload: res.data
}))
};
export const setItemsLoading = () => {
return {
type: ITEMS_LOADING
};
};

React redux proper updating record and reflecting changes on the screen

For learning purpose I made this web app where I'm trying to implement crud operations. All works properly except UPDATE, where MongoDB record is updated but changes on the screen are not reflected till the refresh.
I'm still learning therefore not everything is crystal clear, I'm suspecting a problem in a REDUCER... or in the component mapStateToProp object...
What am I doing wrong here?
routes/api
Item.findByIdAndUpdate for sure update's db correctly, but should it also return anything so the reducer/action could react to it?
const express = require("express");
const router = express.Router();
const auth = require("../../middleware/auth");
// Item Model
const Item = require("../../models/stories");
// #route GET api/items
// #desc Get All Items
// #access Public
router.get("/", (req, res) => {
Item.find()
.sort({ date: -1 })
.then(items => res.json(items));
});
// #route PUT api/items
// #desc Update An Item
// #access Private
router.put("/:_id", auth, (req, res) => {
Item.findByIdAndUpdate(
req.params._id,
req.body,
{ new: false, useFindAndModify: false },
() => {}
);
});
module.exports = router;
reducers
import {
GET_STORIES,
ADD_STORY,
DELETE_STORY,
STORIES_LOADING,
UPDATE_STORY
} from "../actions/types";
const initialState = {
stories: [],
loading: false
};
export default function(state = initialState, action) {
switch (action.type) {
case GET_STORIES:
return {
...state,
stories: action.payload,
loading: false
};
case DELETE_STORY:
return {
...state,
stories: state.stories.filter(story => story._id !== action.payload)
};
case ADD_STORY:
return {
...state,
stories: [action.payload, ...state.stories]
};
case UPDATE_STORY:
return {
...state,
stories: action.payload
};
case STORIES_LOADING:
return {
...state,
loading: true
};
default:
return state;
}
}
actions
import axios from "axios";
import {
GET_STORIES,
ADD_STORY,
DELETE_STORY,
UPDATE_STORY,
STORIES_LOADING
} from "./types";
import { tokenConfig } from "./authActions";
import { returnErrors } from "./errorActions";
export const getStories = () => dispatch => {
dispatch(setStoriesLoading());
axios
.get("/api/stories")
.then(res =>
dispatch({
type: GET_STORIES,
payload: res.data
})
)
.catch(err =>
dispatch(returnErrors(err.response.data, err.response.status))
);
};
export const addStory = story => (dispatch, getState) => {
axios
.post("/api/stories", story, tokenConfig(getState))
.then(res => {
dispatch({
type: ADD_STORY,
payload: res.data
});
})
.catch(err =>
dispatch(returnErrors(err.response.data, err.response.status))
);
};
export const updateStory = story => (dispatch, getState) => {
axios
.put(`/api/stories/${story.id}`, story, tokenConfig(getState))
.then(res => {
dispatch({
type: UPDATE_STORY,
payload: story
});
})
.catch(err =>
dispatch(returnErrors(err.response.data, err.response.status))
);
};
export const deleteStory = id => (dispatch, getState) => {
axios
.delete(`/api/stories/${id}`, tokenConfig(getState))
.then(res => {
dispatch({
type: DELETE_STORY,
payload: id
});
})
.catch(err =>
dispatch(returnErrors(err.response.data, err.response.status))
);
};
export const setStoriesLoading = () => {
return {
type: STORIES_LOADING
};
};
component
import React, { Component } from "react";
import {
Modal,
ModalHeader,
ModalBody,
Form,
FormGroup,
Label,
Input
} from "reactstrap";
import { connect } from "react-redux";
import { updateStory } from "../../actions/storyActions";
import PropTypes from "prop-types";
class UpdateStoryModal extends Component {
constructor(props) {
super(props);
}
state = {
id: this.props.idVal,
modal: false,
title: this.props.titleVal,
body: this.props.bodyVal
};
static propTypes = {
isAuthenticated: PropTypes.bool
};
toggle = () => {
this.setState({
modal: !this.state.modal
});
};
onChange = e => {
this.setState({ [e.target.name]: e.target.value });
};
onSubmit = e => {
e.preventDefault();
const obj = {
id: this.props.idVal,
title: this.state.title,
body: this.state.body
};
this.props.updateStory(obj);
this.toggle();
};
render() {
return (
<div>
{this.props.isAuthenticated ? (
<button
type="button"
className="btn btn-primary"
size="sm"
onClick={this.toggle}
>
Edit Story
</button>
) : (
<h4 className="mb-3 ml-4">Please log in to manage stories</h4>
)}
<Modal isOpen={this.state.modal} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>Edit story</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Label for="story">Title</Label>
<Input
type="text"
name="title"
id="story"
onChange={this.onChange}
value={this.state.title}
/>
<Label for="story">Story</Label>
<Input
type="textarea"
name="body"
rows="20"
value={this.state.body}
onChange={this.onChange}
/>
<button
type="button"
className="btn btn-dark"
style={{ marginTop: "2rem" }}
onClick={this.onSubmit}
>
Edit story
</button>
</FormGroup>
</Form>
</ModalBody>
</Modal>
</div>
);
}
}
const mapStateToProps = state => ({
story: state.story,
isAuthenticated: state.auth.isAuthenticated
});
export default connect(
mapStateToProps,
{ updateStory }
)(UpdateStoryModal);
Yes, you want to return the updated item from your MongoDB database so that you have something to work with in your reducer. It looks like you've setup your action-creator to be prepared for that type of logic. So we just need to make a couple updates:
In your express route you would want something like:
router.put("/:_id", auth, (req, res) => {
//this returns a promise
Item.findByIdAndUpdate(
req.params._id,
req.body,
{ new: false, useFindAndModify: false },
() => {}
)
.then((updatedItem) => {
res.json(updatedItem) //we capture this via our promise-handler on the action
})
.catch((error) => {
return res.status(400).json({ couldnotupdate: "could not update item"})
})
});
Then we can tap into that updated item using res.data in your action-creator promise-handler
export const updateStory = story => (dispatch, getState) => {
axios
.put(`/api/stories/${story.id}`, story, tokenConfig(getState))
.then(res => {
dispatch({
type: UPDATE_STORY,
payload: res.data
});
})
.catch(err =>
dispatch(returnErrors(err.response.data, err.response.status))
);
};
Now that you have the updated item as an action-payload, we need to update your reducer:
case UPDATE_STORY:
return {
...state,
stories: state.stories.map((story) => {
if(story._id == action.payload._id){
return{
...story,
...action.payload
} else {
return story
}
}
})
};
With that you should be able to take the updated story from your back-end and have it reflected to the front.

React Redux PUT request failing

REACT Redux PUT request gives me following error:
"Proxy error: Could not proxy request /api/stories/5ccf12b5f6b087c2a3fcc21b from localhost:3000 to http://localhost:5002.
[1] See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (ECONNRESET)."
I made sample React Redux CRUD app based on #Brad Traversy tutorial and GET, POST, DELETE requests works fine.
API with mongoose:
const express = require("express");
const router = express.Router();
const auth = require("../../middleware/auth");
// Item Model
const Item = require("../../models/stories");
// #route GET api/items
// #desc Get All Items
// #access Public
router.get("/", (req, res) => {
Item.find()
.sort({ date: -1 })
.then(items => res.json(items));
});
// #route POST api/items
// #desc Create An Item
// #access Private
router.post("/", auth, (req, res) => {
const newItem = new Item({
title: req.body.title,
body: req.body.body,
author: "Les Frazier"
});
newItem.save().then(item => res.json(item));
});
router.put("/:_id", auth, (req, res) => {
var query = { _id: req.params._id };
var update = {
_id: req.params._id,
title: req.params.title,
body: req.params.body
};
var options = { new: true, useFindAndModify: false };
Item.findByIdAndUpdate(req.params._id, { $set: update }, options);
});
// #route DELETE api/items/:id
// #desc Delete A Item
// #access Private
router.delete("/:id", auth, (req, res) => {
Item.findById(req.params.id)
.then(item => item.remove().then(() => res.json({ success: true })))
.catch(err => res.status(404).json({ success: false }));
});
module.exports = router;
Child component for updating(PUT) data that is failing
import React, { Component } from "react";
import {
Button,
Modal,
ModalHeader,
ModalBody,
Form,
FormGroup,
Label,
Input
} from "reactstrap";
import { connect } from "react-redux";
import { updateStory, deleteStory } from "../../actions/storyActions";
import PropTypes from "prop-types";
class UpdateStoryModal extends Component {
state = {
modal: false,
title: this.props.story.stories.find(
story => story._id === this.props.value
).title,
body: this.props.story.stories.find(story => story._id === this.props.value)
.body
};
static propTypes = {
isAuthenticated: PropTypes.bool
};
toggle = () => {
this.setState({
modal: !this.state.modal
});
};
onChange = e => {
e.target.name === "title"
? this.setState({ title: e.target.value })
: this.setState({ body: e.target.value });
};
//PUT
onSubmit = e => {
e.preventDefault();
const updateStory = {
_id: this.props.value,
title: this.state.title,
body: this.state.body
};
this.props.updateStory(updateStory);
// Close modal
this.toggle();
};
render() {
return (
<div>
{this.props.isAuthenticated ? (
<Button color="primary" size="sm" onClick={this.toggle}>
Edit Story
</Button>
) : (
<h4 className="mb-3 ml-4">Please log in to manage stories</h4>
)}
<Modal isOpen={this.state.modal} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>Edit story</ModalHeader>
<ModalBody>
<Form onSubmit={this.onSubmit}>
<FormGroup>
<Label for="story">Title</Label>
<Input
type="text"
name="title"
id="story"
onChange={this.onChange}
value={this.state.title}
/>
<Label for="story">Story</Label>
<Input
type="textarea"
name="body"
rows="20"
value={this.state.body}
onChange={this.onChange}
/>
<Button color="dark" style={{ marginTop: "2rem" }} block>
Edit story
</Button>
</FormGroup>
</Form>
</ModalBody>
</Modal>
</div>
);
}
}
const mapStateToProps = state => ({
story: state.story,
title: state.title,
body: state.body,
isAuthenticated: state.auth.isAuthenticated
});
export default connect(
mapStateToProps,
{ updateStory, deleteStory }
)(UpdateStoryModal);
Child component that is adding(POST) data and works fine
import React, { Component } from "react";
import {
Button,
Modal,
ModalHeader,
ModalBody,
Form,
FormGroup,
Label,
Input
} from "reactstrap";
import { connect } from "react-redux";
import { addStory } from "../../actions/storyActions";
import PropTypes from "prop-types";
class AddStoryModal extends Component {
state = {
modal: false,
title: "",
body: ""
};
static propTypes = {
isAuthenticated: PropTypes.bool
};
toggle = () => {
this.setState({
modal: !this.state.modal
});
};
onChange = e => {
e.target.name === "title"
? this.setState({ title: e.target.value })
: this.setState({ body: e.target.value });
};
onSubmit = e => {
e.preventDefault();
const newStory = {
title: this.state.title,
body: this.state.body
};
this.props.addStory(newStory);
// Close modal
this.toggle();
};
render() {
return (
<div>
{this.props.isAuthenticated ? (
<Button
color="dark"
style={{ marginBottom: "2rem" }}
onClick={this.toggle}
>
Add Story
</Button>
) : (
<h4 className="mb-3 ml-4">Please log in to manage stories</h4>
)}
<Modal isOpen={this.state.modal} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>Add new story</ModalHeader>
<ModalBody>
<Form onSubmit={this.onSubmit}>
<FormGroup>
<Label for="story">Title</Label>
<Input
type="text"
name="title"
id="story"
onChange={this.onChange}
/>
<Label for="story">Story</Label>
<Input
type="textarea"
name="body"
rows="20"
onChange={this.onChange}
/>
<Button color="dark" style={{ marginTop: "2rem" }} block>
Add Story
</Button>
</FormGroup>
</Form>
</ModalBody>
</Modal>
</div>
);
}
}
const mapStateToProps = state => ({
title: state.title,
body: state.body,
isAuthenticated: state.auth.isAuthenticated
});
export default connect(
mapStateToProps,
{ addStory }
)(AddStoryModal);
Reducer
import {
GET_STORIES,
ADD_STORY,
DELETE_STORY,
STORIES_LOADING,
UPDATE_STORY
} from "../actions/types";
const initialState = {
stories: [],
loading: false
};
export default function(state = initialState, action) {
switch (action.type) {
case GET_STORIES:
return {
...state,
stories: action.payload,
loading: false
};
case DELETE_STORY:
return {
...state,
stories: state.stories.filter(story => story._id !== action.payload)
};
case ADD_STORY:
return {
...state,
stories: [action.payload, ...state.stories]
};
case UPDATE_STORY:
return {
...state,
stories: state.stories.map(story =>
story._id === action.payload._id ? (story = action.payload) : story
)
};
case STORIES_LOADING:
return {
...state,
loading: true
};
default:
return state;
}
}
Action
import axios from "axios";
import {
GET_STORIES,
ADD_STORY,
DELETE_STORY,
UPDATE_STORY,
STORIES_LOADING
} from "./types";
import { tokenConfig } from "./authActions";
import { returnErrors } from "./errorActions";
export const getStories = () => dispatch => {
dispatch(setStoriesLoading());
axios
.get("/api/stories")
.then(res =>
dispatch({
type: GET_STORIES,
payload: res.data
})
)
.catch(err =>
dispatch(returnErrors(err.response.data, err.response.status))
);
};
export const addStory = story => (dispatch, getState) => {
axios
.post("/api/stories", story, tokenConfig(getState))
.then(res =>
dispatch({
type: ADD_STORY,
payload: res.data
})
)
.catch(err =>
dispatch(returnErrors(err.response.data, err.response.status))
);
};
export const updateStory = story => (dispatch, getState) => {
axios
.put(`/api/stories/${story._id}`, story, tokenConfig(getState))
.then(res =>
dispatch({
type: UPDATE_STORY,
payload: res.data
})
)
.catch(err =>
dispatch(returnErrors(err.response.data, err.response.status))
);
};
export const deleteStory = id => (dispatch, getState) => {
axios
.delete(`/api/stories/${id}`, tokenConfig(getState))
.then(res =>
dispatch({
type: DELETE_STORY,
payload: id
})
)
.catch(err =>
dispatch(returnErrors(err.response.data, err.response.status))
);
};
export const setStoriesLoading = () => {
return {
type: STORIES_LOADING
};
};
Ok so I found main problem...
I have replaced this code"
Item.findByIdAndUpdate(req.params._id, { $set: update }, options);
with this:
Item.findByIdAndUpdate(
req.params._id,
req.body,
{ new: false, useFindAndModify: false },
() => {
console.log("done");
}
);
And the record is being updated now in db. So the main problem is gone.
But whenever I refresh a page I'm still getting this:
Proxy error: Could not proxy request /api/stories/5ccf398fe278beca5efa3d23 from localhost:3000 to http://localhost:5002.
See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (ECONNRESET).

Resources