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] }
Related
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>
</>
)
}
I am receiving the error vesselList is not a function in my homescreen that's using a redux slice
the error is in vesselList() in the dispatch in the useEffect hook please suggest if there is a better way i can reform this code.
the Slice :
vesselSlice :
export const vesselSlice = createSlice({
name: "vesselList",
initialState: {
vessels: [],
},
reducers: {
vesselList: (state, action) => {
state.value = action.payload;
},
},
});
export const {
vesselList,
} =
(keyword = "") =>
async (dispatch) => {
try {
dispatch({ type: VESSEL_LIST_REQUEST });
const { data } = await axios.get(
"http://127.0.0.1:8000/api/vessels/info"
);
dispatch({
type: VESSEL_LIST_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: VESSEL_LIST_FAIL,
payload:
error.response && error.response.data.detail
? error.response.data.detail
: error.message,
});
}
};
export default vesselSlice.reducer;
HomeScreen.js :
function HomeScreen() {
const dispatch = useDispatch();
const Listvessel = useSelector((state) => state.vesselList);
const { error, loading, vessels } = Listvessel;
useEffect(() => {
dispatch(vesselList());
}, [dispatch]);
return (
<div>
Fleet vessels :
<div className="fleet-vessels-info">
{vessels.map((vessel) => (
<VesselCard vessel={vessel} />
))}
</div>
</div>
);
}
export default HomeScreen;
Because vesselList not yet export, why you not use createAsyncThunk to fecth url and dispatch to redux store?
i am trying to send the error messages that sent from my server ( express ) to axios and the error message displays in toastify component but the error message doesn't show up here is the login axios function with the toastify how can i display toastify message inside my page from redux ?
here is my code :
// redux controller
const login = async (username, password) => {
await axios.post("/login",{username,password,},
{ withCredentials: true });};
// reducer page
export function generateError(prop) {
return function (dispatch) {
dispatch({
type: "USER_FAIL"
});
toast.error(prop);
};
}
export function generateSuccess(prop) {
return function (dispatch) {
dispatch({
type: "USER_SUCCESS"
});
toast.success(prop);
};
}
export const login = createAsyncThunk(
"/login",
async ({ username, password }) => {
try {
const data = await authService.login(username, password);
if (data) {
if (data.errors) {
const { username, password } = data.errors;
if (username) generateError(username)
else if (password) generateError(password);
} else {
generateSuccess(data.success);
}
}
return { user: data };
} catch (error) {
console.log(error);
}
}
);
// login page
const handleSubmit = (e) => {
e.preventDefault();
dispatch(login({ username, password }));
}
i am using react-tostify and #redux-toolkit but the message doesn't display inside my page
i fixed it and here is my code :
// auth.js ( redux page )
export const login = createAsyncThunk(
"/login",
async ({ username, password }) => {
try {
const {data} = await axios.post(
"/login",
{
username,
password,
},
{ withCredentials: true }
);
return { user: data };
} catch (error) {
console.log(error);
}
});
const initialState = user
? { isLoggedIn: true, user }
: { isLoggedIn: false, user: null };
const authSlice = createSlice({
name: "auth",
initialState,
extraReducers: {
[login.fulfilled]: (state, action) => {
state.isLoggedIn = true;
state.user = action.payload.user;
},
[login.rejected]: (state, action) => {
state.isLoggedIn = false;
state.user = null;
},
[logout.fulfilled]: (state, action) => {
state.isLoggedIn = false;
state.user = null;
},
}})
const { reducer } = authSlice; export default reducer;
Login Page :
const { isLoggedIn } = useSelector((state) => state.auth);
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
dispatch(login({ username, password })).then(data => {
console.log(data)
if (data.payload.user) {
if (data.payload.user.errors) {
const { username, password } = data.payload.user.errors;
if (username) generateError(username)
else if (password) generateError(password);
} else {
generateSuccess(data.success);
navigate("/dashboard");
}
}
})
}
i realized when i back the data it has an object name payload i used it to get the error messages from express and then i put the message in toastify function gettingError and here it is
const generateError = error => {
toast.error(error, {
position: "bottom-right",
})
}
Hai I'm also looking for the same problem while searching I found a solution at with this : react-toastify-with-redux
my Code : authAction.js
import 'react-toastify/dist/ReactToastify.min.css';
import { toast} from 'react-toastify';
export const registerUser = (userData) => dispatch =>{
axios.post('user/register',userData)
.then(res=>toast.success('Your Account Created Successfully 👍'))
.then(res=> window.location = '/authentication/sign-in')
.catch(err=>dispatch(
{
type: GET_ERRORS,
payload: err.response.data
}
),toast.error("Error 😣"))
// .catch((err)=> {return })
};
On your signUp page just add
<ToastContainer />
That's all ...
This answer is probably late. But I came across this problem and decided to do it my way. I know there is toast. promise to handle promises and I don't want to call dispatch.then every time. So I can up with passing dispatch to my action wrapper. Here is my code.
// utils.ts
type ArgumentTypes<F extends CallableFunction> = F extends (
...args: infer A
) => any
? A[0]
: never;
export const withToast = <T = AnyAction | typeof createAsyncThunk>(
action: T,
{ pending, error, success }: ToastPromiseParams<T>
) => {
return (
dispatch: ReturnType<typeof useAppDispatch>,
actionParams?: ArgumentTypes<T & CallableFunction> | void
) => {
const promise = dispatch(
(action as CallableFunction)(actionParams as any)
).unwrap();
toast.promise(promise, {
pending,
error,
success,
});
};
};
// actions.ts
export const login = createAsyncThunk(
"user/login",
async (payload: {
email: string;
password: string;
}): Promise<Partial<LoginAPIResponse>> => {
const { data } = await axios.post(`${API}/${LOGIN_EP}/`, payload);
return data;
}
);
export const loginWithToast = withToast(login, {
pending: "Logging in...",
error: {
render: (error: any) => {
return error?.password || error?.email
? "Invalid email or password"
: "Something went wrong";
},
},
success: "Logged in successfully",
});
// usage in component
const dispatch = useAppDispatch();
loginWithToast(dispatch, {
email: values.email.value,
password: values.password.value,
});
First createAsyncThunk:
import { coreAxios } from "utilities/axios"; // Own customized axios
import { createAsyncThunk } from "#reduxjs/toolkit";
const BASE_URL = process.env.REACT_APP_MAIN_URL
export const GetProducts = createAsyncThunk(
"inventory/GetProducts",
async () => {
const {data} = await coreAxios.get(`${BASE_URL}/api/product/list/`);
return data
}
);
Second createSlice:
import { createSlice } from "#reduxjs/toolkit";
import { GetProducts } from "services/inventory/product.service";
import { toast } from 'react-toastify';
export const productSlice = createSlice({
name: "products",
initialState: {
productsList: [],
productsLoading: false,
productsError: null,
},
extraReducers:
(builder) => {
builder.addCase(GetProducts.pending, (state) => {
toast.loading('Promise is pending...')
state.productsLoading = true
});
builder.addCase(GetProducts.fulfilled, (state, action) => {
toast.dismiss();
toast.success('Promise resolved 👌');
state.productsList = action.payload
state.productsLoading = false
state.productsError = null
});
builder.addCase(GetProducts.rejected, (state, action) => {
toast.dismiss();
toast.error('Promise rejected 🤯 😣')
state.productsLoading = false
state.productsError = action.error?.message
});
},
});
export default productSlice.reducer;
Third page:
import { ToastContainer } from 'react-toastify';
import { useSelector, useDispatch } from "react-redux";
import { GetProducts } from 'services/inventory/product.service';
const Product = () => {
const { productsList, productsLoading, productsError } = useSelector((state) => state.products);
const dispatch = useDispatch();
useEffect(() => {
dispatch(GetProducts());
}, []);
return (
<div className="grid crud-demo">
<h1>Hello Alim</h1>
<ToastContainer />
</div>
);
}
I everyone. I try to post simple login form with a custom fetch hook. The login works fine if I authenticate well at the first time. But in error case if I try to revalidate the form the component seems to be unmounted (hooks clean up function is called) and i can't refetch my api. Why this component is unmounted i don't understand ? Thanks for your help
signin.ts (the code has been simplified)
import React, { useState } from 'react'
import useFetch from '../shared/hooks/useFetch'
const Signin: React.FunctionComponent = () => {
const [postData, setPostData] = useState({
url: "",
options: {}
})
type ResponseT = {
id: string,
firstname: string,
lastname: string,
roles: string[],
accessToken: string
} & string
const { data, error } = useFetch<ResponseT>(postData.url, postData.options)
const handleSubmit = (e: any) => {
if (e) e.preventDefault()
setPostData({
url: `${process.env.REACT_APP_API_HOST}/signin`,
options: {
method: "POST",
body: JSON.stringify({
email: "testt#test.fr",
password: "bla"
})
}
})
}
return (
<form>
<div>
<div className="form-group">
<button onClick={handleSubmit} className="btn btn-primary btn-block">Sign In</button>
{error && <p>{error}</p>}
{data === '' && <p style={{ height: '100px', backgroundColor: 'red' }}>Loading...</p>}
</div>
</div>
</form>
)
}
export default Signin
useFetch.ts
import { useEffect, useReducer, useRef } from 'react'
interface State<T> {
data?: T
error?: Error
}
type Cache<T> = { [url: string]: T }
type Action<T> =
| { type: 'loading' }
| { type: 'fetched'; payload: T }
| { type: 'error'; payload: Error }
function useNiceFetch<T = unknown>(url?: string, options?: RequestInit): State<T> {
const cache = useRef<Cache<T>>({})
const cancelRequest = useRef<boolean>(false)
const initialState: State<T> = {
error: undefined,
data: undefined,
}
const fetchReducer = (state: State<T>, action: Action<T>): State<T | any> => {
switch (action.type) {
case 'loading':
return { ...initialState, data: '' }
case 'fetched':
return { ...initialState, data: action.payload }
case 'error':
return { ...initialState, error: action.payload }
default:
return state
}
}
const [state, dispatch] = useReducer(fetchReducer, initialState)
useEffect(() => {
if (!url) return
const fetchData = async () => {
if (cancelRequest.current) return
dispatch({ type: 'loading' })
if (cache.current[url]) {
dispatch({ type: 'fetched', payload: cache.current[url] })
return
}
try {
const response = await fetch(url, options) as any
const responseJson = await response.json()
if (!response.ok) {
throw responseJson
}
cache.current[url] = responseJson as T
dispatch({ type: 'fetched', payload: responseJson })
} catch (error) {
if (cancelRequest.current) return
dispatch({ type: 'error', payload: error as Error })
}
}
fetchData()
return () => {
cancelRequest.current = true
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url, options])
return state
}
export default useFetch
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 ?