I'm a begineer to React, redux and web development. I'm making a CRUD application using JSON placeholder as dummy backend , react and redux for showing posts.
I want to render out all the posts form JSON placeholder and simply display it
Post.js
import React, { useEffect, useState } from "react"
import { useDispatch, useSelector } from "react-redux"
import { useNavigate } from "react-router-dom"
import { getPostAll } from "../redux/features/PostSlice"
return (
<>
<div className="row mt-4 d-flex align-items-center justify-content-center">
<h2 className="row mt-4 d-flex align-items-center justify-content-center">
Dashboard
</h2>
<div>
<h2 className="row mt-4 d-flex align-items-center justify-content-center">
All Posts
</h2>
{/* {console.log(getPostAll)} */}
{console.log(dispatch(getPostAll()))}
{dispatch(getPostAll).forEach((element) => {
console.log(element)
})}
</div>
</>
)
}
export default Posts
PostSlice.js
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit"
export const getPost = createAsyncThunk("post/getPost", async ({ id }) => {
return fetch(`https://jsonplaceholder.typicode.com/posts/${id}`).then((res) =>
res.json()
)
})
export const getPostAll = createAsyncThunk("posts/getPosts", async () => {
return fetch(`https://jsonplaceholder.typicode.com/posts`)
.then((res) => res.json())
.then((json) => console.log(json, typeof json, json[0].id))
})
// export const getPostAll = fetch("https://jsonplaceholder.typicode.com/posts")
// .then((response) => response.json())
// .then((json) => console.log(json))
export const deletePost = createAsyncThunk(
"post/deletePost",
async ({ id }) => {
return fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
method: "DELETE",
}).then((res) => res.json())
}
)
export const createPost = createAsyncThunk(
"post/createPost",
async ({ values }) => {
return fetch(`https://jsonplaceholder.typicode.com/posts`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-type": "application/json",
},
body: JSON.stringify({
title: values.title,
body: values.body,
}),
}).then((res) => res.json())
}
)
export const updatePost = createAsyncThunk(
"post/updatePost",
async ({ id, title, body }) => {
return fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
method: "PUT",
headers: {
Accept: "application/json",
"Content-type": "application/json",
},
body: JSON.stringify({
title,
body,
}),
}).then((res) => res.json())
}
)
const PostSlice = createSlice({
name: "post",
initialState: {
post: [],
loading: false,
error: null,
body: "",
edit: false,
},
reducers: {
setEdit: (state, action) => {
state.edit = action.payload.edit
state.body = action.payload.body
},
},
extraReducers: {
[getPost.pending]: (state, action) => {
state.loading = true
},
[getPost.fulfilled]: (state, action) => {
state.loading = false
state.post = [action.payload]
},
[getPost.rejected]: (state, action) => {
state.loading = false
state.error = action.payload
},
[deletePost.pending]: (state, action) => {
state.loading = true
},
[deletePost.fulfilled]: (state, action) => {
state.loading = false
state.post = [action.payload]
},
[deletePost.rejected]: (state, action) => {
state.loading = false
state.error = action.payload
},
[createPost.pending]: (state, action) => {
state.loading = true
},
[createPost.fulfilled]: (state, action) => {
state.loading = false
state.post = [action.payload]
},
[createPost.rejected]: (state, action) => {
state.loading = false
state.error = action.payload
},
[updatePost.pending]: (state, action) => {
state.loading = true
},
[updatePost.fulfilled]: (state, action) => {
state.loading = false
state.post = [action.payload]
},
[updatePost.rejected]: (state, action) => {
state.loading = false
state.error = action.payload
},
},
})
export const { setEdit } = PostSlice.actions
export default PostSlice.reducer
I want to display all the posts from the dummy database
I have tried looping using map functinality and forEach but not desired results.
You want to keep the task of requesting the posts separate from the task of accessing the posts.
To request the posts, you want to dispatch your getPostAll action. You only need to do this once, so put it inside a useEffect.
const dispatch = useDispatch();
useEffect(() => {
dispatch(getPostAll());
}, []);
You don't need to return anything here. Remember, we are separating those tasks.
It looks like you're not actually responding to the getPostAll thunk in your reducer? That's a problem which you need to fix for this approach to work.
Use one or more useSelector hooks to access the current value of the redux state. These values will update automatically as your reducer responds to the changes which were initiated by the getPostAll. On your first render you will have no data, then you'll have no data but loading: true. Finally you will have either posts or an error. Your component needs to render the correct content for all of these cases.
const Posts = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getPostAll());
}, []);
const { post, loading, error } = useSelector(state => state.posts);
// I find it easier to extract this part and `return` instead of using a bunch of `&&`
const renderContent = () => {
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error</div>;
}
if (post) {
return (
<div>
{post.map(item =>
<Post key={item.id} data={item}/>
)}
</div>
);
}
return null;
}
return (
<>
<div className="row mt-4 d-flex align-items-center justify-content-center">
<h2 className="row mt-4 d-flex align-items-center justify-content-center">
Dashboard
</h2>
<div>
<h2 className="row mt-4 d-flex align-items-center justify-content-center">
All Posts
</h2>
{renderContent()}
</div>
</>
)
}
Related
I'm using react with Context Api and react hook form to add a new post. When I submit a post, the new post is in the last position but when I refresh the page, the post goes to the top position. I want the new post to be in the top position immediately. Do you know what I did wrong, please?
PostsContext.tsx
export interface Post {
id: number
content: string
thumbnail: {
url: string
}
created_at: string
updated_at: string
userId: number
}
export interface PostsState {
posts: Post[]
}
export type PostsAction =
| { type: 'SET_POSTS'; payload: Post[] }
| { type: 'ADD_POST'; payload: Post }
const initialState: PostsState = {
posts: [],
}
const reducer = (state: PostsState, action: PostsAction) => {
switch (action.type) {
case 'SET_POSTS':
return { posts: action.payload }
case 'ADD_POST':
return { posts: [...state.posts, action.payload] }
default:
return state
}
}
export const PostsContext = createContext<{
state: PostsState
dispatch: React.Dispatch<PostsAction>
}>({
state: initialState,
dispatch: () => null,
})
export const PostsProvider = ({ children }: PropsWithChildren<{}>) => {
const [state, dispatch] = useReducer(reducer, initialState)
return <PostsContext.Provider value={{ state, dispatch }}>{children}</PostsContext.Provider>
}
Feed.tsx
const Feed = () => {
const { state, dispatch } = usePostsContext()
useEffect(() => {
const fetchPosts = async () => {
const res = await fetch(`${import.meta.env.VITE_API_URL}/api/posts`, {
credentials: 'include',
})
const data = await res.json()
if (res.ok) {
dispatch({ type: 'SET_POSTS', payload: data })
}
}
fetchPosts()
}, [])
return (
<div className="container mx-auto">
<PostForm />
{state.posts.length < 1 ? (
<div className="mt-4 p-8 text-center border border-gray-200 rounded-lg">
<h2 className="text-2xl font-medium">There's nothing here...</h2>
<p className="mt-4 text-sm text-gray-500">
Created posts will appear here, try creating one!
</p>
</div>
) : (
state.posts.map((post) => <Posts key={post.id} post={post} />)
)}
</div>
)
}
export default Feed
PostForm.tsx
const PostForm = () => {
const { dispatch } = usePostsContext()
const {
register,
handleSubmit,
control,
watch,
reset,
formState: { isSubmitSuccessful, errors },
} = useForm<FormInput>({
defaultValues: {
content: '',
},
resolver: yupResolver(postSchema),
})
const selectedFile = watch('thumbnailFile')
const onSubmit: SubmitHandler<FormInput> = async (data) => {
const formData = new FormData()
formData.append('content', data.content)
formData.append('thumbnailFile', data.thumbnailFile[0])
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/posts`, {
method: 'POST',
credentials: 'include',
body: formData,
})
const post = await response.json()
if (response.ok) {
console.log('post created', post)
dispatch({ type: 'ADD_POST', payload: post })
reset()
}
}
return (
<MyForm />
)
}
export default PostForm
Looks like your ADD_POST reducer is adding a new post to the end of the list since you are placing the action payload after the destructing of your old posts.
To place a post at the beginning of the list you need to place the action payload before destructing the old list of posts.
i.e.,
case 'ADD_POST':
return { posts: [action.payload, ...state.posts] }
I am building a blog app using react and redux toolkit. I have a problem when I am trying to delete the post, it is removed in my database and I got also the success message in the UI that the Redux toolkit pulls from MongoDB. But the problem is that when I click on the delete button the post is not removed right away and I need to manually reload the page so that it can be removed from the Ui. Can someone point out what am I doing wrong? Thanks. Here below is the logic I am doing:
postSlice.js:
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import postService from './postService';
const initialState = {
posts: [],
isError: false,
isSuccess: false,
isLoading: false,
message: '',
};
// Create a post
export const createPost = createAsyncThunk(
'posts/create',
async (postData, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await postService.createPost(postData, token);
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
);
// Get all posts
export const getPosts = createAsyncThunk(
'posts/getAll',
async (_, thunkAPI) => {
try {
return await postService.getPosts();
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
);
// Delete post
export const deletePost = createAsyncThunk(
'posts/delete',
async (id, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await postService.deletePost(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 postSlice = createSlice({
name: 'post',
initialState,
reducers: {
reset: (state) => initialState,
},
extraReducers: (builder) => {
builder
.addCase(createPost.pending, (state) => {
state.isLoading = true;
})
.addCase(createPost.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state?.posts.push(action.payload);
})
.addCase(createPost.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
})
.addCase(getPosts.pending, (state) => {
state.isLoading = true;
})
.addCase(getPosts.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.posts = action.payload;
})
.addCase(getPosts.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
})
.addCase(deletePost.pending, (state) => {
state.isLoading = true;
})
.addCase(deletePost.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.posts = state.posts.filter((post) => post._id !== action.payload);
console.log(action.payload);
})
.addCase(deletePost.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
});
},
});
export const { reset } = postSlice.actions;
export default postSlice.reducer;
postService.js
import axios from 'axios';
const API_URL = '/api/posts/';
// Create a post
const createPost = async (postData, token) => {
const config = {
headers: {
Authorization: `Bearer ${token}`,
},
};
const response = await axios.post(API_URL, postData, config);
return response.data;
};
// Get all posts
const getPosts = async () => {
const response = await axios.get(API_URL);
return response.data;
};
// Delete a post
const deletePost = async (postId, token) => {
const config = {
headers: {
Authorization: `Bearer ${token}`,
},
};
const response = await axios.delete(API_URL + postId, config);
return response.data;
};
const postService = {
createPost,
getPosts,
deletePost,
};
export default postService;
post.js
import React from 'react';
import './post.css';
import { FaSmileBeam, FaRegSmile } from 'react-icons/fa';
import { RiEdit2Fill } from 'react-icons/ri';
import { MdDeleteOutline } from 'react-icons/md';
import moment from 'moment';
import { useDispatch } from 'react-redux';
import {
deletePost,
} from '../../features/posts/postSlice';
const Post = ({ post }) => {
const dispatch = useDispatch();
const user = JSON.parse(localStorage.getItem('user'));
return (
<>
<FaRegSmile className="smile__icon" />
</>
);
};
const removePost = () => {
dispatch(deletePost(post._id));
};
const { title, createdAt, body, imageFile, postCreator, author } = post;
return (
<div className="post__container card">
<div className="post__card">
<div className="post__image">
<img src={imageFile} alt="post" />
</div>
<div className="post__content">
<div className="edit__icon">
{author === user?.result?._id && (
<RiEdit2Fill className="edit__icon" fontSize="small" />
)}
</div>
<div className="post__header__title">
<h3>{title}</h3>
</div>
<div className="post__message">
<p>{body}</p>
</div>
<div className="post__header">
<div className="post__header__date">
{user ? (
<span className="user__avatar">
<p>{user?.result?.username.charAt(0).toUpperCase()}</p>
</span>
) : (
''
)}
<span className="post__header__date__text">
{postCreator.toUpperCase()}
<p className="moments">{moment(createdAt).fromNow()}</p>
</span>
</div>
</div>
<div className="post__actions">
<div className="icon__box">
<Smile disabled={!user} />
</div>
<div className="icon__box">
<MdDeleteOutline className="delete__icon" onClick={removePost} />
</div>
</div>
</div>
</div>
</div>
);
};
export default Post;
you need to change following:
const deletePost = async (postId, token) => {
const config = {
headers: {
Authorization: `Bearer ${token}`,
},
};
const response = await axios.delete(API_URL + postId, config);
if(response.data){
return postId;
}
};
Explanation:
In your case, deletePost service was just returning the success message that resulted in action.payload to be not id of post in following:
.addCase(deletePost.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.posts = state.posts.filter((post) => post._id !== action.payload);
console.log(action.payload);
})
this was causing the issue as your logic relies on filtering the deleted post id that should be provided by action.payload. In the above code I have just returned postId whenever deletePost is successful which provides correct action.payload to above case
Hi first post here so please go easy with me. I new to redux and wondered why im not get a rerender when my store is being updated successfully.
here is my reducer
const initialState = {
pending: false,
loadPlanning: [],
error: null
}
export const loadPlanningReducer = (state = initialState, action) => {
if (action.type === INITIALISE_LOAD_PLANNING_PENDING) {
return {
...state,
pending: true
}
}
if (action.type === INITIALISE_LOAD_PLANNING_SUCCESS) {
console.log('updating state');
return Object.assign({}, state, {
...state,
pending: false,
loadPlanning: state.loadPlanning.concat(action.loadPlanning.items)
});
}
if (action.type === INITIALISE_LOAD_PLANNING_ERROR) {
return {
...state,
pending: false,
error: action.error
}
}
return state;
}
export default loadPlanningReducer;
export const getLoadPlanning = (state) => { console.log('reducer state',state); return state.loadPlanning };
export const getLoadPlanningPending = (state) => state.pending;
export const getLoadPlanningError = (state) => state.error;
The view looks like
const mapStateToProps = (state, ownProps) => ({
error: getLoadPlanningError(state),
loadPlanning: getLoadPlanning(state),
pending: getLoadPlanningPending(state),
options: state.options.options,
option: state.options.currentOption,
oidc: state.oidc
})
const mapDispatchToProps = (dispatch) => {
return {
dispatch
};
}
const fetchLoadPlanning = async (props) => {
props.dispatch(initialiseLoadPlanning());
const httpOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${xxxxxxxx}`
}
};
fetch('/Api/Planning/Get_Data?' + "database=xxxxxx", httpOptions)
.then(res => res.json())
.then(res => {
if (res.hasError) {
throw res.error;
}
props.dispatch(initialiseLoadPlanningSuccess(res.data));
return res.data;
})
.catch(error => {
props.dispatch(initialiseLoadPlanningError(error));
});
}
const LoadPlanningList = (props) => {
useEffect(() => {
fetchLoadPlanning(props);
}, [])
useEffect(() => {
console.log('props changed',props);
},[props])
}
The console log of props changed happens on change of props.pending but not on the dispatch of props.dispatch(initialiseLoadPlanningSuccess(res.data));
console log
You help and wisdom would be most helpful, thanks in advance
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 ?
I am learning Redux so this may be a basic question and answer but here we go.
I am trying to fetch a list of items from my backend api (/api/items) and pass them into my ShoppingList.js component so that when the component loads the list of items will be displayed. I think that I have to fetch the data in the action, and then call that action in the useEffect hook in the react component, but I am not too sure as I can't seem to get it to work.
Can anyone help me figure this out?
Redux.js
import { createStore } from 'redux';
import axios from 'axios';
const initialState = {
items: [],
loading: false,
};
export const store = createStore(
reducer,
initialState,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
function reducer(state, action) {
switch (action.type) {
case 'GET_ITEMS':
return {
...state,
items: action.payload,
loading: false,
};
case 'ADD_ITEM':
return {
...state,
items: [...state.items, action.payload],
};
case 'DELETE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload),
};
case 'ITEMS_LOADING':
return {
...this.state,
loading: true,
};
default:
return state;
}
}
export const getItemsAction = () => ({
return(dispatch) {
console.log('here');
axios.get('api/items').then(response => {
console.log(response);
dispatch({ type: 'GET_ITEMS', payload: response.data });
});
},
});
export const addItemAction = item => ({
type: 'ADD_ITEM',
payload: item,
});
export const deleteItemAction = item => ({
type: 'DELETE_ITEM',
payload: item,
});
export const setItemsLoading = () => ({
type: 'ITEMS_LOADING',
});
ShoppingList.js
export default function ShoppingList() {
const items = useSelector(state => state.items);
const dispatch = useDispatch();
const addItem = name => dispatch(addItemAction(name));
const deleteItem = id => dispatch(deleteItemAction(id));
useEffect(() => {
//call get items dispatch?
getItemsAction();
});
return (
<div className="container mx-auto">
<button
onClick={() => {
const name = prompt('Enter Item');
if (name) {
// setItems([...items, { id: uuid(), name: name }]);
addItem({
id: uuid(),
name: name,
});
}
}}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mt-4"
>
Add Item
</button>
<ul className="mt-4">
<TransitionGroup className="shopping-list">
{items.map(({ id, name }) => (
<CSSTransition
key={id}
timeout={500}
classNames="fade"
style={{ marginBottom: '0.5rem' }}
>
<li>
{' '}
<button
className="bg-red-500 rounded px-2 mr-2 text-white"
onClick={deleteItem.bind(this, id)}
>
×
</button>
{name}
</li>
</CSSTransition>
))}
</TransitionGroup>
</ul>
</div>
);
}
You are close to where you need to be, the missing piece is that redux is synchronous, so you need to use something like redux-thunk or redux-saga to handle async actions such as network requests.
Once you have setup whatever library you want, you would call it similarly to calling a redux action, from a useEffect like you suggest.
So for example, if you use a thunk:
export const getItemsAction = () => ({
return (dispatch) => {
axios.get('api/items').then(response => {
console.log(response)
dispatch({ type: 'GET_ITEMS_SUCCESS', payload: response.data })
}).catch(err => dispatch({ type: 'GET_ITEMS_ERROR', error: err })
},
})
Then you could call it from your effect:
useEffect(() => {
dispatch(getItemsAction())
}, []);
Make sure you add an empty dependency array if you only want to call it once, otherwise it will get called every time your component refreshes.
Note that the thunk is able to dispatch other redux actions, and I changed a few of your actions types to make it more clear what is going on;
GET_ITEMS_SUCCESS is setting your successful response & GET_ITEMS_ERROR sets any error if there is one.