usersSlice.js
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import { API } from "../axios/index";
export const signUp = ...
export const logOut = ...
export const signIn = createAsyncThunk("users/signin", async (params) => {
try {
const { loginData, history } = params;
const { data } = await API.post("users/signin", loginData);
history.push("/");
return data;
} catch (error) {
console.log(error);
}
});
const initialState = {
usersInfo: {},
status: "idle",
error: null,
existEmail: false,
};
const usersSlice = createSlice({
name: "users",
initialState,
reducers: {
handleExistEmail: (state, action) => {
state.existEmail = action.payload;
},
},
extraReducers: {
...
[signIn.fulfilled]: (state, action) => {
console.log("here is your data : ", action.payload);
state.status = "succeeded";
if (action.payload) {
localStorage.setItem("user", JSON.stringify(action.payload));
}
},
},
});
export default usersSlice.reducer;
export const { handleExistEmail } = usersSlice.actions;
userRouter.js
const isPasswordCorrent = await bcrypt.compare(password, user.password);
if (!isPasswordCorrent) {
return res
.status(404)
.json({ message: "Password dont match" });
}
Hi all.When password and re-passwordn dont match i want to backend send me status(404) and json({ message: "Password dont match" }) values and i want to catch these values in [signIn.fulfilled] but action.payload send me undefined.But if i do return res.json({ message: "Password dont match" }) instead of return.status(404).json({message: "Password dont match"}) this time i cant catch json({message: "Password dont match"}) from [signIn.fulfilled].Why i have to delete .status(404) part to dont get undefined ?
This is how the createAsyncThunk works. This wrapper itself is a try/catch block, so doesn't make sense to use in this action creator function. If a promise is rejected in this creator function body, then your action returns a rejected sub-action in the store. So you have to listen to this action in the reducer. Or if you really want to use a try/catch block, then in the catch block throw the error. A little example usage:
export const exampleAsyncAction = createAsyncThunk(
ACTION_TYPE,
async (parameter) => {
const result = await apicall(parameter);
return result.doSomeLogic();
}
);
const reducer = createReducer(
...,
[exampleAsyncAction.pending]: (state) => {
state.loading = true;
state.error = null;
},
[exampleAsyncAction.fulfilled]: (state, {payload}) => {
state.result = payload;
state.loading = false;
},
[exampleAsyncAction.rejected]: (state, {error}) => {
state.error = error;
state.loading = false;
},
)
Related
I'm trying to get the id of the generated firebase document, and I'm using addDoc to create a new doc.
I'm generating a new document on button click and that button calls the initializeCodeEditor function.
Anyone please help me with this!
Button Code:
import { useNavigate } from "react-router-dom"
import { useAuthContext } from "../../hooks/useAuthContext"
import { useFirestore } from "../../hooks/useFirestore"
import Button from "./Button"
const StartCodingButton = ({ document, setIsOpen }) => {
const { user } = useAuthContext()
const { addDocument, response } = useFirestore("solutions")
const navigate = useNavigate()
const initializeCodeEditor = async () => {
await addDocument({
...document,
author: user.name,
userID: user.uid,
})
if (!response.error) {
console.log(response.document) // null
const id = response?.document?.id; // undefined
navigate(`/solution/${id}`, { state: true })
}
}
return (
<Button
className="font-medium"
variant="primary"
size="medium"
onClick={initializeCodeEditor}
loading={response.isPending}
>
Start coding online
</Button>
)
}
export default StartCodingButton
addDocument code
import { useReducer } from "react"
import {
addDoc,
collection,
doc,
Timestamp,
} from "firebase/firestore"
import { db } from "../firebase/config"
import { firestoreReducer } from "../reducers/firestoreReducer"
const initialState = {
document: null,
isPending: false,
error: null,
success: null,
}
export const useFirestore = (c) => {
const [response, dispatch] = useReducer(firestoreReducer, initialState)
// add a document
const addDocument = async (doc) => {
dispatch({ type: "IS_PENDING" })
try {
const createdAt = Timestamp.now()
const addedDocument = await addDoc(collection(db, c), {
...doc,
createdAt,
})
dispatch({ type: "ADDED_DOCUMENT", payload: addedDocument })
} catch (error) {
dispatch({ type: "ERROR", payload: error.message })
}
}
return {
addDocument,
response,
}
}
firestoreReducer
export const firestoreReducer = (state, action) => {
switch (action.type) {
case "IS_PENDING":
return { isPending: true, document: null, success: false, error: null }
case "ADDED_DOCUMENT":
return { isPending: false, document: action.payload, success: true, error: null }
}
throw Error("Unknown action: " + action.type)
}
I have recreated this issue and found out this is happening because the response object in the useFirestore hook is not being updated until the next render cycle.
In order to get the updated response object, you can use the useEffect hook to trigger an update to the component whenever the response object changes.
So I recommend you to call initializeCodeEditor and make your app wait until response object change I used useEffect here
const initializeCodeEditor = async () => {
await addDocument({
author: user.name,
userID: user.uid,
})
//skip following if block it's just for understanding
if (!response.error) {
console.log(response.document) // will obviously be null here as at first it is set null
const id = response?.document?.id; // will obviously be undefined
navigate(`/solution/${id}`, { state: true })
}
}
useEffect(() => {
if (!response.error) {
setId(response?.document?.id);
console.log("From App.js useEffect: " + response?.document?.id); // getting the document id here too
}
}, [response])
//and in firestoreReducer
case "ADDED_DOCUMENT":{
console.log("from Reducer: " + action.payload.id); //getting the document id here
return { isPending: false, document: action.payload, success: true, error: null }
}
OR you can use callback also without introducing useEffect like this:
const initializeCodeEditor = async () => {
await addDocument({
author: user.name,
userID: user.uid,
}, (response) => {
console.log("From App: " + response?.document?.id); //Will run as callback
if (!response.error) {
setId(response?.document?.id);
}
})
}
This way, the callback function will be called after the addDocument function has completed and the response object will have the updated document id.
...
extraReducers: {
[adminLogin.fulfilled]: (state, { payload }) => {
const { data, navigate, enqueueSnackbar } = payload;
enqueueSnackbar(
`Login successful.`,
{ variant: 'success' }
);
localStorage.setItem('auth', JSON.stringify(payload));
state.auth = data;
state.loading = false;
state.success = true;
navigate('/dashboard');
},
[adminLogin.rejected]: (state, { payload }) => {
const { enqueueSnackbar } = payload;
state.loading = false;
state.error = payload;
enqueueSnackbar(`Login failed.`, { variant: 'error' });
}
}
Hİ all. As I mentioned in the title notistack doesnt work in RTK extraReducer or try-catch blocks. When i check enqueueSnackbar in console.log() i cant get it properly but when i try to invoke it, it doesn't work.
I am trying to add authentication to a react website using firebase
I currently have this createasyncthunk
export const signin = createAsyncThunk<
// Return type of the payload creator
User | void,
// First argument to the payload creator
userData,
// Types for ThunkAPI
{
rejectValue: errorInterface;
}
>('authentication/signin', (user, thunkApi) => {
const { email, password } = user;
signInWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
return user;
})
.catch((error) => {
console.log(error.message, error.code)
const errorMessage: errorInterface = { message: error.code };
return thunkApi.rejectWithValue(errorMessage);
});
});
the user type is imported from fire base
the userData is the following interface to represent credentials used to log in
interface userData {
email: string;
password: string;
}
the error interface is the following
interface errorInterface {
message: string;
}
the authentication slice is this
export const authSlice = createSlice({
name: 'authentication',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(signin.pending, (state) => {
state.status = 'loading';
state.isAuth = false;
})
.addCase(signin.fulfilled, (state, action) => {
state.status = 'idle';
state.isAuth = true;
if (action.payload) {
state.user = action.payload;
}
})
.addCase(signin.rejected, (state, action) => {
state.status = 'failed';
state.isAuth = false;
if (action.payload) {
state.message = action.payload.message;
}
})
},
});
The problem is that signin.rejected is never trigered. Even when the catch block runs, rejectwithvalue is not updating the payload. I know the catch block is running because I can see the error in the console. Please help me out, thanks in advance
Just to make it clear router uses the code below and my messages.js are inside api folder....
router.use("/messages", require("./messages"));
so my api call is correct.
Backend for posting the message.... I know conversationId will be null if no conversation exists but... I am trying to send message where conversation exists already and still I am getting cannot read the conversationId of undefined....
// expects {recipientId, text, conversationId } in body
// (conversationId will be null if no conversation exists yet)
router.post("/", async (req, res, next) => {
try {
if (!req.user) {
return res.sendStatus(401);
}
const senderId = req.user.id;
const { recipientId, text, conversationId, sender } = req.body;
// if we already know conversation id, we can save time and just add it to message and return
if (conversationId) {
const message = await Message.create({ senderId, text, conversationId });
return res.json({ message, sender });
}
// if we don't have conversation id, find a conversation to make sure it doesn't already exist
let conversation = await Conversation.findConversation(
senderId,
recipientId
);
if (!conversation) {
// create conversation
conversation = await Conversation.create({
user1Id: senderId,
user2Id: recipientId,
});
if (onlineUsers.includes(sender.id)) {
sender.online = true;
}
}
const message = await Message.create({
senderId,
text,
conversationId: conversation.id,
});
res.json({ message, sender });
} catch (error) {
next(error);
}
});
module.exports = router;
This is the frontend that posts the data to the backend....
const saveMessage = async (body) => {
const { data } = await axios.post("/api/messages", body);
return data;
};
Okay so here is detail information on how I am dispatching it.
class Input extends Component {
constructor(props) {
super(props);
this.state = {
text: "",
};
}
handleChange = (event) => {
this.setState({
text: event.target.value,
});
};
handleSubmit = async (event) => {
event.preventDefault();
// add sender user info if posting to a brand new convo,
// so that the other user will have access to username, profile pic, etc.
const reqBody = {
text: event.target.text.value,
recipientId: this.props.otherUser.id,
conversationId: this.props.conversationId,
sender: this.props.conversationId ? null : this.props.user,
};
await this.props.postMessage(reqBody);
this.setState({
text: "",
});
};
render() {
const { classes } = this.props;
return (
<form className={classes.root} onSubmit={this.handleSubmit}>
<FormControl fullWidth hiddenLabel>
<FilledInput
classes={{ root: classes.input }}
disableUnderline
placeholder="Type something..."
value={this.state.text}
name="text"
onChange={this.handleChange}
/>
</FormControl>
</form>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(Input));
const mapDispatchToProps = (dispatch) => {
return {
postMessage: (message) => {
dispatch(postMessage(message));
},
};
};
// message format to send: {recipientId, text, conversationId}
// conversationId will be set to null if its a brand new conversation
export const postMessage = (body) => (dispatch) => {
try {
const data = saveMessage(body);
if (!body.conversationId) {
dispatch(addConversation(body.recipientId, data.message));
} else {
dispatch(setNewMessage(data.message));
}
sendMessage(data, body);
} catch (error) {
console.error(error);
}
};
So I have attached what I want to do here now....
But I am still getting the problem....
// CONVERSATIONS THUNK CREATORS, this is how I am getting data from the backend
export const fetchConversations = () => async (dispatch) => {
try {
const { data } = await axios.get("/api/conversations");
dispatch(gotConversations(data));
} catch (error) {
console.error(error);
}
};
export const setNewMessage = (message, sender) => {
return {
type: SET_MESSAGE,
payload: { message, sender: sender || null },
};
};
// REDUCER
const reducer = (state = [], action) => {
switch (action.type) {
case GET_CONVERSATIONS:
return action.conversations;
case SET_MESSAGE:
return addMessageToStore(state, action.payload);
case ADD_CONVERSATION:
return addNewConvoToStore(
state,
action.payload.recipientId,
action.payload.newMessage
);
default:
return state;
}
};
I am getting an error saying Cannot read property 'conversationId' of undefined while using a reducer function... Should I give the setintial value of the message to empty?
export const addMessageToStore = (state, payload) => {
const { message, sender } = payload;
// if sender isn't null, that means the message needs to be put in a brand new convo
if (sender !== null) {
const newConvo = {
id: message.conversationId,
otherUser: sender,
messages: [message],
};
newConvo.latestMessageText = message.text;
return [newConvo, ...state];
}
return state.map((convo) => {
if (convo.id === message.conversationId) {
const convoCopy = { ...convo };
convoCopy.messages.push(message);
convoCopy.latestMessageText = message.text;
return convoCopy;
} else {
return convo;
}
});
};
Issue
The saveMessage function is declared async
const saveMessage = async (body) => {
const { data } = await axios.post("/api/messages", body);
return data;
};
but the postMessage action creator isn't async so it doesn't wait for the implicitly returned Promise to resolve before continuing on and dispatching to the store. This means that data.message is undefined since a Promise object doesn't have this as a property.
export const postMessage = (body) => (dispatch) => {
try {
const data = saveMessage(body); // <-- no waiting
if (!body.conversationId) {
dispatch(addConversation(body.recipientId, data.message));
} else {
dispatch(setNewMessage(data.message));
}
sendMessage(data, body);
} catch (error) {
console.error(error);
}
};
Solution
Declare postMessage async as well and await the data response value.
export const postMessage = (body) => async (dispatch) => {
try {
const data = await saveMessage(body); // <-- await response
if (!body.conversationId) {
dispatch(addConversation(body.recipientId, data.message));
} else {
dispatch(setNewMessage(data.message));
}
sendMessage(data, body);
} catch (error) {
console.error(error);
}
};
can somebody explain why this code dispatching 'actions.loginSuccess' when i get 401 error from server ?
isn't it should go to 'catch' part of axios request ?
Before i did it without redux toolkit features
const login = ({username, password}) => async dispatch => {
await axios.post(`${API_URL}/token/`, {username, password})
.then(response => {
dispatch(actions.loginSuccess({ client_id: response?.data.client_id }))
history.push('/')
})
.catch(e => {
dispatch(actions.loginError({ error: String(e) }))
})
}
//actions.js
const login = createAction('#USER/login')
const loginSuccess = createAction('#USER/login-success')
const loginError = createAction('#USER/login-error')
export const actions = {
login,
loginSuccess,
loginError
}
//reducers.js
export const userReducer = createReducer(initialState, builder => {
builder.addCase(actions.login, draft => {
draft.loading = true
})
builder.addCase(actions.loginSuccess, (draft, action) => {
draft.loading = false
draft.isLoggedIn = true
draft.data = { ...draft.data, client_id : action.client_id}
})
builder.addCase(actions.loginError, (draft, action) => {
draft.loading = false
draft.error = action.payload.error
draft.isLoggedIn = false
draft.isSignedup = false
})
}
can somebody explain why this code dispatching 'actions.loginSuccess'
when i get 401 error from server ? isn't it should go to 'catch' part
of axios request ?
// there's a difference beetween HTTP Status Code and Server Response Body Code.
// if HTTP status code is not 200, it should dispatched loginError()
// if HTTP status code is 200, and theres a response body JSON
// e.g
const resp = {
statusCode: 401,
message: 'unauthorized',
}
// You must make if conditions to handle that error code
Here's redux-toolkit version of your code to handle either HTTP status code 401, or body response code
// import axios & history
import { createSlice } from '#reduxjs/toolkit';
const initialState = {
data: {},
loading: false,
isLoggedIn: false,
isSignedup: false,
};
// Reducers
const userSlice = createSlice({
name: '#USER',
initialState: initialState,
reducers: {
loginStart(state) {
state.loading = true;
},
loginSuccess(state, action) {
state.data = {
...state.data,
client_id: action.payload.client_id
};
state.loading = false;
state.isLoggedIn = true;
},
loginError(state, action) {
state.loading = false;
state.error = action.payload.error;
state.isLoggedIn = false;
state.isSignedup = false;
},
},
});
// actions
export const { loginStart, loginSuccess, loginError } = userSlice.actions;
export default userSlice.reducer;
export const login = ({ username, password }) => async (dispatch) => {
dispatch(loginStart());
try {
const response = await axios.post(`${API_URL}/token/`, {
username,
password,
});
if(response && response.statusCode !== 200){
return dispatch(loginError({ error: response.message }));
}
dispatch(loginSuccess({ client_id: response?.data.client_id }));
history.push('/');
} catch (e) {
dispatch(loginError({ error: String(e) }));
}
};
don't forget to add userSlice into configureStore()
const reducer = {
"#USER": userReducers, //got from export default userSlice.reducer
};
export default configureStore({
reducer,
middleware,
devTools: process.env.NODE_ENV !== 'production',
});