Why does material-ui-lab rating component sometimes send null onChange? - reactjs

New to working with material-ui and I'm having an issue with input on #material-ui/lab/Rating. Occasionally, when I try to click on the rating component multiple times in a row, the value passed to my controller comes back as null instead of the rating I clicked on. The following is my console after logging incoming data in my controller:
Rating Passed: 5
id: 60a42672c615650cb85629f4
Rating Passed: null
id: 60a42672c615650cb85629f4
Rating Passed: 4
id: 60a42672c615650cb85629f4
Snippet from my front-end:
<CardActions className={classes.cardActions}>
<Rating name={post._id} value={post.raters !== 0 ? post.rating/post.raters : 0} precision={1} max={5} onChange={(event, v)=>{dispatch(ratePost(post._id , {rating: v}))}}/>
</CardActions>
And if it's relevant, my controller:
export const ratePost = async (req, res) => {
try {
console.log("Rating Passed: ", req.body.rating);
const { id: _id } = req.params;
console.log("id: ", _id);
if(!mongoose.Types.ObjectId.isValid(_id)) {
return res.status(404).send('Post to rate not found');
}
const post = await PostMessage.findById(_id);
let newRaters = post.raters + 1;
let newRating = (post.rating + req.body.rating);
if(req.body.rating === null) {
newRaters = post.raters;
newRating = post.rating;
}
const updatedPost = await PostMessage.findByIdAndUpdate(_id, {rating: newRating, raters: newRaters}, {new:true});
res.json(updatedPost);
} catch (error) {
console.error(error);
}
}
Currently, in my controller I just ignore null inputs. However, it makes the site feel unresponsive when a rating click isn't registered. What could cause the rating component to send a null value?

Related

React component uses old data from previous API call

