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.
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.
So this is the scenario / premises:
In order to populate a chat queue in real time I need to open a connection to a websocket, send a message and then set the data to a websocket store. This store will basically manage all the websocket state.
Before populating the chat queue there's two parameters I need: a shiftId coming from one http API request and a connectionId coming from the websocket. Using those two parameters I finally can subscribe to a third http API and start receiving messages to populate the chat queue.
The problem is that due to the async behaviour of the websocket (or that's what I think, please feel to correct me if I'm wrong) I always get an empty "connectionId" when trying to make the put to that "subscription" API. I have tried with async/await and promises but nothing seems to work. I'm pretty new to async/await and websockets with Vuex so pretty sure I'm doing something wrong.
This is the user vuex module where I do all the login/token operations and dispatch a "updateEventsSubscription" action from the shift vuex module. In order for the "updateEventsSubscription" action to work I need to get the response from the "processWebsocket" action (to get the connectionId parameter) and from the "startShift" action (to get the shiftId parameter) coming from the shifts vuex module:
import UserService from '#/services/UserService.js'
import TokenService from '#/services/TokenService.js'
import router from '#/router'
export const namespaced = true
export const state = {
accessToken: '',
errorMessage: '',
errorState: false,
userEmail: localStorage.getItem('userEmail'),
userPassword: localStorage.getItem('userPassword'),
}
export const mutations = {
SET_TOKEN(state, accessToken) {
state.accessToken = accessToken
TokenService.saveToken(accessToken)
},
SET_USER(state, authUserJson) {
state.userEmail = authUserJson.email
state.userPassword = authUserJson.password
localStorage.setItem('userPassword', authUserJson.password)
localStorage.setItem('userEmail', authUserJson.email)
},
SET_ERROR(state, error) {
state.errorState = true
state.errorMessage = error.data.error_description
},
CLOSE_NOTIFICATION(state, newErrorState) {
state.errorState = newErrorState
},
}
export const actions = {
signIn({ commit, dispatch, rootState }, authUserJson) {
return UserService.authUser(authUserJson)
.then((result) => {
commit('SET_USER', authUserJson)
commit('SET_TOKEN', result.data.access_token)
dispatch('token/decodeToken', result.data.access_token, {
root: true,
})
dispatch(
'shifts/updateEventsSubscription',
rootState.token.agentId,
{
root: true,
}
)
router.push('/support')
})
.catch((error) => {
console.log(error)
if (error.response.status === 400) {
commit('SET_TOKEN', null)
commit('SET_USER', {})
commit('SET_ERROR', error.response)
} else {
console.log(error.response)
}
})
},
signOut({ commit }) {
commit('SET_TOKEN', null)
commit('SET_USER', {})
localStorage.removeItem('userPassword')
localStorage.removeItem('userEmail')
TokenService.removeToken()
router.push('/')
},
closeNotification({ commit }, newErrorState) {
commit('CLOSE_NOTIFICATION', newErrorState)
},
}
export const getters = {
getToken: (state) => {
return state.accessToken
},
errorState: (state) => {
return state.errorState
},
errorMessage: (state) => {
return state.errorMessage
},
isAuthenticated: (state) => {
return state.accessToken
},
userEmail: (state) => {
return state.userEmail
},
userPassword: (state) => {
return state.userPassword
},
}
This is websocket store: I pass the connectionId to the state in order to be able to use it in another vuex action to subscribe for new chats:
export const namespaced = true
export const state = {
connected: false,
error: null,
connectionId: '',
statusCode: '',
incomingChatInfo: [],
remoteMessage: [],
messageType: '',
ws: null,
}
export const actions = {
processWebsocket({ commit }) {
const v = this
this.ws = new WebSocket('mywebsocket')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
},
}
export const mutations = {
SET_REMOTE_DATA(state, remoteData) {
const wsData = JSON.parse(remoteData.data)
if (wsData.connectionId) {
state.connectionId = wsData.connectionId
console.log(`Retrieving Connection ID ${state.connectionId}`)
} else {
console.log(`We got chats !!`)
state.messageType = wsData.type
state.incomingChatInfo = wsData.documents
}
},
SET_CONNECTION(state, message) {
if (message == 'open') {
state.connected = true
} else state.connected = false
},
SET_ERROR(state, error) {
state.error = error
},
}
And finally this is the shift store (where the problem is), as you can see I have a startShift action (everything works fine with it) and then the "updateEventsSubscription" where I'm trying to wait for the response from the "startShift" action and the "processWebsocket" action. Debugging the app I realize that everything works fine with the startShift action but the websocket action sends the response after the "updateEventsSubscription" needs it causing an error when I try to make a put to that API (because it needs the connectionId parameter coming from the state of the websocket).
import ShiftService from '#/services/ShiftService.js'
export const namespaced = true
export const state = {
connectionId: '',
shiftId: '',
agentShiftInfo: '{}',
}
export const actions = {
startShift({ commit }, agentId) {
return ShiftService.startShift(agentId)
.then((response) => {
if (response.status === 200) {
commit('START_SHIFT', response.data.aggregateId)
}
})
.catch((error) => {
console.log(error)
if (error.response.status === 401) {
console.log('Error in Response')
}
})
},
async updateEventsSubscription({ dispatch, commit, rootState }, agentId) {
await dispatch('startShift', agentId)
const shiftId = state.shiftId
await dispatch('websocket/processWebsocket', null, { root: true })
let agentShiftInfo = {
aggregateId: state.shiftId,
connectionId: rootState.websocket.connectionId,
}
console.log(agentShiftInfo)
return ShiftService.updateEventsSubscription(shiftId, agentShiftInfo)
.then((response) => {
commit('UPDATE_EVENTS_SUBSCRIPTION', response.data)
})
.catch((error) => {
if (error.response.status === 401) {
console.log('Error in Response')
}
})
},
}
export const mutations = {
START_SHIFT(state, shiftId) {
state.shiftId = shiftId
console.log(`Retrieving Shift ID: ${state.shiftId}`)
},
UPDATE_EVENTS_SUBSCRIPTION(state, agentShiftInfo) {
state.agentShiftInfo = agentShiftInfo
},
}
You should convert your WebSocket action into a promise that resolves when WebSocket is connected.:
export const actions = {
processWebsocket({ commit }) {
return new Promise(resolve=> {
const v = this
this.ws = new WebSocket('mywebsocket')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
resolve();
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
});
},
}
So I realized that I have to resolve the promise on the this.ws.message instead. By doing that all my data is populated accordingly, there's still sync issues (I can't feed the websocket state at the moment because due to its async behaviour the state is not there yet when other components try to use it via: rootGetters.websocket.incomingChats for example) but I guess that's part of another question. Here's the final version of the module action:
export const actions = {
processWebsocket({ commit }) {
return new Promise((resolve) => {
const v = this
this.ws = new WebSocket('wss://ws.rubiko.io')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
resolve(event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
})
},
}
Anyways, thanks #Eldar you were in the right path.
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)
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
}
}
I have been working on a project where I am trying to update my selected data but Axios didn't Update it even after giving a success msg.
User Response it returns from axios:-
completed: true
date: "2021-02-28"
mupp_path: "PATH 1 - LIVING YOUR WHY - Build/Apply/Inspire/Spread (BAIS) – Finding & Achieving Meaning and Purpose in work and life"
project_name: "Design and Test the Training Content for the i-Infinity 3 verticals "
selected: true
task_id: 14
task_name: "This is adding a new task to chekc full inbox functionality "
task_type: "THIS_WEEK"
Actions.js
export const taskTodayUnselect = (id) => async (dispatch) => {
try {
dispatch({ type: types.UNSELECTED_TASK_TODAY_REQUEST });
const { data } = await axios.put(
selectTaskForToday,
{
task_id: id,
selected: false,
},
{
headers: {
Authorization: `JWT ${token}`,
},
}
);
if (data) {
return dispatch({ type: types.UNSELECTED_TASK_TODAY_SUCCESS, payload: data });
}
} catch (error) {
return dispatch({ type: types.UNSELECTED_TASK_TODAY_FAILURE, payload: error });
}
};
thisweek.js
export default function ThisWeek() {
const unselectTaskTodayAPI = (id) => {
dispatch(taskTodayUnselect(id)).then((response) => {
let result = response.payload;
console.log(result);
if (result.success === 'true') {
notifySuccess(result.message);
fetchTaskData(categoryID);
}
});
};
const selectTask = (item) => {
if (item.selected) {
unselectTaskTodayAPI(item);
console.log('unselect');
} else {
selectTaskTodayAPI(item.task_id);
}
};
return (
<TaskDataComponent
item={item}
key={item.task_id}
label="This Week"
selectTask={selectTask}
/>
);
Don't Worry about the TaskDataComponent , it only handle the onClick function which invoke the selectedTask function