Action running twice in react-redux - javascript

I am building a todo-app using React. I am dispatching an action(COMPLETED_TASK) to reducer after request/response from firestore DB. Since I'm also using redux-logger I noticed that this action is running twice.
Here's the code for Action:
export const completedTask = (data, completedBool) => async (
dispatch,
getState
) => {
try {
const formObj = cloneDeep(data);
const onlyDate = formObj.taskTime;
const getId = formObj._id;
const getDate = cleanDate(onlyDate, 'DD');
const getMonth = cleanDate(onlyDate, 'MM');
const getYear = cleanDate(onlyDate, 'YYYY');
const { uid } = getState().firebase.auth;
const todoUpdateString = `todoListByDate.${getDate}.${getId}.completed`;
await db.doc(`todos-col/${uid}&${getMonth}${getYear}`).update({
[todoUpdateString]: completedBool,
});
formObj.completed = completedBool;
dispatch({
type: todoTypes.COMPLETED_TASK,
payload: {
todosByMonthData: {
[`${getMonth}-${getYear}`]: {
[getDate]: { [getId]: formObj },
},
},
selectedDate: {
selectedDay: getDate,
selectedMonth: getMonth,
selectedYear: getYear,
},
},
});
} catch (err) {
console.log('Error!!', err);
}
};
In the screenshot, at 1, the action is dispatched to the reducer(it logs payload data from reducer below), and then again the action with type: "COMPLETED_TASK" is dispatched but it is not received by the reducer.
Why is this happening? Can anybody help?

Related

How to Revalidate when data in fetcher is change using swr

I want to revalidate the date when on change. This is what I tried:
const fetchData = async() => {
const {
data
} = await axios.get(`/api/admin/orders${criteria}`, {
params: {
name: debouncedValue,
},
});
return data;
};
const {
data,
error: err,
mutate,
} = useSWR(`/api/admin/orders${criteria}/${nameSearch}`, fetchData);
const handleChange = (e: React.ChangeEvent < HTMLInputElement > ) => {
mutate();
setNameSearch(e.target.value);
};
But the data is no revalidate, I have to use the onTabFocus revalidation.

React & Typescript - custom hook to pull status updates resets back to initial status

