React Redux How to ensure Item is deleted without needing to refresh page using dispatch and slice - javascript

I am working on the following tutorial: https://www.youtube.com/watch?v=UXjMo25Nnvc&list=PLillGF-RfqbbQeVSccR9PGKHzPJSWqcsm&index=4
Full GitHub project here from Brad Traversy: https://github.com/bradtraversy/mern-tutorial
For the Delete Goals section (starting at 37:06 to 44:56 timestamp)
Dashboard file: https://github.com/htqanh305/vocab-app/blob/main/frontend/src/pages/Dashboard.jsx
import {useEffect} from 'react'
import {useNavigate} from 'react-router-dom' // for redirecting
import {useSelector, useDispatch} from 'react-redux' // grab user to check state
import GoalForm from '../components/GoalForm'
import GoalItem from '../components/GoalItem'
import Spinner from '../components/Spinner'
import {getGoals, reset} from '../features/goals/goalSlice'
function Dashboard() {
const navigate = useNavigate()
const dispatch = useDispatch()
const {user} = useSelector((state) => state.auth)
const {goals, isLoading, isError, message} = useSelector((state) => state.goals)
useEffect(() => {
if(isError) {
console.log(message)
}
if(!user) {
navigate('/login')
}
dispatch(getGoals())
console.log("I reached this point")
return () => {
dispatch(reset())
}
}, [user, navigate, isError, message, dispatch])
if(isLoading) {
return <Spinner />
}
if(!user) {
navigate('/login')
} else {
return (
<>
<section className="heading">
<h1>Welcome {user.name} </h1>
<p>Goals Dashboard</p>
</section>
<GoalForm/>
<section className="content">
{goals.length > 0 ? (
<div className="goals">
{goals.map((goal) => (
<GoalItem key={goal._id} goal={goal} />
))}
</div>
) :
(<h3> You have not set any goals </h3>)}
</section>
</>
)
}
}
export default Dashboard
goalSlice file: https://github.com/bradtraversy/mern-tutorial/blob/main/frontend/src/features/goals/goalSlice.js
import {createSlice, createAsyncThunk} from '#reduxjs/toolkit'
import goalService from './goalService'
const initialState = {
goals: [],
isError: false,
isSuccess: false,
isLoading: false,
message: ''
}
// Create new goal
//thunkAPI object has a getState method that helps get anything we want/ any part of the state
// ie. get auth state to get token to access user so we can set/get goals
export const createGoal = createAsyncThunk('goals/create', async(goalData, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token // get token from outside of goal state (auth state)
return await goalService.createGoal(goalData, token)
} catch (error) {
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
return thunkAPI.rejectWithValue(message)
}
} )
// Get user goals
export const getGoals = createAsyncThunk('goals/getAll', async (_, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token // get token from outside of goal state (auth state)
return await goalService.getGoals(token)
} catch (error) {
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
return thunkAPI.rejectWithValue(message)
}
})
// Delete goal
export const deleteGoal = createAsyncThunk('goals/delete', async(id, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token // get token from outside of goal state (auth state)
return await goalService.deleteGoal(id, token)
} catch (error) {
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
return thunkAPI.rejectWithValue(message)
}
} )
export const goalSlice = createSlice({
name: 'goal',
initialState,
reducers: {
reset: (state) => initialState
},
extraReducers: (builder) => {
builder
.addCase(createGoal.pending, (state) => {
state.isLoading = true
})
.addCase(createGoal.fulfilled, (state, action) => {
state.isLoading = false
state.isSuccess = true
state.goals.push(action.payload)
})
.addCase(createGoal.rejected, (state, action) => {
state.isLoading = false
state.isError = true
state.message = action.payload
})
.addCase(getGoals.pending, (state) => {
state.isLoading = true
})
.addCase(getGoals.fulfilled, (state, action) => {
state.isLoading = false
state.isSuccess = true
state.goals = action.payload
})
.addCase(getGoals.rejected, (state, action) => {
state.isLoading = false
state.isError = true
state.message = action.payload
})
.addCase(deleteGoal.pending, (state) => {
state.isLoading = true
})
.addCase(deleteGoal.fulfilled, (state, action) => {
state.isLoading = false
state.isSuccess = true
// filter out the UI when delete a goal, only show goals that are not deleted
console.log("confirm")
state.goals = state.goals.filter(
goal => goal._id !== action.payload.id)
console.log(action.payload.id)
console.log(state.goals)
})
.addCase(deleteGoal.rejected, (state, action) => {
state.isLoading = false
state.isError = true
state.message = action.payload
})
}
})
export const {reset} = goalSlice.actions
export default goalSlice.reducer
GoalItem file:
import {useDispatch} from 'react-redux'
import { deleteGoal } from '../features/goals/goalSlice'
function GoalItem({goal}) {
const dispatch = useDispatch()
return (
<div className="goal">
<div>
{new Date(goal.createdAt).toLocaleDateString('en-US')}
</div>
<h2>{goal.text}</h2>
<button onClick={() => dispatch(deleteGoal(goal._id))} className="close">X</button>
</div>
)
}
export default GoalItem
I'm following along in the tutorial, and no matter how much I try, I can't seem to get the delete goal functionality working.
When I create a new goal (item) and try to delete it, the item still remains on the page until I refresh or click on it twice. I'm not sure if it's something wrong with the goalSlice function or my Dashboard file, but for some reason it's not working.
I followed the tutorial to a tee, but it still looks like it isn't doing what it's supposed to.
Does anyone happen to know what is causing this Redux issue, or if I'm going crazy? Any suggestions or advice would be helpful. Thanks!

