I have redux-toolkit setup for managing app state. There is an 'authenticated' state variable that is false/true depending if the user is logged in. In the loginSuccessful reducer we set 'is_auth' in local storage and pull it from local storage in 'initialState'. In the login component we retrieve this variable from state to try and redirect based on the login being successful or not, but I can't seem to read the state variable after it is changed from the reducer and a redirect never happens...
here is the logic in login component
//app state
const authenticated = useSelector((state) => state.user.authenticated);
const error = useSelector((state) => state.user.error);
//form state
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const dispatch = useDispatch();
const handleLogin = (e) => {
e.preventDefault();
login({ email, password }, dispatch);
};
if (authenticated) return <Navigate to='/admin' />;
here is the reducer code:
const userSlice = createSlice({
name: 'user',
initialState: {
currentUser: null,
isLoading: false,
error: false,
authenticated: null || localStorage.getItem('is_auth'),
},
reducers: {
loginStart: (state) => {
state.isLoading = true;
state.error = false;
// state.authenticated = false;
},
loginSuccess: (state, action) => {
state.isLoading = false;
state.currentUser = action.payload;
localStorage.setItem('is_auth', true);
// state.authenticated = true;
},
loginFailure: (state) => {
state.isLoading = false;
state.error = true;
// state.authenticated = false;
},
logout: (state) => {
state.currentUser = null;
},
},
});
here is the login API request:
export const login = async (creds, dispatch) => {
dispatch(loginStart());
try {
const res = await apiRequest.post('auth/login', creds);
dispatch(loginSuccess(res.data));
} catch (err) {
dispatch(loginFailure());
}
};
Related
The goal is to basically "logout", which means removing the JWT that's currently stored in local storage.
Ideally, what I want is that the logout button is clicked, the logout function is dispatched which is essentially just removing the JWT from local storage, the reset function is dispatched which means that the state is reverted back to the initial state, and finally, it will navigate onto the login page via the useNavigate hook.
The problem is, it can't seem to work. Oddly enough, I tried testing out if it's reading anything on the selector
Here is the code for the dashboard, which checks if there's an existing user in local storage, and if null, redirects to the login page:
const navigate = useNavigate();
const dispatch = useDispatch();
const { user } = useSelector((state) => state.auth);
const { posts, isLoading, isError, message } = useSelector(
(state) => state.post
);
useEffect(() => {
if (isError) {
console.log(message);
}
if (!user) {
navigate("/login");
}
dispatch(getPosts());
return () => {
dispatch(reset());
};
}, [user, message, isError, navigate, dispatch]);
if (isLoading) {
return <Spinner />;
}
here's the logout function from the slice as well as the slice containing the reset:
export const logout = createAsyncThunk("auth/logout", async () => {
await authService.logout();
});
export const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
reset: (state) => {
state.isLoading = false;
state.isSuccess = false;
state.isError = false;
state.message = "";
},
},
extraReducers: (builder) => {
builder
.addCase(register.pending, (state) => {
state.isLoading = true;
})
.addCase(register.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.user = action.payload;
})
.addCase(register.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
state.user = null;
})
.addCase(login.pending, (state) => {
state.isLoading = true;
})
.addCase(login.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.user = action.payload;
})
.addCase(login.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
state.user = null;
})
.addCase(logout.fulfilled, (state) => {
state.user = null;
});
},
});
Here's the function that handles the logout button click event:
const handleLogout = () => {
dispatch(logout());
dispatch(reset());
navigate("/");
};
Edit:
The returned callback function in the first set of codes is basically the same all throughout, it just reverts the state into the default state (null values).
The logout action is asynchronous, and the logout.fulfilled action is what clears the state.user back to null. The handleLogout function doesn't wait for the Promise to resolve though, it just dispatches the two actions and immediately navigates to "/". It's likely the case that the logout logic hasn't completed and reset the user state when the UI is checking the user state on the dashboard.
It seems that handleLogout should wait for the logout action to resolve, then issue the other action dispatch and navigate.
export const logout = createAsyncThunk("auth/logout", async () => {
return await authService.logout(); // <-- return Promise
});
const handleLogout = async () => {
await dispatch(logout()); // <-- await Promise to resolve
dispatch(reset());
navigate("/");
};
I want to create createAsyncThunk function for register user but there is a problem when rejected action, it is throw an error
the code of authSlice:
`
export const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
reset: (state) => {
state.isLoading = false;
state.isSuccess = false;
state.isError = false;
state.message = "";
},
},
extraReducers: (builder) => {
builder
.addCase(register.pending, (state) => {
state.isLoading = true;
})
.addCase(register.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.user = action.payload;
})
.addCase(register.rejected, (state, action) => {
console.log({ action });
state.isLoading = false;
state.isError = true;
state.message = action.payload;
state.user = null;
})
},
});
the code of function register user
// Register user
export const register = createAsyncThunk(
"auth/register",
async (user, thunkAPI) => {
const { rejectWithValue } = thunkAPI;
try {
return await authService.register(user);
} catch (error) {
const message = error.message;
return rejectWithValue(message);
}
}
);
import axios from "axios";
const API_URL = "http://localhost:5000/auth/condidate";
const register = async (userData) => {
const response = await axios.post(API_URL, userData);
if (response.data) {
localStorage.setItem("user", JSON.stringify(response.data));
}
return response.data;
};
const authService = {
register,
};
export default authService;
`
there is no problem with register fulfilled but rejected regisiter throw an error
enter image description here
try to register user with react and redux toolkit but there is a problem with reject the action of register
const initialState = {
user: [],
isLoading: false,
error: false
}
export const registerNewUser = createAsyncThunk('register/registerNewUser',
async({ rejectWithValue }) => {
try {
const resp = await axios.post('http://localhost:4000/register')
return resp.data
} catch (error) {
return rejectWithValue(error.response.data)
}
})
This is my register slice
const registerSlice = createSlice({
name: 'user',
initialState,
reducers: {
newUser: {
reducer(state, action) {
state.user = action.payload
}
}
},
extraReducers(builder) {
builder
.addCase(registerNewUser.pending, (state) => {
state.isLoading = true;
})
.addCase(registerNewUser.fulfilled, (state, action) => {
console.log(action);
state.isLoading = false;
state.error = false;
state.user = action.payload
console.log(action.payload);
})
.addCase(registerNewUser.rejected, (state) => {
state.isLoading = false;
state.error = true;
})
}
})
This is my function
const Home = () => {
const dispatch = useDispatch()
const navigate = useNavigate()
const errRef = useRef()
const [name, setName] = useState('')
const [username, setUsername] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [id, setId] = useState(1)
const [errMsg, setErrMsg] = useState('')
useEffect(() => {
setErrMsg('')
}, [name, username, email, password])
const signupBtn = (e) => {
e.preventDefault()
try {
dispatch(registerNewUser(newUser));
if(name && username && email && password)
navigate('/login')
} catch (error) {
console.log(error);
}
And this is my store
import { configureStore } from '#reduxjs/toolkit'
import usersReducer from '../features/usersSlice'
import registerReducer from '../features/registerSlice'
export const store = configureStore({
reducer: {
users: usersReducer,
register: registerReducer
}
})
Two problems with the registerNewUser action creator:
the arg parameter is missing from the argument list of the payload callback function, and
the fulfillWithValue utility function is not being used to return (fulfill) a value from the action creator.
The signature of createAsyncThunk is: createAsyncThunk( <action_type_string>, <payload_callback_function>[, <options_object>] ).
The payload callback function has this signature: payload_callback_function( <arg>, <thunkAPI> ). <arg> is the value (most likely an object) passed to the payload callback function when calling the action creator. For example:
registerNewUser( newUser ); // where <arg> = `newUser`.
Your registerNewUser action creator payload callback function has the <thunkAPI> argument in the position where <arg> should be (i.e. the first argument in the argument list).
The Redux Toolkit docs say that:
fulfillWithValue is a utility function that you can return in your action creator to fulfill with a value...
Try updating registerNewUser to look something like this:
export const registerNewUser = createAsyncThunk('register/registerNewUser',
async( newUser, { fulfillWithValue, rejectWithValue }) => {
// It isn't clear if you intended to use 'newUser' in this function or not.
// But it is sent from your 'signupBtn' event handler function when
// dispatch(registerNewUser(newUser)) is called.
try {
const resp = await axios.post('http://localhost:4000/register')
return fulfillWithValue(resp.data)
} catch (error) {
return rejectWithValue(error.response.data)
}
}
I use react redux redux-toolkit
When switching to CardDetails, everything works and I get data in the state of only one element, but when I go from this page to any other page that has useEffect and in which I use the same state, it does not work and I have to reload the page manually. For what reason can this happen?
Here is the page code for one element
const CardDetails = () => {
const {id} = useParams()
const dispatch = useDispatch()
const location = useLocation()
const {posts, loading} = useSelector((state) => ({...state.posts}))
useEffect(() => {
dispatch(getPost(id))
}, []);
console.log(posts)
return (
<div>
<img style={{maxWidth: "150px"}} src={posts.img} alt="image"/>
</div>
);
};
Here is the page I go to from CardDetails
const Main = () => {
const dispatch = useDispatch()
const {posts, loading} = useSelector((state) => ({...state.posts}))
const user = JSON.parse(localStorage.getItem('profile'))
useEffect(() => {
dispatch(getPosts())
}, []);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [creator, setCreator] = useState("");
const [category, setCategory] = useState("");
const [img, setImg] = useState("");
const [tags, setTags] = useState(["tag1", "tag2"]);
const onSavePostClicked = () =>{
const newPost = {title, description, category, img}
dispatch(createPost(newPost))
setTitle('')
setDescription('')
setCreator('')
setCategory('')
setImg('')
}
return (
And here is the postsSlice
import {createAsyncThunk, createSlice} from "#reduxjs/toolkit";
import * as api from "../../../api/index";
import {fetchPost, fetchPostsCategory} from "../../../api/index";
export const getPosts = createAsyncThunk(
'posts/getPosts',
async (thunkAPI)=> {
const response = await api.fetchPosts()
return response.data.data
})
export const getPost = createAsyncThunk(
'posts/getPost',
async (id,thunkAPI)=> {
const response = await api.fetchPost(id)
return response.data.data
})
export const createPost = createAsyncThunk(
'posts/createPosts',
async (newPost,thunkAPI)=> {
const response = await api.createPost(newPost)
return response.data.data
})
export const deletePost = createAsyncThunk(
'posts/deletePost',
async (post,thunkAPI)=> {
const {_id} = post
const response = await api.deletePost(_id)
return response.data
})
export const updatePost = createAsyncThunk(
'posts/updatePost',
async (post,thunkAPI)=> {
const {_id} = post
const postEdited = {...post, published: true}
const response = await api.updatePost(_id, postEdited)
return response.data
})
export const getCategory = createAsyncThunk(
'posts/getCategory',
async (category,thunkAPI)=> {
const response = await api.fetchPostsCategory(category)
return response.data.data
})
const initialState = {
posts: [],
loading: false,
error:null
}
export const postsSlice = createSlice({
name: "posts",
initialState,
reducers:{
postAdded: {
reducer(state, action) {
state.posts.push(action.payload)
},
prepare(title, description, creator, category, img) {
return {
payload: {
title,
description,
creator,
category,
img
}
}
}
},
},
extraReducers: {
[createPost.pending]: (state, action) => {
state.loading = true;
},
[createPost.fulfilled]: (state, action) => {
state.loading = false;
state.posts.push(action.payload);
},
[createPost.rejected]: (state, action) => {
state.loading = false;
state.error = action.payload.message;
},
[getPosts.pending]: (state) => {
state.loading = true
},
[getPosts.fulfilled]: (state, action) => {
state.loading = false
state.posts = action.payload
},
[getPosts.rejected]: (state) => {
state.loading = false
},
[deletePost.fulfilled]: (state, action)=>{
if (!action.payload?._id) {
console.log('Delete could not complete')
console.log(action.payload)
return;
}
const { _id } = action.payload;
state.posts = state.posts.filter(post => post._id !== _id);
},
[updatePost.pending]: (state, action) => {
state.loading = true;
},
[updatePost.fulfilled]: (state, action) => {
const { _id } = action.payload;
state.loading = false;
const posts = state.posts.filter(post => post._id !== _id);
state.posts = [...posts, action.payload];
},
[updatePost.rejected]: (state, action) => {
state.loading = false;
state.error = action.payload.message;
},
[getCategory.pending]: (state) => {
state.loading = true
},
[getCategory.fulfilled]: (state, action) => {
state.loading = false
console.log()
state.posts = action.payload
},
[getCategory.rejected]: (state) => {
state.loading = false
},
[getPost.pending]: (state) => {
state.loading = true
},
[getPost.fulfilled]: (state, action) => {
state.loading = false
state.posts = action.payload
},
[getPost.rejected]: (state) => {
state.loading = false
},
},
})
export const {postAdded} = postsSlice.actions
export default postsSlice.reducer
When you are using useEffect with an empty array as deps [] it will only be trigger when you mount your component (and cleaned when you unmount the component)
Here is a summary of my sign-up modal component below:
const auth = useSelector((state: RootState) => state.auth);
const authError = useSelector((state: RootState) => state.error);
const { isLoading, isAuthenticated, user, error, isSuccessful } = auth;
const dispatch = useDispatch();
const { register, handleSubmit, errors } = useForm();
console.log("useForm err", errors);
const { addToast } = useToasts();
useEffect(() => {
dispatch(removeError()); // cleaning previous authError message when modal opens
}, [isSuccessful]);
const onSubmit = (data) => {
const { email, username, password } = data;
dispatch({
type: REGISTER_USER_REQUEST,
data: { email, username, password },
});
if (isSuccessful) {
addToast(`Thank you for signing up! Now you can log in`, {
appearance: "success",
autoDismiss: true,
});
onClose();
} else {
return;
}
};
auth reducer below:
const initialState: AuthState = {
isLoading: false,
isAuthenticated: false,
isSuccessful: false,
user: null,
error: null,
};
export default (state = initialState, action) => {
return produce(state, (draft) => {
switch (action.type) {
case REGISTER_USER_REQUEST: {
draft.isLoading = true;
draft.isSuccessful = false;
draft.error = null;
break;
}
case REGISTER_USER_SUCCESS: {
draft.isLoading = false;
draft.isSuccessful = true;
draft.error = null;
break;
}
case REGISTER_USER_FAILURE: {
draft.isLoading = false;
draft.isSuccessful = false;
draft.error = action.error;
break;
}
I noticed that once sign-up form filled and click the submit button (expecting isSuccessful changes into true) but isSuccessful state is still false and then I have to click the submit button once more then it changes into true as the state works asynchronously in React so I put it into the dependency of useEffect but no luck, still does not work.
Anything I have missed?
You should move this block into your effect:
useEffect(() => {
if (isSuccessful) {
addToast(`Thank you for signing up! Now you can log in`, {
appearance: "success",
autoDismiss: true,
});
onClose();
}
}, [isSuccessful])
You check the value of isSuccessful right after dispatching the action that will change the state at some point, that's too soon.