reducer
import { COMBINE_POST } from '../Actions/actionType'
const initialState = {
posts: null,
users: null,
comments: null,
post: null
}
export const rootReducer = (state = initialState, action) => {
switch (action.type) {
case 'getPosts':
return {
...state,
posts: action.data
}
case 'getUsers':
return {
...state,
users: action.data
}
case 'getComments':
return {
...state,
comments: action.data
}
case COMBINE_POST :
return {
...state,
post: action.payload
}
case 'removePost':
console.log('removepost', state.post.post)
return {
...state,
post: state.post.post.filter(item => item.id !==
action.payload)
}
default:
return state
}
}
action
import { COMBINE_POST } from './actionType'
export const fetchPosts = () => {
return dispatch => {
fetch(`https://jsonplaceholder.typicode.com/posts/`)
.then(res => res.json())
.then(res => dispatch({ type: 'getPosts', data: res }))
}
}
export const fetchUsers = () => {
return dispatch => {
fetch(`https://jsonplaceholder.typicode.com/users/`)
.then(res => res.json())
.then(res => dispatch({ type: 'getUsers', data: res }))
}
}
export const fetchComments = () => {
return dispatch => {
fetch(`https://jsonplaceholder.typicode.com/comments/`)
.then(res => res.json())
.then(res => dispatch({ type: 'getComments', data: res }))
}
}
export const removePost = id => {
return dispatch => {
fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
method: 'DELETE',
})
dispatch({ type: 'removePost', payload: id })
}
}
export const combimePost = arr => ({ type: COMBINE_POST, payload: arr })
component render
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { fetchPosts, fetchUsers, fetchComments, removePost } from '../../Redux/Actions/action'
import { combimePost } from '../../Redux/Actions/action'
import './newsList.scss'
export const NewsList = () => {
const dispatch = useDispatch()
const selector = useSelector(state => state.rootReducer)
useEffect(() => {
dispatch(
fetchPosts()
)
}, [])
useEffect(() => {
dispatch(
fetchUsers()
)
}, [])
useEffect(() => {
dispatch(
fetchComments()
)
}, [])
This is where I combine the post and user id
useEffect(() => {
const combinePost = selector.posts?.map(post => ({
...post,
user: selector.users?.find(user => post.userId === user.id),
commetn: selector?.comments?.find(comment => post.id === comment.postId)
}))
return dispatch(
combimePost(
{
post: combinePost,
}
)
)
}, [selector.posts, selector.users, selector.comments])
return <>
{selector?.post?.post?.map((res, i) => (
<div className="card text-center" key={i}>
<div className="card-header">
{res.user?.name}
</div>
<div className="card-body">
<h5 className="card-title">{res.title}</h5>
<p className="card-text">{res.comment?.body}</p>
<button
className="btn btn-outline-danger"
onClick={() => dispatch(removePost(res.id))}
>DELETE</button>
</div>
<div className="card-footer text-muted">
</div>
</div>
)
)}
</>
}
When a post is deleted, all posts disappear from the page, in the console it shows that the selected post has been deleted, but the page is not rendered, it becomes empty. what is the problem ?
When a post is deleted, all posts disappear from the page, in the console it shows that the selected post has been deleted, but the page is not rendered, it becomes empty. what is the problem ?
Related
I am using axios to fetch data and then want to render the component. For that, I have loading which gets set to true when fetching and to false when all the data has come.
But I am getting error. Is there a way to trigger useEffect before rendering of component ?
Following is the code:
GithubReducer.js
import {
SET_USERS,
CLEAR_USERS,
SET_LOADING,
SET_USER,
CLEAR_USER,
} from "../types";
const GithubReducer = (state, action) => {
switch (action.type) {
case SET_USERS: {
return { ...state, users: action.payload };
}
case CLEAR_USERS: {
return { ...state, users: [] };
}
case SET_LOADING: {
return { ...state, loading: action.payload };
}
case SET_USER: {
return { ...state, user: action.payload };
}
case CLEAR_USER: {
return { ...state, user: null };
}
default:
return state;
}
};
export default GithubReducer;
GithubState.js
import React, { useReducer } from "react";
import axios from "axios";
import {
SET_USERS,
CLEAR_USERS,
SET_LOADING,
SET_USER,
CLEAR_USER,
} from "../types";
import GithubReducer from "./GithubReducer";
import GithubContext from "./GithubContext";
const GithubState = (props) => {
const initialState = {
loading: false,
users: [],
user: null,
};
const [state, dispatch] = useReducer(GithubReducer, initialState);
const setLoading = (val) => dispatch({ type: SET_LOADING, payload: val });
const getGithubUsers = async () => {
setLoading(true);
dispatch({ type: CLEAR_USER });
const res = await axios.get(`https://api.github.com/users`);
dispatch({
type: SET_USERS,
payload: res.data,
});
setLoading(false);
};
const clearUsers = () => {
dispatch({ type: CLEAR_USERS });
};
const searchUsersWithName = async (username) => {
setLoading(true);
const res = await axios.get(
`https://api.github.com/search/users?q=${username}`
);
dispatch({ type: SET_USERS, payload: res.data.items });
setLoading(false);
};
const fetchGithubUserProfile = async (username) => {
setLoading(true);
const res = await axios.get(`https://api.github.com/users/${username}`);
dispatch({ type: SET_USER, payload: res.data });
setLoading(false);
};
return (
<GithubContext.Provider
value={{
getGithubUsers,
clearUsers,
searchUsersWithName,
fetchGithubUserProfile,
users: state.users,
loading: state.loading,
user: state.user,
}}
>
{props.children}
</GithubContext.Provider>
);
};
export default GithubState;
User.js
import React, { useContext, useEffect } from "react";
import { useParams } from "react-router-dom";
import GithubContext from "../../context/github/GithubContext";
import Spinner from "../layout/Spinner";
const User = () => {
const { fetchGithubUserProfile, user, loading } = useContext(GithubContext);
const { username } = useParams();
useEffect(() => {
fetchGithubUserProfile(username);
// eslint-disable-next-line
}, []);
if (loading) return <Spinner />;
else {
return (
<div className="user">
<button>Go Back</button>
<section className="about">{user.login}</section>
</div>
);
}
};
export default User;
And, this is the error I am getting:
TypeError: Cannot read properties of null (reading 'login')
User
D:/anubh/Desktop/github-finder/src/components/users/User.js:21
18 | return (
19 | <div className="user">
20 | <button>Go Back</button>
> 21 | <section className="about">{user.login}</section>
| ^ 22 | </div>
23 | );
24 | }
Very simple. You can't. useEffect runs after componentDidMount, or after the JSX has been rendered.
Here is a solution. Render your JSX conditionally depending on state, which you can set once your data is retrieved.
return (
{data ? <MyComponent /> : null}
)
I am in the process of finishing my todo application, since redux is a bit new to me of course I encounter problems. My problem is when I click the delete button (recycle icon) I want a confirmation box to pop up with basic Yes and No buttons, I have built that, but... when I click one recycle button all of the other confirmation boxes get set to True and they pop up in sync. I need only one box to pop up for the right todo.id.
Note: I have built this before without redux, but I am still wrapping my head around redux.
Here is my code:
JS:
import React, {useState, Fragment} from 'react'
import { useDispatch, useSelector } from 'react-redux';
import "./todo.css"
const Todos = () => {
const dispatch = useDispatch();
const todos = useSelector(state => state.todos);
const confirmationSt = useSelector(state => state.confirmation)
const handleConfirm = id => {
dispatch({
type: "CONFIRM",
})
}
const handleContinue = () => {
dispatch({
type: "CONTINUE",
})
}
const handleClick = id => dispatch({
type: "DELETE_TODO",
payload: id,
})
if (!todos || !todos.length) {
return <p>Empty</p>
}
return (
<ul className='unlist'>{todos.map(todo =><Fragment key={todo.id}> <div className='todoContent'><li >{todo.label}</li>
<div><button className='delete' onClick={handleConfirm}><i className="fas fa-recycle"></i></button>
<button className='delete' onClick={handleConfirm}><i className="fas fa-wrench"></i></button>
</div>
</div>
{confirmationSt === true ? <div className='confirmation-box'>
Are you sure?
<button onClick={() => handleClick(todo.id)}>Yes</button>
<button onClick={handleContinue}>No</button>
</div> : null}
</Fragment>
)}
</ul>
)
}
Reducer:
const initalState = {
todos: [],
confirmation: false,
}
const reducer = (state = initalState, action) => {
switch (action.type) {
case "ADD_TODO":
return {
...state,
todos: [...state.todos, action.payload],
}
case "DELETE_TODO":
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
}
case "CONFIRM":
return {
...state,
confirmation: !state.confirmation,
}
case "CONTINUE":
return {
...state,
confirmation: false
}
default: return state;
}
}
export default reducer
I have changed confirmation from boolean to id of the task
import React, { useState, Fragment } from 'react'
import { useDispatch, useSelector } from 'react-redux';
import "./todo.css"
const Todos = () => {
const dispatch = useDispatch();
const todos = useSelector(state => state.todos);
const confirmationSt = useSelector(state => state.confirmation)
const handleConfirm = id => {
dispatch({
type: "CONFIRM",
payload: id
})
}
const handleContinue = () => {
dispatch({
type: "CONTINUE",
})
}
const handleClick = id => dispatch({
type: "DELETE_TODO",
payload: id,
})
if (!todos || !todos.length) {
return <p>Empty</p>
}
return (
<ul className='unlist'>
{ todos.map(todo =>
<Fragment key={todo.id}>
<div className='todoContent'>
<li >{todo.label}</li>
<div>
<button className='delete' onClick={()=>handleConfirm(todo.id)}>
<i className="fas fa-recycle"></i>
</button>
<button className='delete' onClick={()=>handleConfirm(todo.id)}>
<i className="fas fa-wrench"></i>
</button>
</div>
</div>
{
confirmationSt === todo.id ?
<div className='confirmation-box'>
Are you sure?
<button onClick={() => handleClick(todo.id)}>Yes</button>
<button onClick={handleContinue}>No</button>
</div>
: null
}
</Fragment>
)}
</ul>
)
}
const initalState = {
todos: [],
confirmation: -1,
}
const reducer = (state = initalState, action) => {
switch (action.type) {
case "ADD_TODO":
return {
...state,
todos: [...state.todos, action.payload],
}
case "DELETE_TODO":
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
}
case "CONFIRM":
return {
...state,
confirmation: action.payload,
}
case "CONTINUE":
return {
...state,
confirmation: false
}
default: return state;
}
}
export default reducer
import produce from "immer";
const initialState = {
isLoading: true,
error: "",
burgers: [],
};
export default function (state = initialState, action) {
switch (action.type) {
case "ADD_BURGER_BUCKET": {
return produce(state, (draftState) => {
if (Array.isArray(action.payload)) {
draftState.burgers.push(...action.payload);
} else {
draftState.burgers.push(action.payload);
}
});
}
case "REMOVE_BURGERS_BUCKET": {
return produce(state, (draftState) => {
draftState.burgers = []
});
}
**case "REMOVE_ONE_BURGER_BUCKET": {
return produce(state, (draftState) => {
console.log(action.payload, draftState.burgers) console.log => 3 Proxy {0: {…}}
draftState.burgers.filter(el => el.id !== action.payload)
})
}** HERE THIS ONE DOES NOT WORK!!!
default:
return state;
}
}
return ( <===== BURGER BUTTON
<Burger
key={burger.id}
text={burger.name}
cost={burger.cost}
onClick={() => {
dispatch({
type: "REMOVE_ONE_BURGER_BUCKET",
payload: burger.id, <=== PASS ID TO REDUCER
}); <==== THIS ONE DOESN'T REMOVE THE ELEMENT FROM AN ARRAY
localStorage.setItem("burger", JSON.stringify(burger));
localStorage.setItem(
"burgersBucket",
JSON.stringify(
list.burgers.filter((el) => el.id !== burger.id)
)
);
history.push("/redo");
}}
/>
);
}
I want to remove element from array by its id, but I can't do it, that's what I get in the console
3 Proxy {0: {…}}
I have useselector and useDispatch hooks both imported.
FULL CODE
import React from "react";
import { Wrapper } from "../../styled/general";
import { Menu, MenuTitle, Burgers } from "../home/homestyled";
import Burger from "../../component/burger";
import { useDispatch, useSelector } from "react-redux";
import { useEffect } from "react";
import { useHistory } from "react-router-dom";
export default function Index() {
const list = useSelector((state) => state.burgerBucket);
const dispatch = useDispatch();
const history = useHistory();
useEffect(() => {
console.log(list.burgers);
if (list.burgers.length > 0) {
localStorage.setItem("burgersBucket", JSON.stringify(list.burgers));
} else {
let burgersBucket = JSON.parse(localStorage.getItem("burgersBucket"));
dispatch({ type: "ADD_BURGER_BUCKET", payload: burgersBucket });
}
}, []);
return (
<Wrapper>
<Menu>
<MenuTitle>My Bucket</MenuTitle>
<Burgers>
{[...list.burgers, { finish: true }, { addMore: true }].map(
(burger) => {
if (burger.addMore) {
return (
<Burger
key={-2}
bg={"lightgreen"}
text={"Додати ще"}
onClick={() => {
history.push("/");
}}
/>
);
}
if (burger.finish) {
return (
<Burger
key={-1}
bg={"#ff5050"}
text={"Завершити"}
onClick={() => {
dispatch({ type: "REMOVE_BURGERS_BUCKET" });
history.push("/");
}}
/>
);
}
return (
<Burger
key={burger.id}
text={burger.name}
cost={burger.cost}
onClick={() => {
dispatch({
type: "REMOVE_ONE_BURGER_BUCKET",
payload: burger.id,
});
localStorage.setItem("burger", JSON.stringify(burger));
localStorage.setItem(
"burgersBucket",
JSON.stringify(
list.burgers.filter((el) => el.id !== burger.id)
)
);
history.push("/redo");
}}
/>
);
}
)}
</Burgers>
</Menu>
</Wrapper>
);
}
enter code here
FULL CODE REDUCER
import produce from "immer";
const initialState = {
isLoading: true,
error: "",
burgers: [],
};
export default function (state = initialState, action) {
switch (action.type) {
case "ADD_BURGER_BUCKET": {
return produce(state, (draftState) => {
if (Array.isArray(action.payload)) {
draftState.burgers.push(...action.payload);
} else {
draftState.burgers.push(action.payload);
}
});
}
case "REMOVE_BURGERS_BUCKET": {
return produce(state, (draftState) => {
draftState.burgers = []
});
}
case "REMOVE_ONE_BURGER_BUCKET": {
return produce(state, (draftState) => {
console.log(action.payload, draftState.burgers)
draftState.burgers.filter(el => el.id !== action.payload)
})
}
default:
return state;
}
}
ALSO MAYBE THIS ONE WILL BE IMPORTANT, THIS IS THE CODE(PAGE) I GET REDIRECTED ONCE THE USER CLICKED THAT BURGER-BUTTON
FULL CODE
import React, { useEffect, useState, useContext } from "react";
import { Wrapper } from "../../styled/general";
import { Menu, MenuTitle } from "../home/homestyled";
import {
BurgerIngridients,
IngridientWrapper,
BurgerDetails,
DetailsTitle,
IngridientsDetails,
Total,
DetailsButtonContinue,
} from "./burgerredostyled";
import Ingridient from "../../component/Ingridient";
import IngridientdetailBlock from "../../component/IngridientsDetailBlock";
import { useHistory } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { useBurger } from "../../hooks/useBurger";
import { sumOfToppings } from "../../helpers/sum";
export default function Index() {
const burger = useSelector((state) => state.burger);
const [toppings, error, isLoading] = useBurger(
"http://localhost:3000/toppings"
);
const dispatch = useDispatch();
const history = useHistory();
useEffect(() => {
if (burger.id) {
localStorage.setItem("burger", JSON.stringify(burger));
} else {
let localStorageBurger = JSON.parse(localStorage.getItem("burger"));
dispatch({ type: "ADD_BURGER", payload: localStorageBurger });
}
for (let i = 0; i < toppings.length; i++) {
for (let j = 0; j < burger.consists_of.length; j++) {
if (burger.consists_of[j].name === toppings[i].name) {
toppings[i].quantity = burger.consists_of[j].quantity;
}
}
}
if (toppings.length > 0) {
dispatch({
type: "ADD_BURGER",
payload: { ...burger, consists_of: toppings },
});
}
}, [isLoading]);
const add = (text, num) => {
for (let i = 0; i < toppings.length; i++) {
if (toppings[i].name === text) {
toppings[i].quantity = num;
}
}
dispatch({
type: "ADD_BURGER",
payload: { ...burger, consists_of: toppings },
});
localStorage.setItem(
"burger",
JSON.stringify({ ...burger, consists_of: toppings })
);
};
if (!isLoading)
return (
<Wrapper>
<Menu>
<MenuTitle>{burger && burger.name}</MenuTitle>
<IngridientWrapper>
<BurgerIngridients>
{burger.consists_of.map(({ name, quantity, id }) => {
return (
<Ingridient
key={id}
text={name}
add={add}
initValue={quantity}
/>
);
})}
</BurgerIngridients>
<BurgerDetails>
<DetailsTitle>
<span>{burger && burger.name} | інформація</span>{" "}
<DetailsButtonContinue
onClick={() => {
dispatch({
type: "ADD_BURGER_BUCKET",
payload: { ...burger, cost: sumOfToppings(burger) },
});
history.push("/bucket");
}}
>
продовжити
</DetailsButtonContinue>
</DetailsTitle>
<IngridientsDetails>
{burger.consists_of
.filter((el) => el.quantity > 0)
.map((el) => {
return (
<IngridientdetailBlock
key={el.id}
text={el.name}
price={el.quantity * el.cost}
qty={el.quantity}
></IngridientdetailBlock>
);
})}
</IngridientsDetails>
<Total>Загалом {sumOfToppings(burger)}(грн.)</Total>
</BurgerDetails>
</IngridientWrapper>
</Menu>
</Wrapper>
);
if (isLoading) {
return <span>loading</span>;
}
}
Array.prototype.filter does not mutate the array, it creates a new one.
So this:
draftState.burgers.filter(el => el.id !== action.payload)
is not actually changing draftState.burgers. But this will:
produce(state, (draftState) => {
draftState.burgers = draftState.burgers.filter(el => el.id !== action.payload)
})
I'm having some problems with deleting the post in my app. So, after deleting the post, the state should update and the component should re-render, right? So, after deleting my post, component re-renders with the same data. If I refresh, then only the updated data is shown on the page. For example, if I have 3 posts in my app when I delete ONE post, the component re-renders, but still it shows 3 posts. I don't know why this is happening.
Here's my code.
UserFeed
import React, { Component } from "react"
import { getUserPosts, getCurrentUser } from "../actions/userActions"
import { connect } from "react-redux"
import Cards from "./Cards"
class UserFeed extends Component {
componentDidMount() {
const authToken = localStorage.getItem("authToken")
if (authToken) {
this.props.dispatch(getCurrentUser(authToken))
if (this.props && this.props.userId) {
this.props.dispatch(getUserPosts(this.props.userId))
} else {
return null
}
}
}
render() {
console.log("render called")
const { isFetchingUserPosts, userPosts } = this.props
console.log(isFetchingUserPosts, userPosts)
return isFetchingUserPosts ? (
<p>Fetching....</p>
) : (
<div>
{userPosts &&
userPosts.map(post => {
return <Cards key={post._id} post={post} />
})}
</div>
)
}
}
const mapStateToPros = state => {
return {
isFetchingUserPosts: state.userPosts.isFetchingUserPosts,
userPosts: state.userPosts.userPosts.userPosts,
userId: state.auth.user._id
}
}
export default connect(mapStateToPros)(UserFeed)
Cards
import React, { Component } from "react"
import { connect } from "react-redux"
import { deletePost } from "../actions/userActions"
class Cards extends Component {
handleDelete = (_id) => {
this.props.dispatch(deletePost(_id))
}
render() {
const { _id, title, description } = this.props.post
return (
<div className="card">
<div className="card-content">
<div className="media">
<div className="media-left">
<figure className="image is-48x48">
<img
src="https://bulma.io/images/placeholders/96x96.png"
alt="Placeholder image"
/>
</figure>
</div>
<div className="media-content" style={{border: "1px grey"}}>
<p className="title is-5">{title}</p>
<p className="content">{description}</p>
<button onClick={() => {this.handleDelete(_id)}} className="button is-success">Delete</button>
</div>
</div>
</div>
</div>
)
}
}
const mapStateToProps = state => {
return state
}
export default compose(withRouter, connect(mapStateToProps))(Cards)
deletePost action
export const deletePost = (id) => {
return async dispatch => {
dispatch({ type: "DELETING_POST_START" })
try {
const res = await axios.delete(`http://localhost:3000/api/v1/posts/${id}/delete`)
dispatch({
type: "DELETING_POST_SUCCESS",
data: res.data
})
} catch(error) {
dispatch({
type: "DELETING_POST_FAILURE",
data: { error: "Something went wrong" }
})
}
}
}
userPosts reducer
const initialState = {
isFetchingUserPosts: null,
isFetchedUserPosts: null,
userPosts: [],
fetchingUserPostsError: null,
isDeletingPost: false,
isDeletedPost: false,
deletingError: false,
}
const userPosts = (state = initialState, action) => {
switch (action.type) {
case "FETCHING_USER_POSTS_START":
return {
...state,
isFetchingUserPosts: true,
fetchingUserPostsError: null,
}
case "FETCHING_USER_POSTS_SUCCESS":
return {
...state,
isFetchingUserPosts: false,
isFetchedUserPosts: true,
userPosts: action.data,
fetchingUserPostsError: null,
}
case "FETCHING_USER_POSTS_ERROR":
return {
...state,
isFetchingUserPosts: false,
isFetchedUserPosts: false,
fetchingUserPostsError: action.data.error,
}
case "DELETING_POST_START":
return {
...state,
isDeletingPost: true,
deletingError: null,
}
case "DELETING_POST_SUCCESS":
const filteredPostList = state.postList.filter((post) => post._id !== action.data._id)
return {
...state,
isDeletingPost: false,
isDeletedPost: true,
userPosts: filteredPostList,
deletingError: null,
}
case "DELETING_POST_ERROR":
return {
...state,
isDeletingPost: false,
deletingError: action.data.error,
}
default:
return state
}
}
export default userPosts
Delete post action needs to pass on id to the reducer upon success.
Delete post action
export const deletePost = (id) => {
return async dispatch => {
dispatch({ type: "DELETING_POST_START" })
try {
const res = await axios.delete(`http://localhost:3000/api/v1/posts/${id}/delete`)
dispatch({
type: "DELETING_POST_SUCCESS",
data: res.data,
id
})
} catch(error) {
dispatch({
type: "DELETING_POST_FAILURE",
data: { error: "Something went wrong" }
})
}
}
}
Access action.id in user posts reducer
case "DELETING_POST_SUCCESS":
return {
...state,
isDeletingPost: false,
isDeletedPost: true,
userPosts: state.postList.filter(post => post._id !== action.id),
deletingError: null,
}
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.