Related

I am getting undefined while sending patch request

I am working on a MERN app with redux toolkit. Currently, I am facing a problem with my update functionality, when I click on the update button I can see in redux dev tools the request is rejected and in the console, the id is showing undefined while I am passing it. I am probably missing something in my code, if someone can point it out and explain that would be great. Thanks in advance. Here below are my code:
postService.js:
import axios from 'axios';
const API_URL = '/api/posts/';
const updatePost = async (_id, postData) => {
const response = await axios.patch(API_URL + _id, postData);
return response.data;
};
const postService = {
updatePost,
};
export default postService;
postSlice.js:
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import postService from './postService';
const initialState = {
posts: [],
isError: false,
isSuccess: false,
isLoading: false,
message: '',
};
export const updatePost = createAsyncThunk(
'posts/updatePost',
async ({ id, postData }, thunkAPI) => {
const { postCreator, title, body, imageFile } = postData;
try {
return await postService.updatePost(id, {
postCreator,
title,
body,
imageFile,
});
} catch (error) {
console.log(error.message);
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
);
export const postSlice = createSlice({
name: 'post',
initialState,
reducers: {
reset: (state) => initialState,
},
extraReducers: (builder) => {
builder
.addCase(updatePost.pending, (state) => {
state.isLoading = true;
})
.addCase(updatePost.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
const { id, postCreator, title, body, imageFile } = action.payload;
const existingPost = state.find((post) => post.id === id);
if (existingPost) {
existingPost.postCreator = postCreator;
existingPost.title = title;
existingPost.body = body;
existingPost.imageFile = imageFile;
}
})
.addCase(updatePost.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
})
export default postSlice.reducer;
Form.js:
const Form = ({ activeId, setActiveId }) => {
const [postData, setPostData] = useState({
postCreator: '',
title: '',
body: '',
imageFile: '',
});
const post = useSelector((state) =>
activeId ? state.posts.posts.find((post) => post._id === activeId) : null
);
const user = JSON.parse(localStorage.getItem('user'));
const dispatch = useDispatch();
useEffect(() => {
if (post) setPostData(post);
}, [post]);
const clearInputField = () => {
setActiveId(0);
setPostData({
postCreator: '',
title: '',
body: '',
imageFile: '',
});
};
const handleSubmit = async (e) => {
e.preventDefault();
if (activeId) {
dispatch(updatePost({ activeId, postData }));
clearInputField();
} else {
dispatch(createPost(postData));
clearInputField();
}
};
In the updatePost thunk in postSlice.js, you are attempting to destructure the variables { id, postData } from the payload creator args.
But in Form.js, you are sending an object { activeId, postData } when you dispatch updatePost.
So both id and postData will be undefined because neither exist on the object.
You could change it to:
dispatch(updatePost({id: activeId, postData: formData}))

React Redux state re-render issues with object from mongodb

These are my codes for creating a new blog post:
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { createBlog, reset } from "../features/blogSlice";
const Createblog = () => {
const dispatch = useDispatch();
const [title, setTitle] = useState("");
const [texts, setTexts] = useState("");
const history = useNavigate();
const { user, isError, isSuccess, message } = useSelector((state) => {
return state.auth;
});
useEffect(() => {
if (!user) {
history("/login");
}
dispatch(reset());
}, [user, isError, isSuccess, message, history, dispatch]);
const handleSubmit = async (e) => {
e.preventDefault();
const data = {
title,
texts,
};
dispatch(createBlog(data));
history("/");
};
return (
<>
<div className="container mt-3">
<div className="row">
<div className="col-3"></div>
<div className="col-6 bg-info">
<h1>Create A New Blog</h1>
</div>
<div className="col-3"></div>
</div>
<div className="row">
<div className="col-3"></div>
<div className="col-6">
<form className="mt-3">
<input
type="text"
name="title"
className="form-control my-2"
placeholder="Type your Title"
onChange={(e) => setTitle(e.target.value)}
/>
<textarea
name="texts"
className="form-control"
id="exampleFormControlTextarea1"
rows="10"
onChange={(e) => setTexts(e.target.value)}
></textarea>
<button className="btn btn-primary col-6 my-2" onClick={handleSubmit}>
Click to post
</button>
</form>
</div>
<div className="col-3"></div>
</div>
</div>
</>
);
};
export default Createblog;
This is my redux slice for creating the Blog
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import blogService from "./blogService";
const initialState = {
blogs: [],
isError: false,
isSuccess: false,
isLoading: false,
message: "",
};
export const createBlog = createAsyncThunk("blogs/createBlog", async (blogData, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await blogService.createBlog(blogData, token);
} catch (error) {
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString();
return thunkAPI.rejectWithValue(message);
}
});
export const getBlogs = createAsyncThunk("blogs/", async (_, thunkAPI) => {
try {
// const token = thunkAPI.getState().auth.user.token;
return await blogService.getBlogs();
} catch (error) {
const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString();
return thunkAPI.rejectWithValue(message);
}
});
const blogSlice = createSlice({
name: "blogs",
initialState,
reducers: {
reset: (state) => {
return initialState;
},
},
extraReducers: (builder) => {
// Get All Blogs
builder
.addCase(getBlogs.pending, (state) => {
state.isLoading = true;
})
.addCase(getBlogs.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.blogs = action.payload;
})
.addCase(getBlogs.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
})
// create blog
.addCase(createBlog.pending, (state) => {
state.isLoading = true;
})
.addCase(createBlog.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.blogs.push(action.payload);
})
.addCase(createBlog.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
});
},
});
export const { reset } = blogSlice.actions;
export default blogSlice.reducer;
And, these are the redux blog service codes:
import Axios from "axios";
const api_Link = "http://localhost:8080/";
const createBlog = async (blogData, token) => {
const config = {
headers: {
Authorization: `Bearer ${token}`,
},
};
const response = await Axios.post(api_Link + `blog`, blogData, config);
return response.data;
};
const getBlogs = async () => {
const response = await Axios.get(api_Link);
const data = response.data.data;
return data;
};
const blogService = {
createBlog,
getBlogs,
};
export default blogService;
And, this is how I view the blog posts:
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { getBlogs, reset } from "../features/blogSlice";
const Blogpage = () => {
const dispatch = useDispatch();
const history = useNavigate();
const { blogs, isSuccess, isLoading, isError, message } = useSelector((state) => state.blogs);
useEffect(() => {
if (isError) {
console.log(message);
}
dispatch(getBlogs());
return () => {
dispatch(reset());
};
}, [history, isError, message, dispatch]);
return (
<>
<div className="container">
<div className="row">
<div className="col">
<h1 className="text-centr">Following are your blogs</h1>
</div>
</div>
{blogs.map((item, sl) => {
return (
<section className="bg-success text-light mt-4" key={sl}>
<div className="row">
<div className="col">
<h1 className="text-start">{item.title}</h1>
<h5 className="text-start bg-success text-light">Author: {item.creator.name}</h5>
<p className="text-start">{item.texts}</p>
</div>
</div>
</section>
);
})}
</div>
</>
);
};
export default Blogpage;
Normally I can view the blog posts. Creating blog, redirecting everything works just fine. The problem occurs only when I try to retrieve anything from the creator field. Please see below. Here is what I get from db
{
_id: new ObjectId("62b7a94583a09428b900a069"),
date: 2022-06-26T00:33:09.838Z,
title: 'This is title',
texts: 'These are texts',
creator: {
_id: new ObjectId("62ae7514d2106b408c190667"),
name: 'Md.Rashel',
email: 'example#gmail.com',
}
Whenever I try to retrieve item.creator.name it shows this error : "Uncaught TypeError: Cannot read properties of undefined (reading 'name')". But, once I reload the page, it works fine again.
I'm not sure how to fix this. I'm not also sure whether it is redux issue or react issue. Could you please help fix the issue? Thanks in advance.

React | Redux Toolkit Shared State Undefined

I am creating a simple app to play with Redux ToolKit along react; however, I can see the object in the redux chrome tab, but unable to access it through components using react hooks.
My slice:
import {
createSlice,
createSelector,
createAsyncThunk,
} from "#reduxjs/toolkit";
const initialState = {
cryptoList: [],
loading: false,
hasErrors: false,
};
export const getCryptos = createAsyncThunk("cryptos/get", async (thunkAPI) => {
try {
const cryptosUrl =
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd";
let response = await fetch(cryptosUrl);
console.log(response);
return await response.json();
} catch (error) {
console.log(error);
}
});
const cryptoSlice = createSlice({
name: "cryptos",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getCryptos.pending, (state) => {
state.loading = true;
});
builder.addCase(getCryptos.fulfilled, (state, { payload }) => {
state.loading = false;
state.cryptoList = payload;
});
builder.addCase(getCryptos.rejected, (state, action) => {
state.loading = false;
state.hasErrors = action.error.message;
});
},
});
export const selectCryptos = createSelector(
(state) => ({
cryptoList: state.cryptoList,
loading: state.loading,
}),
(state) => state
);
export default cryptoSlice;
My component:
import React, { useEffect } from "react";
import { getCryptos, selectCryptos } from "./cryptoSlice";
import { useSelector, useDispatch } from "react-redux";
const CryptoComponent = () => {
const dispatch = useDispatch();
const { cryptoList, loading, hasErrors } = useSelector(selectCryptos);
useEffect(() => {
dispatch(getCryptos());
}, [dispatch]);
const renderCrypto = () => {
if (loading) return <p>Loading Crypto...</p>;
if (hasErrors) return <p>Error loading news...</p>;
if (cryptoList) {
return cryptoList.data.map((crypto) => <p> {crypto.id}</p>);
}
};
return (
<div className="container">
<div className="row">CryptoComponent: {renderCrypto()}</div>
</div>
);
};
export default CryptoComponent;
All constructed values from the state: cryptoList, loading, hasErrors, seem to be undefined at the component level.
Any suggestions are appreciated!
Have you tried using the following createSelector code:
export const selectCryptos = createSelector(
(state) => state,
(state) => ({
cryptoList: state.cryptoList,
loading: state.loading,
})
);
As per documentation state should be the first parameter:
https://redux.js.org/usage/deriving-data-selectors