I am using React Query to fetch data from an API I have built. The component is rendering the old data from the previous api call and not updating with new the data from the new api call.
The new data is only rendering when I refresh the page.
Component:
export const ProfilePageStats = (props: {
user: User;
id: number;
}) => {
const { chatId } = useParams();
const { status: subscribeStatus, data: subscribeData } =
useSubscriptionsWithType(
chatId ? chatId : "",
props.id,
props.user.id,
"SUBSCRIBE"
);
const { status: unsubscribeStatus, data: unsubscribeData } =
useSubscriptionsWithType(
chatId ? chatId : "",
props.id,
props.user.id,
"UNSUBSCRIBE"
);
if (unsubscribeStatus == "success" && subscribeStatus == "success") {
console.log("Working", unsubscribeData);
return (
<ProfilePageStatsWithData
user={props.user}
subscribed={Object.keys(subscribeData).length}
unsubscribed={Object.keys(unsubscribeData).length}
/>
);
}
if (unsubscribeStatus == "error" && subscribeStatus == "error") {
console.log("error");
return <ProfilePageStatsLoading />;
}
if (unsubscribeStatus == "loading" && subscribeStatus == "loading") {
console.log("loading");
return <ProfilePageStatsLoading />;
}
return <ProfilePageStatsLoading />;
};
export const useSubscriptionsWithType = (
chatId: string,
id: number,
userId: number,
type: string
) => {
return useQuery(
["subscriptionsWithType"],
async () => {
const { data } = await api.get(
`${chatId}/subscriptions/${id}/${userId}?type=${type}`
);
return data;
},
{
enabled: chatId > 0 && userId > 0,
refetchOnWindowFocus: false,
}
);
};
The component should update to show the new user values but shows the previous user values. If I click out and select a different user entirely it then shows the values for the previously clicked user.
I can see that React Query is fetching with the correct values for the query but the component still renders the old user data?
It turns out that the fetchStatus value is changing to "fetching" but it not actually calling the api. Hence, why its only using the old values?
Your key part of the useQuery is what tells the hook when to update.
You only use ["subscriptionsWithType"] as key, so it will never know that you need to refetch something.
If you add userId there, it will update when that changes.
So, using
return useQuery(
["subscriptionsWithType", userId],
async () => {
...
will work.
It is likely, that you want all the params, that you use in the url, to be added there.
I solved it by adding a useEffect and refetching based on the changing user id.
useEffect(() => {
refetch();
}, [props.user.id]);

React - PayPal Button fires without checking conditions

I'm using react-paypal-express-checkout
I've to options: Cash and PayPal.
Cash working fine and checks all conditions.
But bcs PayPal is a seperate component in my CartScreen component it opens and don't check a single if conditions and opens the PayPal window
The CashButton comes with function "cashTranSuccess" it's the same function as "TranSuccess"
just without the paymentID bcs it's only needed for react-paypal-express-checkout
So what I'm looking for is, to check all TranSuccess() conditions before open the PayPal window.
PayPalButton.js
import React from 'react';
import PaypalExpressBtn from 'react-paypal-express-checkout';
export default class PayPalButton extends React.Component {
render() {
const onSuccess = (payment) => {
// Congratulation, it came here means everything's fine!
console.log('The payment was succeeded!', payment);
// You can bind the "payment" object's value to your state or props or whatever here, please see below for sample returned data
this.props.tranSuccess(payment);
};
const onCancel = (data) => {
// User pressed "cancel" or close Paypal's popup!
console.log('The payment was cancelled!', data);
// You can bind the "data" object's value to your state or props or whatever here, please see below for sample returned data
};
const onError = (err) => {
// The main Paypal's script cannot be loaded or somethings block the loading of that script!
console.log('Error!', err);
// Because the Paypal's main script is loaded asynchronously from "https://www.paypalobjects.com/api/checkout.js"
// => sometimes it may take about 0.5 second for everything to get set, or for the button to appear
};
let env = 'sandbox'; // you can set here to 'production' for production
let currency = 'EUR'; // or you can set this value from your props or state
let carttotal = this.props.carttotal; // same a s above, this is the total amount (based on currency) to be paid by using Paypal express checkout
// Document on Paypal's currency code: https://developer.paypal.com/docs/classic/api/currency_codes/
const client = {
sandbox:
'',
production: 'YOUR-PRODUCTION-APP-ID',
};
// In order to get production's app-ID, you will have to send your app to Paypal for approval first
// For sandbox app-ID (after logging into your developer account, please locate the "REST API apps" section, click "Create App"):
// => https://developer.paypal.com/docs/classic/lifecycle/sb_credentials/
// For production app-ID:
// => https://developer.paypal.com/docs/classic/lifecycle/goingLive/
// NB. You can also have many Paypal express checkout buttons on page, just pass in the correct amount and they will work!
// Style Options: https://developer.paypal.com/docs/checkout/standard/customize/buttons-style-guide/ ; https://wise.com/gb/blog/custom-paypal-button
let style = {
size: 'medium',
color: 'gold',
label: 'pay',
tagline: false,
};
return (
<PaypalExpressBtn
env={env}
client={client}
currency={currency}
total={carttotal}
onError={onError}
shipping={1}
onSuccess={onSuccess}
onCancel={onCancel}
style={style}
/>
);
}
}
CartScreen
const tranSuccess = async (payment) => {
const { paymentID } = payment;
// Check time, min amoint, for delivery add delivery fees
if (timeValidation === true) {
if (sliderDeliveryValue === 'delivery') {
if (carttotal > settings[0]?.minDeliveryAmount) {
await axios.post(
'/api/payment',
{ cartItems, paymentID, time, sliderDeliveryValue, carttotal },
{
headers: { Authorization: token },
}
);
cartItems.map((remove) => {
dispatch(deleteFromCart(remove));
});
//console.log(cartItems.length);
toast.success(
'Order successful',
{
position: toast.POSITION.TOP_RIGHT,
}
);
} else {
toast.error(
`Min amount${settings[0]?.minDeliveryAmount}€`,
{
position: toast.POSITION.TOP_RIGHT,
}
);
}
} else if (sliderDeliveryValue === 'pickup') {
if (carttotal > 2) {
await axios.post(
'/api/payment',
{ cartItems, paymentID, time, sliderDeliveryValue, carttotal },
{
headers: { Authorization: token },
}
);
cartItems.map((remove) => {
dispatch(deleteFromCart(remove));
});
//console.log(cartItems.length);
toast.success(
'Order successful',
{
position: toast.POSITION.TOP_RIGHT,
}
);
} else {
toast.error(`Min amount 2.00€`, {
position: toast.POSITION.TOP_RIGHT,
});
}
} else {
toast.error('Choose delivery method', {
position: toast.POSITION.TOP_RIGHT,
});
}
} else {
toast.error('closed', {
position: toast.POSITION.TOP_RIGHT,
});
}
};
<PayPalButton
carttotal={carttotal}
tranSuccess={tranSuccess}
/>
<div onClick={cashTranSuccess}>
<CashButton />
</div>
Consider using the official #paypal/react-paypal-js
An example of validation using onInit and onClick functions and the actions.enable/disable callbacks or returning a promise (actions.resolve/reject) can be found in the developer documentation. Adapt this to check whatever condition you need.

(Refactor/Improve) Loop to make API calls and manupilate Array following the "no-loop-func"

Despite looking and following numerous answers here at stackoverflow,I have still failed to refactor this code to abide by the ESLint no-loop-func.
I keep getting the following warning, despite my efforts to refactor the code:
Compiled with warnings.
Function declared in a loop contains unsafe references to variable(s) 'lastResult', 'biologyBooks', 'page' no-loop-func
Here's the code:
import React from 'react';
import { apiFullCall } from '../../apiHelper';
const MyComponent = props => {
const [state, setState] = React.useState({ total: 0, biologyBooksByAuthor: [] });
let isLoaded = React.useRef(true);
const token = sessionStorage.getItem('token');
const authorID = sessionStorage.getItem('author_id');
const getBooks = async() => { // fetch items
let page = 1;
let scienceBooks, biologyBooks;
// create empty arrays to store book objects for each loop
let scienceBooks = biologyBooks = [];
// create a lastResult object to help check if there is a next page
let lastResult = { next: null };
do { // the looping - this is what I have failed to refactor
try {
await apiFullCall( // Make API calls over paginated records
'',
token,
'get',
`books/?author_id=1&page=${page}`
).then(res => {
if (res) {
const { status, body } = res;
if (status === 200 || status === 201) {
lastResult = body; // assign lastResult to pick "next"
body &&
body.results &&
body.results.map(eachBook => { // we map() over the returned "results" array
// the author with queried "author_id" writes science books;
// so we add each book (an object) into the science category
scienceBooks.push(eachBook);
// We then filter the author's biology books (from other science books)
biologyBooks = scienceBooks.filter(
({ is_biology }) =>
typeof(is_biology) === "boolean" && is_biology === true
);
return null;
}
);
// increment the page with 1 on each loop
page++;
}
}
}).catch(error => console.error('Error while fetching data:', error));
} catch (err) { console.error(`Oops, something went wrong ${err}`); }
// keep running until there's no next page
} while (lastResult.next !== null);
// update the state
setState(prevState => ({
...prevState, total: scienceBooks.length, biologyBooksByAuthor: biologyBooks,
}));
};
React.useEffect(() => { // fetch science books by author (logged in)
if (isLoaded && authorID) {
getBooks();
};
return function cleanup() {...}; // clean up API call, on unmount
}, [isLoaded, authorID]);
return (
// render the JSX code
);
}
Please note that I actually declared the said variables lastResult, biologyBooks and page outside the "do-while".
Any help or clues will be greatly appreciated.
The function the warning is referring to is the .then callback, if you're using async/await stick to it, try removing the .then part by assigning the result to a variable instead and remove the unnecessary .map, you can concatenate previous results with spread operator or .concat.
import React from 'react';
import { apiFullCall } from '../../apiHelper';
const MyComponent = props => {
const [state, setState] = React.useState({
total: 0,
scienceBooksByAuthor: [],
});
const isLoaded = React.useRef(true);
const token = sessionStorage.getItem('token');
const authorID = sessionStorage.getItem('author_id');
const getBooks = async () => {
// fetch items
let page = 1;
let scienceBooks = [];
// create a lastResult object to help check if there is a next page
let lastResult = { next: null };
do {
// the looping - this is what I have failed to refactor
try {
const res = await apiFullCall(
// Make API calls over paginated records
'',
token,
'get',
`books/?author_id=1&page=${page}`,
);
if (res) {
const { status, body } = res;
if (status === 200 || status === 201) {
lastResult = body; // assign lastResult to pick "next"
// concatenate new results
scienceBooks = [
...scienceBooks,
...((lastResult && lastResult.results) || []),
];
// increment the page with 1 on each loop
page += 1;
}
}
} catch (err) {
console.error(`Oops, something went wrong ${err}`);
}
// keep running until there's no next page
} while (lastResult.next !== null);
const biologyBooks = scienceBooks.filter(
({ is_biology }) =>
typeof is_biology === 'boolean' && is_biology === true,
);
// update the state
setState(prevState => ({
...prevState,
total: scienceBooks.length,
scienceBooksByAuthor: scienceBooks,
}));
};
React.useEffect(() => {
// fetch science books by author (logged in)
if (isLoaded && authorID) {
getBooks();
}
return function cleanup() {...}; // clean up API call, on unmount
}, [isLoaded, authorID]);
return (
// render the JSX code
);
};

Passing data from one screen to another screen with react-navigation

I'm currently passing the data to one component in react-native with some code which is shown as below:
class Login extends Component {
signInWithGoogle = async () => {
try {
const result = await Google.logInAsync({
iosClientId: IOS_CLIENT_ID,
androidClientId: ANDROID_CLIENT_ID,
scopes: ["profile", "email"]
});
if (result.type === "success") {
console.log(
"LoginScreen.js.js 21 | ",
result.user.givenName,
result.user.familyName,
result.user.email,
result.user.photoUrl
);
this.props.navigation.navigate(
"MyDrawer",
(username = result.user.givenName),
(lastname = result.user.familyName),
(email = result.user.email),
(photoUrl = result.user.photoUrl)
);
return result.accessToken;
} else {
return { cancelled: true };
}
} catch (e) {
console.log("LoginScreen.js.js 30 | Error with login", e);
return { error: true };
}
};
}
My question is how do I pass the data to a different component?
you need to pass an object as second parameter to the navigate method that is in props.
to pass a data to a different component, provide an object as the second argument for the navigate function
change this
this.props.navigation.navigate(
"MyDrawer",
(username = result.user.givenName),
(lastname = result.user.familyName),
(email = result.user.email),
(photoUrl = result.user.photoUrl)
);
to
this.props.navigation.navigate("MyDrawer",{
name: result.user.givenName,
lastname : result.user.familyName,
email :result.user.email,
photoUrl = result.user.photoUrl
});
now in the component that you are passing the data to, you can get the keys using the getKey method of react-navigation
TargetComponent.js
render() {
email = this.props.navigation.getParam("email", defaultValue);
password= this.props.navigation.getParam("password", defaultValue);
}

Redux devTools shows difference in state, but React component won't update

I have a React app with buttons that fetch recent bills and top donors for each Congress member. Both actions (fetchBillsByRep and getRepFinances) fetch correct data and correctly update Redux state. BUT. While fetching bills results in immediate re-render, fetching donors does not.
I have read more than a dozen answers to similar questions, and have tried the solutions. I am not mutating state; I always make a new copy. I am hitting debuggers where I expect to - and getting all values as expected.
inside MemberCard component, handleDonorsClick
//donors
handleDonorsClick = () => {
let id = this.props.crp_id
if (this.props.chamber === "senate"){
this.props.getSenatorFinances(id)
} else if (this.props.chamber === "house"){
this.props.getRepFinances(id)
}
this.setState({ showDonors: true })
debugger
//showDonors = true
}
HouseActions action creators: all values as expected:
export function fetchBillsByRep(id){
return (dispatch) => {
return fetch(API_BASE_URL+'/search/bills/member/'+id)
.then(resp => resp.json())
.then(bills => {
if (!bills.error) {
dispatch({type:"FETCH_BILLS_BY_REP", payload: { bills: bills, id:id}})
} else {
alert(bills.error.fullMessage)
}
}
).catch(error => alert(error))
}
}
export function getRepFinances(id){
return (dispatch) => {
return fetch(API_BASE_URL+'/search/financial_disclosures/member/'+id)
.then((resp) => resp.json())
.then(financialDisclosure => {
if (!financialDisclosure.error) {
dispatch({
type:"GET_REP_FINANCES",
payload: { financialDisclosure: financialDisclosure, id:id }})
} else {
alert(financialDisclosure.error.fullMessage)
}
}
).catch(error => alert(error))
}
}
house.js reducer:
again, all values as expected
export default (state = [], action) => {
switch(action.type){
case "SET_HOUSE":
return action.house
//<truncated>
case "FETCH_BILLS_BY_REP":
let bills = action.payload.bills
let house = state.map(rep => {
//find rep update bills
if (rep.propublica_id === action.payload.id){
rep.bills = bills
}
return rep
}
)
return house
case "GET_REP_FINANCES":
let financialDisclosure = action.payload.financialDisclosure
let house1 = state.map(rep => {
if (rep.crp_id === action.payload.id){
rep.financialDisclosure = financialDisclosure
rep.donors = financialDisclosure.donors
}
console.log(rep.donors)
return rep
//rep now has donors under rep.donors
}
)
return house1
//house1 is being returned as the new state for house
//inside house1, rep has updated rep.donors
default:
return state;
}
}
Redux DevTools State:Diff Tab after clicking get donors button:
(truncated, just showing top 2)
house
51
donors
0{
"id": 621,
"org_name": "Buchanan, Ingersoll & Rooney",
"total": 21600,
"pacs": 0,
"indivs": 21600
}
1{
"id": 622,
"org_name": "Telacu",
"total": 18900,
"pacs": 0,
"indivs": 18900
}
MemberCard component logic for deciding whether to render bills or donors:
//format back of card
let align
let content
let space
if (this.state.showBills){
image = ""
align = "center"
space = <br/>
content =
<>
<MemberBills
member={this.props}
showBills={this.state.showBills}/>
{hideBillsButton}
</>
} else if (this.state.showDonors){
//debugger hits, this.state.showDonors = true
image = ""
align = "center"
space = <br/>
content =
<>
<MemberDonors
member={this.props} showDonors={this.state.showDonors}/>
{/*member has value, showDonors is true*/}
{hideDonorsButton}
{/*hideDonorsButton has value*/}
</>
MemberDonors render:
<div>
<br/>
<hr/>
<h4 className="center">Top Three Donors</h4>
{popUpNotice}
{donorList}
{/* donorList has correct values */}
<br/>
{donorsSource}
{/* donorsSource has correct values */}
<br/>
<br/>
</div>
I don't get any errors. Instead, I get the MembersDonors view, showing "Top Three Donors", the popUp notice ... but no donorList. However, if I refresh the page, find that particular Member again, and re-click the showDonors button, the donors appear.
I am following the same pattern I used for fetching and showing bills.
Any ideas what I'm missing?
You didn't show your reducer in its entirety, but I bet it's because you are mutating your state, not creating a new version.
a = {}
b = a
b.d = 10
console.log(a)
console.log(b)
console.log(a === b)
What you need is to return a new copy of your state, which can be done using Object.assign or the spread operator
a = {}
b = {...a}
b.d = 10
console.log(a)
console.log(b)
console.log(a === b)
So in short, the easiest way to fix your code is to make a copy in your return statement for all of your cases.
return {...rep}

Resources