Bit of a weird one. In my component I am getting a "task" object from my "taskDetails" reducer. Then I have a useEffect function that checks if the task.title is not set, then to call the action to get the task.
However when the page loads I get an error cannot read property 'title' of null which is strange because when I look in my Redux dev tools, the task is there, all its data inside it, yet it can't be retrieved.
Here is the relevant code:
const taskId = useParams<{id: string}>().id;
const dispatch = useDispatch();
const history = useHistory();
const [deleting, setDeleting] = useState(false)
const taskDetails = useSelector((state: RootStateOrAny) => state.taskDetails);
const { loading, error, task } = taskDetails;
const successDelete = true;
const deleteTaskHandler = () => {
}
useEffect(() => {
if(!successDelete) {
history.push('/home/admin/tasks')
} else {
if(!task.title || task._id !== taskId) {
dispatch(getTaskDetails(taskId))
}
}
},[dispatch, task, taskId, history, successDelete])
REDUCER
export const taskDetailsReducer = (state = { task: {} }, action) => {
switch(action.type) {
case TASK_DETAILS_REQUEST:
return { loading: true }
case TASK_DETAILS_SUCCESS:
return { loading: false, success: true, task: action.payload }
case TASK_DETAILS_FAIL:
return { loading: false, error: action.payload }
case TASK_DETAILS_RESET:
return { task: {} }
default:
return state
}
}
ACTION
export const getTaskDetails = id => async (dispatch) => {
try {
dispatch({
type: TASK_DETAILS_REQUEST
})
const { data } = await axios.get(`http://localhost:5000/api/tasks/${id}`)
dispatch({
type: TASK_DETAILS_SUCCESS,
payload: data
})
} catch (error) {
dispatch({
type: TASK_DETAILS_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message
})
}
}
In my reducer in the TASK_DETAILS_REQUEST case, I just had loading: false.
I had failed to specify the original content of the state, I did this by adding ...state.
export const taskDetailsReducer = (state = { task: {} }, action) => {
switch(action.type) {
case TASK_DETAILS_REQUEST:
return { ...state, loading: true }
case TASK_DETAILS_SUCCESS:
return { loading: false, success: true, task: action.payload }
case TASK_DETAILS_FAIL:
return { loading: false, error: action.payload }
case TASK_DETAILS_RESET:
return { task: {} }
default:
return state
}
}
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.
Using react.js & firebase
The code below represents a simple button which increases/decreases +1/-1 whenever its clicked. It also updates one of the documents on the backend (using firebase). Everything seems to work fine on the surface but not on firebase. When you click on the button, it'll show +1 on the UI and console.log but not on firebase. In other words when plusCount state is at 0, it shows +1 on firebase and when plusCount state is at +1, it shows 0 on firebase. How can I fix this to make sure it shows the same number on the frontend and the backend? I also added the useFirestore hook component below, there may be a mistake that I'm unaware of in there somewhere.
Thank you for any help.
Button component:
import React, { useState } from 'react';
import { useFirestore } from "../../hooks/useFirestore"
export default function Testing({ doc }) {
const { updateDocument } = useFirestore('projects')
const [plusActive, setPlusActive] = useState(false)
const [plusCount, setPlusCount] = useState(0)
function p() {
setPlusActive(prevState => !prevState);
plusActive ? setPlusCount(plusCount - 1) : setPlusCount(plusCount + 1)
}
const handlePlus = (e) => {
e.preventDefault();
p();
updateDocument(doc.id, {
votes: plusCount
})
}
console.log(plusCount)
return (
<div>
<button onClick={handlePlus}>like | {plusCount}</button>
</div>
)
}
useFirestore hook component:
import { projectFirestore, timestamp } from "../firebase/config"
let initialState = {
document: null,
isPending: false,
error: null,
success: null,
}
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 }
case 'DELETED_DOCUMENT':
return { isPending: false, document: null, success: true, error: null }
case 'ERROR':
return { isPending: false, document: null, success: false, error: action.payload }
case "UPDATED_DOCUMENT":
return { isPending: false, document: action.payload, success: true, error: null }
default:
return state
}
}
export const useFirestore = (collection) => {
const [response, dispatch] = useReducer(firestoreReducer, initialState)
const [isCancelled, setIsCancelled] = useState(false)
// collection ref
const ref = projectFirestore.collection(collection)
// only dispatch if not cancelled
const dispatchIfNotCancelled = (action) => {
if (!isCancelled) {
dispatch(action)
}
}
// add a document
const addDocument = async (doc) => {
dispatch({ type: 'IS_PENDING' })
try {
const createdAt = timestamp.fromDate(new Date())
const addedDocument = await ref.add({ ...doc, createdAt })
dispatchIfNotCancelled({ type: 'ADDED_DOCUMENT', payload: addedDocument })
}
catch (err) {
dispatchIfNotCancelled({ type: 'ERROR', payload: err.message })
}
}
// delete a document
const deleteDocument = async (id) => {
dispatch({ type: 'IS_PENDING' })
try {
await ref.doc(id).delete()
dispatchIfNotCancelled({ type: 'DELETED_DOCUMENT' })
}
catch (err) {
dispatchIfNotCancelled({ type: 'ERROR', payload: 'could not delete' })
}
}
// update a document
const updateDocument = async (id, updates) => {
dispatch({ type: "IS_PENDING" })
try {
const updatedDocument = await ref.doc(id).update(updates)
dispatchIfNotCancelled({ type: "UPDATED_DOCUMENT", payload: updatedDocument })
return updatedDocument
}
catch (error) {
dispatchIfNotCancelled({ type: "ERROR", payload: error })
return null
}
}
useEffect(() => {
return () => setIsCancelled(true)
}, [])
return { addDocument, deleteDocument, updateDocument, response }
}```
For your use-case, you should useEffect() to listen the changes for plusCount. See code below:
useEffect(() => {
updateDocument('test', {
votes: plusCount
})
}, [plusCount]);
const handlePlus = (e) => {
e.preventDefault();
setPlusActive(prevState => !prevState);
plusActive ? setPlusCount(plusCount - 1) : setPlusCount(plusCount + 1)
}
Everytime you click the button it will listen to the changes of plusCount which then the updateDocument will also be triggered together with the updated state. See below screenshot for the result:
As you can see, the frontend and backend is now aligned.
You can find more information by checking out this documentation.
When I send request I got my data back in json and I can see it when console.log().
But when I try to change my state, it's not changing. Can you guys please help me to understand why? Don't judge too hard I am still learning. Thank you
Here is my code
export const login = createAsyncThunk(
'auth/login',
async (user) => {
try {
const response = await loginUser(user);
console.log(response.data) /// data present
return response.data
} catch (error) {
console.log(error)
}
}
)
export const authSlice = createSlice({
name: 'auth',
initialState: { user: {}, status: '', message: '', success: false, error: '' },
reducers: {
[login.pending]: (state, action) => (
state.status = 'loading'
),
[login.fulfilled]: (state, { payload }) => {
state.user = payload.user
state.status = 'success'
state.message = payload.message
state.success = true
state.error = ''
},
[login.rejected]: (state, { payload }) => {
state.user = payload
state.status = 'failed'
state.success = false
state.error = payload
}
}
})
You need use the extraReducers with builder, something like that :
reducers: {
//something, we don't care it
},
extraReducers: (builder) => {
builder.addCase(login.pending, (state) => {
state.status = what you want
});
builder.addCase(login.fulfilled, (state) => {
blabla
});
... and again and again
},
see https://redux-toolkit.js.org/api/createAsyncThunk (all is on the doc, no better source for RDK)
Here i have my component code for SignIng Up user and check for Error. At first error is null.
let error = useSelector((state) => state.authReducer.error);
const checkErrorLoading = () => {
console.log("If error found"); //At first it gives null, but on backend there is error
toast.error(error);
console.log(loading, error);
};
const handleSubmit = async (e) => {
if (isSignup) {
dispatch(signup(form, history));
checkErrorLoading();
} else {
dispatch(signin(form, history));
checkErrorLoading();
}
};
Now at my singupForm, i provide wrong input or wrong data. The backend gives me error that is completely fine.
ISSUE => But when i click on Login button. At first attempt it does not provide any error message. After second attempt it works fine, but not at first attempt. At first attempt it gives me Error value NULL while there is still an error
Here is my action.
export const signup = (formData, history) => async (dispatch) => {
try {
const res = await api.signUp(formData);
dispatch({ type: authConstants.AUTH_REQUEST });
if (res.status === 200) {
const { data } = res;
console.log(data);
dispatch({
type: authConstants.AUTH_SUCCESS,
payload: data,
});
}
console.log(res.status);
history.push("/");
} catch (error) {
console.log(error.response);
dispatch({
type: authConstants.AUTH_FAILURE,
payload: error.response.data.error,
});
}
};
and than reducer.
const initialState = {
authData: null,
error: null,
loading: false,
};
const authReducer = (state = initialState, action) => {
switch (action.type) {
case authConstants.AUTH_REQUEST:
return { ...state, loading: true, error: null };
case authConstants.AUTH_SUCCESS:
localStorage.setItem("profile", JSON.stringify({ ...action?.payload }));
return { ...state, authData: action?.data, loading: false, error: null };
case authConstants.AUTH_FAILURE:
console.log(action.payload);
return { ...state, loading: false, error: action.payload };
}
You should use useEffect instead of local function (checkErrorLoading ) for such cases:
useEffect(() => {
console.log("If error found");
toast.error(error);
console.log(loading, error);
},[error]);
Currently what you doing is creating local function that closures error variable, which initially is null + state is updated asynchronously, so you cannot execute function right after dispatching (even if variable wouldn't be closured, you will not have fresh state there)
Recently I've transitioned from using one optimistic action to adding two more to detect success/failure server responses.
With the optimistic approach I was able to just pass in my action the shorthand way and chain from the promise:
class Post extends Component {
onUpdateClick(props) {
this.props.updatePost(this.props.params.id, props)
.then(() => /* Action goes here */);
}
}
...
export default connect(mapStateToProps, { updatePost })(Post);
Now that I'm dispatching multiple actions and using mapDispatchToProps the action returns undefined.
Uncaught (in promise) TypeError: Cannot read property 'then' of undefined
What's going on here? Note that I'm using redux-promise.
function mapDispatchToProps(dispatch) {
return {
dispatch(updatePost(id, props))
.then(result => {
if (result.payload.response && result.payload.response.status !== 200) {
dispatch(updatePostFailure(result.payload.response.data));
} else {
dispatch(updatePostSuccess(result.payload.data));
}
});
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Post);
export function updatePost(id, props) {
const request = axios.put(`${ROOT_URL}/posts/${id}`, props);
return {
type: UPDATE_POST,
payload: request,
};
}
export function updatePostSuccess(activePost) {
return {
type: UPDATE_POST_SUCCESS,
payload: activePost,
};
}
export function updatePostFailure(error) {
return {
type: UPDATE_POST_FAILURE,
payload: error,
};
}
const initialState = {
activePost: { post: null, error: null, loading: false },
};
export default function(state = initialState, action) {
let error;
switch (action.type) {
case UPDATE_POST: {
return { ...state, activePost: { ...state.post, loading: true, error: null } };
}
case UPDATE_POST_SUCCESS: {
return { ...state, activePost: { post: action.payload, loading: false, error: null } };
}
case UPDATE_POST_FAILURE: {
error = action.payload || { message: action.payload.message };
return { ...state, activePost: { ...state.activePost, loading: false, error: error } };
}
}
}
The syntax of you mapDispatchToProps function seems to be incorrect.
It must returns an object containing methods as properties.
try to write something like that :
function mapDispatchToProps(dispatch) {
return {
updatePost() {
return dispatch(updatePost(id, props))
.then(result => {
if (result.payload.response && result.payload.response.status !== 200) {
return dispatch(updatePostFailure(result.payload.response.data));
}
return dispatch(updatePostSuccess(result.payload.data));
});
}
}
}