TypeError: Products.find is not a function in( react/typescript)

I use redux thunk
why? find is notFunction
on console log redux given data
help pls
I have a strange problem because even data is sent inside the console and I can access the data but can not find in Redax even the initial value of the Hats object
my dependency
import React, { useEffect } from 'react';
import * as b from 'react-bootstrap';
import { useHistory } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux'
import { productsDetailActions } from '../StateManagement/Actions/productActions';
type Props = {
match: any;
params: any;
pro: any,
}
interface PropsInterface {
match: Props;
}
const SingleProduct = ({ match }: PropsInterface) => {
const dispatch = useDispatch()
console.log(match.params.id)
useEffect(() => {
dispatch(productsDetailActions(match.params.id))
}, [])
const Products = useSelector((state: any) => (state.productDetail.product))
console.log(Products)
const product = Products.find((item: any) => {
return item._id === match.params.id
})
console.log(product)
const history = useHistory()
return (
<div>
<b.Row className='my-2 '>
<b.Col>
<b.Button className='btn-danger' onClick={() => history.goBack()}>بازگشت</b.Button>
</b.Col>
</b.Row>
<b.Row>
<b.Col md={5}>
<b.Card className="shadow-sm p-3 mb-5 bg-white">
<b.Card.Body className=''>
<b.Image src={product && product?.image} fluid />
<b.Card.Text className="text-center">{product && product?.description}</b.Card.Text>
</b.Card.Body>
<b.Card.Body className="text-muted">
<h3 style={{ textAlign: 'center' }}>{product && product?.name}</h3>
</b.Card.Body>
</b.Card>
</b.Col>
<b.Col xs={5} md>
<b.Card className='shadow-sm p-3 mb-5 bg-white'>
<b.Card.Header>
<h5 style={{ textAlign: 'center' }}>{product && product?.name}</h5>
</b.Card.Header>
<b.Card.Body>
<b.ListGroup variant='flush'>
<b.ListGroup.Item>
{product && product?.description}
</b.ListGroup.Item>
<b.ListGroup.Item>
{product && product?.name}
</b.ListGroup.Item>
</b.ListGroup>
</b.Card.Body>
<b.Card.Footer>
<b.Button type='button' className='btn btn-block' >خرید محصول</b.Button>
</b.Card.Footer>
</b.Card>
</b.Col>
</b.Row>
</div>
)
}
export default SingleProduct
How can I prevent such problems from occurring? I have a similar one even on the map method
and my reducers are the following:
interface ActionInterface {
payload?: any,
type?:string
}
type state = {
state: any,
loading: boolean,
products?:any
}
const initialState = {
products: [],
loading:false,
}
export const productReducers = (state=initialState,action:ActionInterface) => {
switch (action.type) {
case 'send_req':
return { loading: true, products: [] }
case 'req_success':
return { loading: false, products: action.payload }
default:
return state
}
}
export const productDetailReducers = (state={product:{}},action:ActionInterface) => {
switch (action.type) {
case 'send_req_detail':
return { loading: true, ...state}
case 'req_success_detail':
return { loading: false, product: action.payload }
default:
return state
}
}
and these are the actions:
import axios from 'axios'
export const productsActions = () => async (dispatch: any) => {
try {
dispatch({ type: 'send_req' })
const response = await axios.get('http://localhost:8001/api/products')
dispatch({ type: 'req_success', payload: response.data})
}catch (err) {
console.log(err)
}
}
export const productsDetailActions = (id:any) => async (dispatch: any) => {
try {
dispatch({ type: 'send_req_detail' })
const response = await axios.get(`http://localhost:8001/api/products/${id}`)
dispatch({ type: 'req_success_detail', payload: response.data })
console.log(response.data)
}catch (err) {
console.log(err)
}
}
There's a chance you are using the wrong keys to grab something from your redux store.
You are seeing this error: Product.find is not a function because your Product is likely not an array.
I can affirm the above looking at your reducer function for product detail:
export const productDetailReducers = (state={product:{}},action:ActionInterface) => {
Notice that the initial state value here is: product:{}.
hence const Products = useSelector((state: any) => (state.productDetail.product)) will return {} which DOESN'T have a find method.
Review these lines:
const Products = useSelector((state: any) => (state.productDetail.product))
console.log(Products)
const product = Products.find((item: any) => {
return item._id === match.params.id
})
And maybe set product=[] here:
export const productDetailReducers = (state={ product:[] }, action: ActionInterface) => {
It'll be helpful if you could share a snippet of your store object.
Tip: you should really ever use any in TS as last resort.

Why is the array in my redux reducer not available from another component after a redirect to another page of my app?

I have two separate components. I want to have a button that when clicked on will add an element to an array in my reducer and redirect to another component, this component that gets redirected to needs to render the data that was just added to the array. The page redirects to the component I want but the data does not load and the console.logs don't show anything.
This is the component that has the redirect button. On this component the console.log(socialNetworkContract.members[0]) shows the string I expect.
const Posts = () => {
const dispatch = useDispatch();
const getProfile = async (member) => {
const addr = await dispatch({ type: 'ADD_MEMBER', response: member })
console.log(member)
window.location.href='/member'
console.log('----------- member------------')
console.log(socialNetworkContract.members[0])
}
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
return (
<div>
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
<button onClick={() => getProfile(p.publisher)}>Profile</button>
</tr>})}
</div>
)
}
export default Posts;
This is my reducer
import { connect, useDispatch, useSelector } from "react-redux";
let init = {
posts:[],
post:{},
profiles:[],
profile:{},
members:[],
member:{}
}
export const socialNetworkContract = (state = init, action) => {
const { type, response } = action;
switch (type) {
case 'ADD_POST':
return {
...state,
posts: [...state.posts, response]
}
case 'SET_POST':
return {
...state,
post: response
}
case 'ADD_PROFILE':
return {
...state,
profiles: [...state.profiles, response]
}
case 'SET_PROFILE':
return {
...state,
profile: response
}
case 'ADD_MEMBER':
return {
...state,
members: [...state.members, response]
}
case 'SET_MEMBER':
return {
...state,
member: response
}
default: return state
}
};
and this is the component that is redirected to. this just says undefined in console.log(socialNetworkContract.members[0])
const Member = () => {
const [user, setUser] = useState({});
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
useEffect(async()=>{
try {
const pro = socialNetworkContract.members[0]
console.log(socialNetworkContract.members[0])
await setUser(pro)
console.log(socialNetworkContract.members[0])
} catch (e) {
console.error(e)
}
}, [])
I have the route set in Routes.js as
<Route path="/member" exact component={Member} />
Use history.push('/') instead of window.location.href which will reload your whole page and you will lost your local state data.
const {withRouter} from "react-router-dom";
const Posts = (props) => {
const dispatch = useDispatch();
const getProfile = async (member) => {
const addr = await dispatch({ type: 'ADD_MEMBER', response: member })
console.log(member)
props.history.push('/member');
console.log('----------- member------------')
console.log(socialNetworkContract.members[0])
}
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
return (
<div>
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
<button onClick={() => getProfile(p.publisher)}>Profile</button>
</tr>})}
</div>
)
}
export default withRouter( Posts );

Categories

Resources