Situation:
Im trying to write a custom hook that allows me to fetch data and launches a setInterval which polls status updates every two seconds. Once all data is processed, the interval is cleared to stop polling updates.
Problem:
My problem is that the function passed to setInterval only has the initial state (empty array) although it has already been updated. Either it is resetting back to the initial state or the function has an old reference.
Code:
Link to Codesandbox: https://codesandbox.io/s/pollingstate-9wziw7?file=/src/Hooks.tsx
Hook:
export type Invoice = {id: number, status: string};
export async function executeRequest<T>(callback: () => T, afterRequest: (response: {error: string | null, data: T}) => void) {
const response = callback();
afterRequest({error: null, data: response});
}
export function useInvoiceProcessing(formData: Invoice[]): [result: Invoice[], executeRequest: () => Promise<void>] {
const timer: MutableRefObject<NodeJS.Timer | undefined> = useRef();
const [result, setResult] = useState<Invoice[]>(formData);
// Mock what the API would return
const firstRequestResult: Invoice[] = [
{id: 1, status: "Success"},
{id: 2, status: "Pending"}, // This one needs to be polled again
{id: 3, status: "Failed"}
];
const firstPoll: Invoice = {id: 2, status: "Pending"};
const secondPoll: Invoice = {id: 2, status: "Success"};
// The function that triggers when the user clicks on "Execute Request"
async function handleFirstRequest() {
await executeRequest(() => firstRequestResult, response => {
if (!response.error) {
setResult(response.data)
if (response.data.some(invoice => invoice.status === "Pending")) {
// Initialize the timer to poll every 2 seconds
timer.current = setInterval(() => updateStatus(), 2000);
}
} else {
// setError
}
})
}
let isFirstPoll = true; // Helper variable to simulate a first poll
async function updateStatus() {
// Result has the initial formData values (NotUploaded) but should already have the values from the first request
console.log(result);
const newResult = [...result];
let index = 0;
for (const invoice of newResult) {
if (invoice.status === "Pending") {
await executeRequest(() => isFirstPoll ? firstPoll : secondPoll, response => {
if (!response.error) {
newResult[index] = response.data;
} else {
// Handle error
}
});
}
index++;
}
setResult(newResult);
isFirstPoll = false;
const areInvoicesPending = newResult.some(invoice => invoice.status === "Pending");
if (!areInvoicesPending) {
console.log("Manual clear")
clearInterval(timer.current);
}
};
useEffect(() => {
return () => {
console.log("Unmount clear")
clearInterval(timer.current);
}
}, [])
return [result, handleFirstRequest];
Usage:
const [result, executeRequest] = useInvoiceProcessing([
{ id: 1, status: "NotUploaded" },
{ id: 2, status: "NotUploaded" },
{ id: 3, status: "NotUploaded" }
]);
async function handleRequest() {
console.log("Start request");
executeRequest();
}
return (
<div className="App">
<button onClick={handleRequest}>Execute Request</button>
<p>
{result.map((invoice) => (
<Fragment key={invoice.id}>
{invoice.status}
<br />
</Fragment>
))}
</p>
</div>
);
EDIT1
I have one possible solution. This post helped me in the right direction: React hooks functions have old version of a state var
The closure that updateStatus uses is outdated. To solve that, I saved updateStatus in a useRef (updateStatus also needs useCallback). Although not necessary, I had to store result in a useRef as well but I'm not sure yet why.
const updateStatusRef = useRef(updateStatus);
useEffect(()=>{
updateStatusRef.current = updateStatus;
}, [updateStatus]);
Here's a working example: https://codesandbox.io/s/pollingstate-forked-njw4ct?file=/src/Hooks.tsx

Why can't I access data after fetching?

I'm trying to keep session stayed logged in after refreshing the browser. The user data that is being fetched is not rendering after being fetched. The console is saying "Cannot read properties of undefined (reading 'user'). This is my code for the login/sign up page.
The data I'm trying to access is in the picture below:
(Auth.js)
const Auth = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const [isSignup, setIsSignup] = useState(false);
const [inputs, setInputs] = useState({
name: "",
username: "",
email: "",
password: ""
})
const handleChange = (e) => {
setInputs(prevState => {
return {
...prevState,
[e.target.name]: e.target.value
}
})
}
const sendRequest = async (type = '') => {
const res = await axios.post(`/user/${type}`, {
name: inputs.name,
email: inputs.email,
username: inputs.username,
password: inputs.password,
}).catch(error => console.log(error))
const data = await res.data;
console.log(data)
return data;
}
const handleSubmit = (e) => {
e.preventDefault()
console.log(inputs)
if (isSignup) {
sendRequest("signup")
.then((data) => {
dispatch(authActions.login());
localStorage.setItem('userId', data.user._id);
navigate("/posts");
});
} else {
sendRequest("login")
.then((data) => {
dispatch(authActions.login());
localStorage.setItem('userId', data.user._id);
navigate("/posts");
});
}
}
Redux store file
const authSlice = createSlice({
name: "auth",
initialState: { isLoggedIn: false },
reducers: {
login(state) {
state.isLoggedIn = true
},
logout(state) {
state.isLoggedIn = false
}
}
})
export const authActions = authSlice.actions
export const store = configureStore({
reducer: authSlice.reducer
})
Chaining promises using .then() passes the resolved value from one to the next. With this code...
sendRequest("...")
.then(() => dispatch(authActions.login()))
.then(() => navigate("/posts"))
.then(data => localStorage.setItem('token', data.user))
You're passing the returned / resolved value from navigate("/posts") to the next .then() callback. The navigate() function returns void therefore data will be undefined.
Also, your redux action doesn't return the user so you can't chain from that either.
To access the user data, you need to return it from sendRequest()...
const sendRequest = async (type = "") => {
try {
const { data } = await axios.post(`/user/${type}`, { ...inputs });
console.log("sendRequest", type, data);
return data;
} catch (err) {
console.error("sendRequest", type, err.toJSON());
throw new Error(`sendRequest(${type}) failed`);
}
};
After that, all you really need is this...
sendRequest("...")
.then((data) => {
dispatch(authActions.login());
localStorage.setItem('userId', data.user._id);
navigate("/posts");
});
Since you're using redux, I would highly recommend moving the localStorage part out of your component and into your store as a side-effect.

Cannot update product quantity stored in MongoDB, using Node.js and React.js

I am building a warehouse management site. I have the data stored in MongoDB. I want the product quantity to be updated on a button click and show it on the UI. But after updating the quantity, when I console log it I get undefined.
This is the function to decrease the quantity on click and update it on DB:
const ProductDetail = () => {
const { productId } = useParams();
const [product] = useProductDetail(productId);
const { _id, name, img, description, supplier, price, quantity } = product;
const [updatedQuantity, setUpdatedQuantity] = useState(quantity);
const handleDecreaseQuantity = (id) => {
const { quantity, ...rest } = product;
const previousQuantity = quantity;
setUpdatedQuantity(previousQuantity - 1)
const updatedProduct = { updatedQuantity, ...rest }
fetch(`http://localhost:5000/updateProduct/${id}`, {
method: 'PUT',
headers: {
"content-type": "application/json"
},
body: JSON.stringify(updatedProduct)
})
.then(res => res.json())
.then(data => {
console.log(data)
})
console.log(updatedQuantity);
}
This is product update API:
app.put('/updateProduct/:id', async (req, res) => {
const updatedProduct = req.body;
const { updatedQuantity } = updatedProduct;
const id = req.params.id;
const query = { _id: ObjectId(id) };
const options = { upsert: true };
const updateProduct = {
$set: {
quantity: updatedQuantity
},
};
const result = await productCollection.updateOne(query, updateProduct, options);
console.log(result);
res.send(result);
})
setState actions are asynchronous and are batched for performance gains. This is explained in the documentation of setState.
setState() does not immediately mutate this.state but creates a
pending state transition. Accessing this.state after calling this
method can potentially return the existing value. There is no
guarantee of synchronous operation of calls to setState and calls may
be batched for performance gains.
This part of the code might not be executing as you think.
const previousQuantity = quantity;
setUpdatedQuantity(previousQuantity - 1);
const updatedProduct = { updatedQuantity, ...rest };
Update your product quantity and send it to API. By the action is completed, your state also might be updated.
const previousQuantity = quantity;
const updatedProduct = { updatedQuantity: previousQuantity - 1, ...rest };
setUpdatedQuantity(previousQuantity - 1);

Converting from class to functional component with async state setting

I have a simple class-based component that I'm trying to convert to a function-based component, but am running into all kinds of dead ends.
My component is a straightforward adaptation of the boilerplate gifted-chat package, and uses Watson Assistant as a backend to provide responses. There's nothing complex about the backend part, these are just thin wrappers on Watson Assistants's API:
getSessionID = async (): Promise<string>
gets a session ID for use in communicating with the backend, and
sendReply = async (reply: string, sessionID: string): Promise<string>
returns Assistant's response to the string provided as a reply. These are not the source of the trouble I'm having (the bodies of both could be replaced with return await "some string" and I'd have the same issues): the class-based version (below) works perfectly.
But I'm at a loss to figure out how to convert this to a functional form, in particular:
I'm struggling to find a suitable replacement for componentWillMount. Using useEffect with sessionID as state results in errors: getMessage gets called (even if I await) before the required sessionID is set.
I can avoid this by not making sessionID state (which it arguably shouldn't be) and just making it a global (as in the functional attempt below). But even if I do this:
After each user reply, and receipt of a response, the user reply is removed from the conversation, so that the entire conversation just consists of generated replies.
Both of these problems are, I think, linked to the lack of callbacks in the hook-based state setting idiom, but the issue could also lie elsewhere. In any case, I'm at a loss to know what to do.
Chatter.tsx (working class based version)
import React from 'react'
import { GiftedChat } from 'react-native-gifted-chat'
import WatsonAssistant from "../services/WatsonAssistant"
class Chatter extends React.Component {
state = {
messages: [],
sessionID: null,
}
componentWillMount() {
WatsonAssistant.getSessionID()
.then((sID) => {
this.setState( {
sessionID: sID,
} )
} )
.then(() => this.getMessage(''))
.catch((error) => {
console.error(error)
} )
}
onSend = (message = []): void => {
this.setState((previousState) => ( {
messages: GiftedChat.append(previousState.messages, message),
} ), () => {
this.getMessage(message[0].text.replace(/[\n\r]+/g, ' '))
} )
}
getMessage = async (text: string): Promise<void> => {
let response = await WatsonAssistant.sendReply(text, this.state.sessionID)
let message = {
_id: Math.round(Math.random() * 1000000).toString(),
text: response,
createdAt: new Date(),
user: {
_id: '2',
name: 'Watson Assistant',
},
}
this.setState((previousState) => ( {
messages: GiftedChat.append(previousState.messages, message),
} ))
}
render() {
return (
<GiftedChat
messages={ this.state.messages }
onSend={ messages => this.onSend(messages) }
user={ {
_id: 1,
} }
/>
)
}
}
export default Chatter
Chatter.tsx (failed function based attempt)
import React, {FC, ReactElement, useEffect, useState } from 'react'
import { GiftedChat } from 'react-native-gifted-chat'
import WatsonAssistant from "../services/WatsonAssistant"
let sessionID: string
const Chatter: FC = (): ReactElement => {
const [ messages, setMessages ] = useState([])
useEffect(() => {
const fetchData = async () => {
WatsonAssistant.getSessionID()
.then(sID => sessionID = sID )
.then(() => getMessage(''))
.catch((error) => {
console.error(error)
} )
}
fetchData()
}, [ ])
const onSend = async (message = []) => {
const newMessages = await GiftedChat.append(messages, message)
await setMessages(newMessages)
await getMessage(message[0].text.replace(/[\n\r]+/g, ' '))
}
const getMessage = async (text: string): Promise<void> => {
let response = await WatsonAssistant.sendReply(text, sessionID)
let message = {
_id: Math.round(Math.random() * 1000000).toString(),
text: response,
createdAt: new Date(),
user: {
_id: '2',
name: 'Watson Assistant',
},
}
await setMessages(await GiftedChat.append(messages, message))
}
return (
<GiftedChat
messages={ messages }
onSend={ messages => onSend(messages) }
user={ {
_id: 1,
} }
/>
)
}
export default Chatter
Chatter.tsx (working function based version)
import React, {FC, ReactElement, useEffect, useState } from 'react'
import { GiftedChat } from 'react-native-gifted-chat'
import WatsonAssistant from "../services/WatsonAssistant"
let sessionID: string
const Chatter: FC = (): ReactElement => {
const [ messages, setMessages ] = useState([])
useEffect(() => {
const fetchData = async () => {
WatsonAssistant.getSessionID()
.then(sID => sessionID = sID )
.then(() => getMessage('', []))
.catch((error) => {
console.error(error)
} )
}
fetchData()
}, [ ])
const onSend = async (message = []) => {
const newMessages = await GiftedChat.append(messages, message)
await setMessages(newMessages) // Apparently, no waiting goes on here
await getMessage(message[0].text.replace(/[\n\r]+/g, ' '), newMessages)
}
const getMessage = async (text: string, currentMessages): Promise<void> => {
let response = await WatsonAssistant.sendReply(text, sessionID)
let message = {
_id: Math.round(Math.random() * 1000000).toString(),
text: response,
createdAt: new Date(),
user: {
_id: '2',
name: 'Watson Assistant',
},
}
await setMessages(await GiftedChat.append(currentMessages, message))
}
return (
<GiftedChat
messages={ messages }
onSend={ messages => onSend(messages) }
user={ {
_id: 1,
} }
/>
)
}
export default Chatter
Ok, since I don't have your full code I'm not sure this will just work as-is (in particular without the types from your dependencies I'm not sure if/how much the compiler will complain), but should give you something you can adapt easily enough.
const reducer = ({ messages }, action) => {
switch (action.type) {
case 'add message':
return {
messages: GiftedChat.append(messages, action.message),
};
case 'add sent message':
return {
// Not sure if .append is variadic, may need to adapt
messages: GiftedChat.append(messages, action.message, action.message[0].text.replace(/[\n\r]+/g, ' ')),
}
}
};
const Chatter = () => {
const [sessionID, setSessionID] = useState(null);
const [messages, dispatch] = useReducer(reducer, []);
const getMessage = async (text: string, sessionID: number, type: string = 'add message'): Promise<void> => {
const response = await WatsonAssistant.sendReply(text, sessionID);
const message = {
_id: Math.round(Math.random() * 1000000).toString(),
text: response,
createdAt: new Date(),
user: {
_id: '2',
name: 'Watson Assistant',
},
};
dispatch({
type,
message,
});
};
useEffect(() => {
const fetchData = async () => {
WatsonAssistant.getSessionID()
.then(sID => (setSessionID(sID), sID))
.then(sID => getMessage('', sID))
.catch((error) => {
console.error(error)
});
}
fetchData();
}, []);
return (
<GiftedChat
messages={messages}
onSend={messages => getMessage(messages, sessionID, 'add sent message')}
user={{
_id: 1,
}}
/>
);
};
Main difference is useReducer. As far as I can tell in the original code you had two actions: append this message or append this message and then a copy of it with the text regex replaced. I've used different dispatches to the reducer to handle the cases rather than the callback to setState. I've modified your attempt at useEffect, here I'm (ab)using the comma operator to return the ID returned from the service so that it can be fed directly to getMessage as a parameter rather than relying on state that hasn't been updated yet.
I'm still kinda skeptical in general about the hooks API, but assuming this works I actually think it simplifies the code here.

Categories