Unexpected React countdown component re-render, when input changes - reactjs

I have created a custom countdown component using react-countdown package.
It work fines in general. But when I type inside a text input in my page, it will render again somehow and resets to its initial time. I have checked the onChange event of the input but it has nothing to do with the countdown. I'm really confused why this happens.
My idea in creating the countdown was that, if I change the key prop of countdown component, I will have a fresh countdown. Because as I know if we change the key prop in react components they will re-render.
Countdown component:
const AgapeCountdown = ({ duration, children, restartKey, ...props }) => {
const classes = useStyles();
const defaultRenderer = ({ hours, minutes, seconds, completed }) => {
if (completed) {
return children;
}
return (
<span className={classes.root}>
{minutes}:{seconds}
</span>
);
};
return (
<Countdown
renderer={defaultRenderer}
date={Date.now() + duration}
key={restartKey}
{...props}
/>
);
};
Usage:
<AgapeCountdown duration={10000} restartKey={countdownKey}>
<AgapeButton onClick={handleResendOtpClick} className={classes.textButton}>
ارسال مجدد کد
</AgapeButton>
</AgapeCountdown>;
input element in the same page:
<AgapeTextField
placeholder="مثال: ۱۲۳۴۵"
variant="outlined"
fullWidth
onChange={handleOtpChange}
value={otp}
helperText={otpHelperText}
error={otpHelperText}
/>
input change handler:
const handleOtpChange = (event) => {
if (otpRegex.test(event.target.value)) {
setOtpHelperText(null);
setDisableOtpAction(false);
setOtp(event.target.value).then(() => {
nextButtonClicked();
});
} else {
setOtp(event.target.value);
setOtpHelperText(helperInvalidOtp);
setDisableOtpAction(true);
}
};
where countdownKey get updated:
const handleResendOtpClick = () => {
setCountdownKey(countdownKey + 1);
console.log('hello from resendotpclick');
registerApiService({
mobile: phoneNumberPure,
})
.then((response) => {
if (response.status === 200) {
// TODO show user that otp code resent.
}
})
.catch((error) => {
// TODO show user that otp code resend failed.
});
};
full code for deeper inspection:
const LoginStep2 = ({ dialogHandler, ...props }) => {
const classes = useStyles(props);
const setIsLoginOpen = dialogHandler;
const dispatch = useDispatch();
const phoneNumberPure = useSelector(selectPhone);
const ELogin = useSelector(selectELogin);
const [otp, setOtp] = useStateWithPromise(null);
const [otpHelperText, setOtpHelperText] = React.useState(null);
const [disableOtpAction, setDisableOtpAction] = React.useState(true);
const [phoneNumber, setPhoneNumber] = React.useState('');
const [countdownKey, setCountdownKey] = React.useState(1);
React.useEffect(() => {
if (phoneNumberPure) {
setPhoneNumber(phoneNumberPure.split('-')[1]);
}
}, [phoneNumberPure]);
const handlePrevIconClicked = () => {
if (ELogin) {
dispatch(next());
return;
}
dispatch(prev());
};
const nextButtonClicked = () => {
setDisableOtpAction(true);
const convertedOtp = convertPersianDigitsToEnglish(otp);
loginApiService({ mobile: phoneNumberPure, otp: convertedOtp })
.then((response) => {
if (response.status === 200) {
if (response.data.access_token) {
const jsonUser = {
phone: phoneNumberPure,
token: response.data.access_token,
social: null,
email: null,
};
localStorage.setItem('user', JSON.stringify(jsonUser));
if (ELogin) {
setIsLoginOpen(false);
return;
}
dispatch(next());
}
} else if (response.status === 404) {
setOtpHelperText(helperWrongOtp);
}
})
.catch((error) => {
setOtpHelperText(helperWrongOtp);
})
.finally(() => {
setTimeout(() => {
setDisableOtpAction(false);
}, 1000);
});
};
const handleResendOtpClick = () => {
setCountdownKey(countdownKey + 1);
console.log('hello from resendotpclick');
registerApiService({
mobile: phoneNumberPure,
})
.then((response) => {
if (response.status === 200) {
// TODO show user that otp code resent.
}
})
.catch((error) => {
// TODO show user that otp code resend failed.
});
};
const handleOtpChange = (event) => {
if (otpRegex.test(event.target.value)) {
setOtpHelperText(null);
setDisableOtpAction(false);
setOtp(event.target.value).then(() => {
nextButtonClicked();
});
} else {
setOtp(event.target.value);
setOtpHelperText(helperInvalidOtp);
setDisableOtpAction(true);
}
};
return (
<Grid container>
<Grid item xs={12}>
<IconButton onClick={handlePrevIconClicked}>
<BsArrowRight className={classes.arrowIcon} />
</IconButton>
</Grid>
<Grid
item
container
xs={12}
justify="center"
className={classes.logoContainer}
>
<img src={AgapeLogo} alt="لوگوی آگاپه" />
</Grid>
<Grid
item
container
xs={12}
justify="center"
className={classes.loginTitle}
>
<Typography variant="h4">کد تایید را وارد نمایید</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="body1" className={classes.noMargin}>
کد تایید به شماره
<span className={classes.phoneNumberContainer}>{phoneNumber}</span>
ارسال گردید
</Typography>
</Grid>
<Grid
item
container
xs={12}
justify="space-between"
className={classes.loginInputs}
>
<Grid item xs={12}>
<AgapeTextField
placeholder="مثال: ۱۲۳۴۵"
variant="outlined"
fullWidth
onChange={handleOtpChange}
value={otp}
helperText={otpHelperText}
error={otpHelperText}
/>
</Grid>
</Grid>
<Grid item xs={12}>
<AgapeButton
color="primary"
disabled={disableOtpAction}
onClick={nextButtonClicked}
fullWidth
>
تایید
</AgapeButton>
</Grid>
<Grid
item
container
xs={12}
justify="space-between"
className={classes.textButtonsContainer}
>
<Grid item xs={4}>
<AgapeCountdown duration={10000} restartKey={countdownKey}>
<AgapeButton
onClick={handleResendOtpClick}
className={classes.textButton}
>
ارسال مجدد کد
</AgapeButton>
</AgapeCountdown>
</Grid>
<Grid item xs={4} className={classes.callButton}>
<AgapeButton className={classes.textButton}>
دریافت از طریق تماس
</AgapeButton>
</Grid>
</Grid>
</Grid>
);
};

I found the problem. this is the part actually creates the problem:
<Countdown
renderer={defaultRenderer}
date={Date.now() + duration}
key={restartKey}
{...props}
/>
the Date.now() will update. And it makes the countdown to restart. for solving this problem I used a ref which stop the component to re-render if it changes:
const AgapeCountdown = ({ duration, children, restartKey, ...props }) => {
const classes = useStyles();
const startDate = React.useRef(Date.now());
const defaultRenderer = ({ hours, minutes, seconds, completed }) => {
return (
<span className={classes.root}>
{minutes}:{seconds}
</span>
);
};
return (
<Countdown
renderer={defaultRenderer}
date={startDate.current + duration}
key={restartKey}
{...props}
/>
);
};

Related

Loop in useEffect?

I'm trying to select a date and the data should be reloaded every time a specific date is selected. But with my code, it would loop.
Currently I'm using syncfusion's schedule component. Redux toolkit.
Here is my code.
const Schedules = () => {
const { facilities } = useSelector((store) => store.allFacilities);
const { fieldsScheduler } = useSelector((store) => store.allFields);
const { timeSlots, isLoadingReservation } = useSelector(
(store) => store.allReservation
);
const [calRef, setCalRef] = useState();
const [time, setTime] = useState("");
useEffect(() => {
if (calRef && time) {
console.log("TIME", time);
dispatch(getAllReservation(time)); <--- Loop
}
}, [time, calRef]);
useEffect(() => {
dispatch(getFacilities());
dispatch(getFields());
}, []);
const dispatch = useDispatch();
if (isLoadingReservation) {
return <Loading />;
}
const headerInfo = (props) => {
return (
<Grid container>
<Grid item xs={12}>
<h6>{props.subject}</h6>
</Grid>
</Grid>
);
};
const bodyInfo = (props) => {
return (
<Grid container>
<Grid item xs={12}>
<h6>{`Giá tiền cụ thể`}</h6>
</Grid>
</Grid>
);
};
const footerInfo = (props) => {
return (
<Grid container>
<Grid item xs={12}>
<h6>{`Footer here`}</h6>
</Grid>
</Grid>
);
};
const eventTemplate = (props) => {
return (
<>
<div>
<Typography variant="p">
{`${props.subject}: ${props.rentalFee}`}K
</Typography>
</div>
<div>
<Typography variant="p">{`${moment(props.startTime, "HHmm").format(
"HH:mm"
)} - ${moment(props.endTime, "HHmm").format("HH:mm")} `}</Typography>
</div>
</>
);
};
const onDataBinding = () => {
// var scheduleObj = document.querySelector(".e-schedule").ej2_instances[0];
// var currentViewDates = scheduleObj.getCurrentViewDates();
// dispatch(setCurrentDate(moment(startDate).format("YYYY-MM-DD[T]HH:mm:ss")));
// dispatch(
// getAllReservation(moment(startDate).format("YYYY-MM-DD[T]HH:mm:ss"))
// );
var currentViewDates = calRef.getCurrentViewDates();
var startDate = currentViewDates[0];
var endDate = currentViewDates[currentViewDates.length - 1];
console.log("Start date", startDate);
// setTime(moment(startDate).format("YYYY-MM-DD[T]HH:mm:ss"));
};
return (
<MDBox
width="100%"
height="100%"
minHeight="100vh"
borderRadius="lg"
shadow="lg"
bgColor="white"
sx={{ overflowX: "hidden" }}
>
<ScheduleComponent
cssClass="timeline-resource-grouping"
width="100%"
height="100%"
locale="vi"
readonly={true}
currentView="TimelineDay"
allowDragAndDrop={false}
dataBinding={onDataBinding}
// onChange={onDataBinding}
ref={(t) => setCalRef(t)}
// quickInfoTemplates={{
// header: headerInfo.bind(this),
// content: bodyInfo.bind(this),
// footer: footerInfo.bind(this),
// }}
//Data get all event in here. then mapping in ResourceDirective (field)
eventSettings={{
dataSource: timeSlots,
fields: {
subject: { name: "subject" },
id: "reservationId",
endTime: { name: "endTime" },
startTime: { name: "startTime" },
rentalFee: { name: "rentalFee", title: "Phí thuê sân" },
},
template: eventTemplate.bind(this),
}}
group={{ resources: ["Facilities", "Fields"] }}
>
<ResourcesDirective>
<ResourceDirective
field="facilityId"
title="Chọn cơ sở"
name="Facilities"
allowMultiple={false}
dataSource={facilities}
textField="facilityName"
idField="id"
></ResourceDirective>
<ResourceDirective
field="fieldId"
title="Sân"
name="Fields"
allowMultiple={true}
dataSource={fieldsScheduler}
textField="fieldName"
idField="id"
groupIDField="facilityId"
colorField="color"
></ResourceDirective>
</ResourcesDirective>
<ViewsDirective>
<ViewDirective option="TimelineDay" />
<ViewDirective option="Day" />
</ViewsDirective>
<Inject
services={[
Day,
Week,
TimelineViews,
TimelineMonth,
Agenda,
Resize,
DragAndDrop,
]}
/>
</ScheduleComponent>
</MDBox>
);
};
export default Schedules;
This is my slice.
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import { toast } from "react-toastify";
import customFetch from "utils/axios";
const initialState = {
isLoadingReservation: false,
timeSlots: [],
currentDate: "",
};
const authHeader = (thunkAPI) => {
return {
headers: {
Authorization: `Bearer ${thunkAPI.getState().user.user.accessToken}`,
},
};
};
export const getAllReservation = createAsyncThunk(
"allReservation/getAllReservation",
async (currentDay, thunkAPI) => {
console.log("Log:", currentDay);
try {
const response = await customFetch.post(
`/reservation-slots`,
{ date: currentDay },
authHeader(thunkAPI)
);
return response.data.timeSlots;
} catch (error) {
return thunkAPI.rejectWithValue(error.response.data);
}
}
);
const allReservationSlice = createSlice({
name: "allReservation",
initialState,
reducers: {
setCurrentDate: (state, action) => {
console.log("Payload", action.payload);
state.currentDate = action.payload;
},
},
extraReducers: {
[getAllReservation.pending]: (state, action) => {
state.isLoadingReservation = true;
},
[getAllReservation.fulfilled]: (state, action) => {
state.isLoadingReservation = false;
state.timeSlots = action.payload;
},
[getAllReservation.rejected]: (state, action) => {
state.isLoadingReservation = false;
toast.error(action.payload);
},
},
});
export const { setCurrentDate } = allReservationSlice.actions;
export default allReservationSlice.reducer;
But there is no way to do that. I used useEffect to update the UI again, but it still loop again.
I don't know if there is a way to solve my problem?
I sat for 3 days straight and the situation did not improve much.

React REDUX is updating all state after the update action

I've been figuring out this bug since yesterday.
All of the states are working before the update action. I have console log all the states before the update action.
Then after creating a model, the update action is executed.
This is the result when I console log.
I wondered why dataGrid returns an error since I point to all the id in the DataGrid component.
Uncaught Error: MUI: The data grid component requires all rows to have a unique `id` property.
This is my code:
Models Reducer:
import * as actionTypes from 'constants/actionTypes';
export default (models = [], action) => {
switch (action.type) {
case actionTypes.FETCH_MODELS:
return action.payload.result;
case actionTypes.CREATE:
return [...models, action.payload.result];
case actionTypes.UPDATE:
return models.map((model) => (model.model_id === action.payload.result.model_id ? action.payload.result : model));
case actionTypes.DELETE:
return models.filter((model) => model.model_id !== action.payload);
default:
return models;
}
};
In my model component:
import * as actionTypes from 'constants/actionTypes';
export default (models = [], action) => {
switch (action.type) {
case actionTypes.FETCH_MODELS:
return action.payload.result;
case actionTypes.CREATE:
return [...models, action.payload.result];
case actionTypes.UPDATE:
return models.map((model) => (model.model_id === action.payload.result.model_id ? action.payload.result : model));
case actionTypes.DELETE:
return models.filter((model) => model.model_id !== action.payload);
default:
return models;
}
};
My ModelForm:
<Formik
enableReinitialize={true}
initialValues={modelData}
validationSchema={Yup.object().shape({
model_code: Yup.string(4).min(4, 'Minimum value is 4.').max(50, 'Maximum value is 4.').required('Model code is required'),
model_description: Yup.string().max(200, 'Maximum value is 200.'),
model_status: Yup.string().min(5).max(10, 'Maximum value is 10.')
})}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
try {
if (scriptedRef.current) {
if (currentId === 0) {
// , name: user?.result?.name
dispatch(createModel({ ...values }, setFormVisible));
} else {
dispatch(updateModel(currentId, { ...values }, setFormVisible));
}
setStatus({ success: true });
setSubmitting(false);
}
} catch (err) {
console.error(err);
if (scriptedRef.current) {
setStatus({ success: false });
setErrors({ submit: err.message });
setSubmitting(false);
}
}
}}
>
{({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, resetForm, values }) => (
<form noValidate onSubmit={handleSubmit}>
<Grid container spacing={1}>
<Grid item lg={4} md={4} sm={12}>
<JTextField
label="Model"
name="model_code"
value={values.model_code}
onBlur={handleBlur}
onChange={handleChange}
touched={touched}
errors={errors}
/>
</Grid>
</Grid>
<Grid container spacing={1} sx={{ mt: 1 }}>
<Grid item lg={4} md={4} sm={12}>
<JTextField
label="Description"
name="model_description"
value={values.model_description}
onBlur={handleBlur}
onChange={handleChange}
touched={touched}
type="multiline"
rows={4}
errors={errors}
/>
</Grid>
</Grid>
{currentId ? (
<Grid container spacing={1} sx={{ mt: 1 }}>
<Grid item lg={4} md={4} sm={12}>
<JSelect
labelId="model_status"
id="model_status"
name="model_status"
value={values.model_status}
label="Status"
onBlur={handleBlur}
onChange={handleChange}
errors={errors}
>
<MenuItem value="ACTIVE">ACTIVE</MenuItem>
<MenuItem value="INACTIVE">INACTIVE</MenuItem>
</JSelect>
</Grid>
</Grid>
) : (
''
)}
<Box sx={{ mt: 2 }}>
<ButtonGroup variant="contained" aria-label="outlined button group">
<Button size="small" disabled={isSubmitting} type="submit">
Save
</Button>
<Button size="small" onClick={resetForm}>
Cancel
</Button>
{currentId ? (
<Button size="small" color="secondary" onClick={handleDelete}>
Delete
</Button>
) : (
''
)}
</ButtonGroup>
</Box>
</form>
)}
</Formik>
Why products, parts or other states are updating too? Since I only update the model create action?
Please check this out: https://www.awesomescreenshot.com/video/11412230?key=a0212021c59aa1097fa9d38917399fe3
I Hope someone could help me figure out this bug. This is only the problem else my CRUD template is good.
Update:
Found out that actions in redux should always be unique or else multiple reducers with the same action name will be triggered.
I have updated my action types to:
// AUTHENTICATION ACTIONS
export const AUTH = 'AUTH';
export const LOGOUT = 'LOGOUT';
// MODEL ACTIONS
export const FETCH_MODELS = 'FETCH_MODELS';
export const CREATE_MODEL = 'CREATE_MODEL';
export const UPDATE_MODEL = 'UPDATE_MODEL';
export const DELETE_MODEL = 'DELETE_MODEL';
// PRODUCTS ACTIONS
export const FETCH_PRODUCTS = 'FETCH_PRODUCTS';
export const CREATE_PRODUCT = 'CREATE_PRODUCT';
export const UPDATE_PRODUCT = 'UPDATE_PRODUCT';
export const DELETE_PRODUCT = 'DELETE_PRODUCT';
// ASSEMBLY ACTIONS
export const FETCH_ASSEMBLY = 'FETCH_ASSEMBLY';
export const CREATE_ASSEMBLY = 'CREATE_ASSEMBLY';
export const UPDATE_ASSEMBLY = 'UPDATE_ASSEMBLY';
export const DELETE_ASSEMBLY = 'DELETE_ASSEMBLY';
// PARTS ACTIONS
export const FETCH_PARTS = 'FETCH_PARTS';
export const CREATE_PART = 'CREATE_PART';
export const UPDATE_PART = 'UPDATE_PART';
export const DELETE_PART = 'DELETE_PART';
Reducers to:
import * as actionTypes from 'constants/actionTypes';
export default (models = [], action) => {
switch (action.type) {
case actionTypes.FETCH_MODELS:
return action.payload.result;
case actionTypes.CREATE_MODEL:
return [...models, action.payload.result];
case actionTypes.UPDATE_MODEL:
console.log(models);
return models.map((model) => (model.model_id === action.payload.result.model_id ? action.payload.result : model));
case actionTypes.DELETE_MODEL:
return models.filter((model) => model.model_id !== action.payload);
default:
return models;
}
};
and Actions to:
import * as actionTypes from 'constants/actionTypes';
import * as api from 'api/index.js';
import Swal from 'sweetalert2';
export const getModels = () => async (dispatch) => {
try {
const { data } = await api.fetchModels();
dispatch({ type: actionTypes.FETCH_MODELS, payload: data });
} catch (error) {
console.log(error);
Swal.fire('Error!', 'Something went wrong', 'error');
}
};
export const createModel = (model, setFormVisible) => async (dispatch) => {
try {
const { data } = await api.createModel(model);
dispatch({ type: actionTypes.CREATE_MODEL, payload: data });
setFormVisible(false);
Swal.fire('Success!', 'Model has been added successfully', 'success');
} catch (error) {
console.log(error);
Swal.fire('Error!', 'Something went wrong', 'error');
}
};
export const updateModel = (id, model, setFormVisible) => async (dispatch) => {
try {
const { data } = await api.updateModel(id, model);
dispatch({ type: actionTypes.UPDATE_MODEL, payload: data });
setFormVisible(false);
Swal.fire('Success!', 'Model updated successfully', 'success');
} catch (error) {
console.log(error);
Swal.fire('Error!', 'Something went wrong', 'error');
}
};
export const deleteModel = (id) => async (dispatch) => {
try {
await await api.deleteModel(id);
dispatch({ type: actionTypes.DELETE_MODEL, payload: id });
Swal.fire('Success!', 'Model deleted successfully', 'success');
} catch (error) {
console.log(error);
Swal.fire('Error!', 'Something went wrong', 'error');
}
};

Why isn't functional child component changing state on props change?

I have a form template with a popup as child component. The popup works to display response from server for a form query(ajax) to login/forgot-password, etc. and will disappear automatically after some time.
Now, for the first time popup works fine and displays response and disappears but if I try it again from same page (as query is sent as ajax and page reloading doesn't happen) the message gets updated but it won't appear.
And what I have concluded to be the problem is that state of child component(isShown) not updating on message update and I can't seem to solve this. Or it can be I am overlooking some other thing .
AppForm.jsx (parent template)
function MainContent(props) {
const { title, underTitle, children, buttonText, customHandler, bottomPart } = props;
const [banner, setBanner] = useState({code: null, msg: null})
const reqHandler = async () => {
customHandler().then((resp) => {
setBanner({code: 'success', msg: resp.data.msg})
}).catch((err) => {
setBanner({ code: 'error', msg: err.response.data.msg })
});
}
return (
<ThemeProvider theme={formTheme}>
<Container maxWidth="sm">
<AppFormPopup statusCode={banner.code} msg={banner.msg} />
<Box sx={{ mt: 7, mb: 12 }}>
<Paper>
{children} //displays the fields required by derived form page
<Button type='submit' sx={{ mt: 3, mb: 2 }} onClick={reqHandler}>
{buttonText}
</Button>
</Paper>
</Box>
</Container>
</ThemeProvider>
)
}
function AppForm(props) {
return (
<>
<AppFormNav />
<MainContent {...props} />
</>
);
}
AppForm.propTypes = {
children: PropTypes.node,
};
export default AppForm;
AppFormPopup.jsx (child Component which shows popup msg)
function MainContent(props){
const { msg } = props;
const [progress, setProgress] = useState(0);
const [isShown, setIsShown] = useState(true); //state used for controlling visibility of popup msg.
useEffect(() => {
const timer = setInterval(() => {
setProgress((oldProgress) => {
if(oldProgress === 100){
setIsShown(false);
// return 0;
}
return Math.min(oldProgress + 10, 100);
});
}, 500);
return () => {
clearInterval(timer);
};
});
return (
<Grid container sx={{
display: isShown ? 'block' : 'none',
}}>
<Grid item sx={{ color: "white", pl: 1 }}> {msg} </Grid>
</Grid>
)
}
export default function AppFormPopup({msg}) {
if(msg === null) return;
return (
<MainContent msg={msg} />
)
}
ForgotPass.jsx (form page which derives form template AppForm.jsx)
export default function ForgotPass() {
const loc = useLocation().pathname;
const mailRef = useRef(null);
const reqHandler = async () => {
const email = mailRef.current.value;
if(email){
const resp = await axios.post(loc, {email})
return resp;
}
}
return (
<AppForm
buttonText="Send Reset Link"
customHandler={reqHandler}
>
<Box sx={{ mt: 6 }}>
<TextField
autoFocus
label="Email"
inputRef={mailRef}
/>
</Box>
</AppForm>
)
}
This is due to the fact that your useEffect will only render once. My suggestion would be to change your useEffect to render if a value change, for example: Let say you got a state name of UserName. Now each time that UserName is being change will rerender your UseEffect function. By adding a [UserName] at end of useEffect it will cause a rerender each time UserName change.
useEffect(() => {
const timer = setInterval(() => {
setProgress((oldProgress) => {
if(oldProgress === 100){
setIsShown(false);
// return 0;
}
return Math.min(oldProgress + 10, 100);
});
}, 500);
return () => {
clearInterval(timer);
};
},[UserName]);
It feels weird answering your question but here we go.
So, thanks #GlenBowler his answer was somewhat right and nudged me towards the right answer.
Instead of modifying of that useEffect, we needed another useEffect to check for changes in msg. So, code should look like this:
AppFormPopup.jsx
function MainContent(props){
const { msg } = props;
const [progress, setProgress] = useState(0);
const [isShown, setIsShown] = useState(true); //state used for controlling visibility of popup msg.
useEffect(() => {
setIsShown(true);
setProgress(0);
}, [msg]);
useEffect(() => {
const timer = setInterval(() => {
setProgress((oldProgress) => {
if(oldProgress === 100){
setIsShown(false);
// return 0;
}
return Math.min(oldProgress + 10, 100);
});
}, 500);
return () => {
clearInterval(timer);
};
});
return (
<Grid container sx={{
display: isShown ? 'block' : 'none',
}}>
<Grid item sx={{ color: "white", pl: 1 }}> {msg} </Grid>
</Grid>
)
}
export default function AppFormPopup({msg}) {
if(msg === null) return;
return (
<MainContent msg={msg} />
)
}

React, filter is not working after refreshing the component

EDIT: for some reason it works in production, however I can't proceed on dev mode.
I'm writing a Contacts Book app, which shows "main" contacts and "subcontacts". When I first load this contacts book, it just run as expected, contacts are loaded successfully within the Busqueda(means search) component, which is an autocomplete that loads all the contacts and when one is selected, the information in displayed in other components.
I have buttons to edit contact, subcontact and erase them too, however, when I do any of these actions, I send a "reload" action so the list is updated with the changes, and it throws allResults.filter is not a function as allResults seems to be an object.
function Agenda() {
const MiContexto = useOutletContext();
useEffect(()=>{MiContexto.CrearContactoModalDispatch(false); },[]);
const [selectedValue, setSelectedValue] =useState(null);
const [selectedSubContact, setSelectedSubContact] =useState(null);
useEffect(()=>{
MiContexto.setSelectedContact(selectedValue);
MiContexto.setAgenda_nombre_empresa(selectedValue?.nombre_empresa);
},[selectedValue]);
const [allResults, setAllResults] =useState([]);
const [coincideArray, setCoincideArray] =useState(allResults?.filter(entry => entry?.empresa_id === selectedValue?.empresa_id));
const [reloadBusqueda,setReloadBusqueda] = useState(0);
useEffect(()=>{
setCoincideArray(allResults?.filter(entry => entry?.empresa_id === selectedValue?.id_empresa));
},[allResults!==null, selectedValue]);
return (
<div className={'outletBasicSetting'}>
<Busqueda setSelectedValue={setSelectedValue} allResults={setAllResults} reload={reloadBusqueda}/>
<Grid container>
<Grid item xs={12} md={6}>
<EntidadPrincipal selectedValue={selectedValue}/>
</Grid>
<Grid container item xs={12} md={6}>
{coincideArray[0]?.id_empresa && coincideArray?.map((persona)=>{
return(
<Grid key={persona.id_contacto} item xs={12} md={6}>
<SubEntidad persona={persona}/>
</Grid>)
})}
</Grid>
</Grid>
<CrearContacto opening={MiContexto.CrearContactomodal} setReloadBusqueda={setReloadBusqueda}/>
{coincideArray[0]?.id_empresa
?
<EditarQuien openQuien={MiContexto.quienModal} contacto={selectedValue} coincideArray={coincideArray} setReloadBusqueda={setReloadBusqueda} setSelectedSubContact={setSelectedSubContact}/>
:
<EditarContactoP opening={MiContexto.quienModal} setReloadBusqueda={setReloadBusqueda} contacto={selectedValue}/>}
<EditarContactoP opening={MiContexto.EditarPModal} setReloadBusqueda={setReloadBusqueda} contacto={selectedValue}/>
<EditarContactoS opening={MiContexto.EditarCModal} setReloadBusqueda={setReloadBusqueda} contacto={selectedSubContact}/>
<CrearSubContacto opening={MiContexto.crearSubContactoModal} setReloadBusqueda={setReloadBusqueda} contacto={selectedValue}/>
<BorrarQuien openBorrarQuien={MiContexto.borrarCualContactoAgendaModal} contacto={selectedValue} coincideArray={coincideArray} setReloadBusqueda={setReloadBusqueda} setSelectedSubContact={setSelectedSubContact}/>
</div>
);
}
Busqueda Component:
import { Autocomplete, Stack, TextField, Chip, Avatar, autocompleteClasses, styled, Popper } from '#mui/material';
import React, { useEffect, useState } from 'react';
import ListboxComponent from './VariableSizeList';
import { toast } from 'react-toastify';
function Busqueda({setSelectedValue,allResults,reload}){
const [jsonResults, setJsonResults] = useState([]);
var token = "";
const [selected, setSelected] = useState( null);
const [storedSelected,setStoredSelected] = useState(null);
const [open, setOpen] = React.useState(false);
const handleChange = (e,v) =>{
setSelected(v);
setStoredSelected(v);
};
useEffect(()=>{
token=JSON.parse(localStorage.getItem('token'));
},[]);
useEffect(()=>{
setSelectedValue(selected);
allResults(jsonResults);
},[allResults, jsonResults, selected, setSelectedValue]);
const StyledPopper = styled(Popper)({
[`& .${autocompleteClasses.listbox}`]: {
boxSizing: 'border-box',
'& ul': {
padding: 0,
margin: 0,
},
},
});
useEffect(() => {
console.time("Tiempo de carga de LeerAgenda");
fetch(process.env['REACT_APP_DEV_PROXY']+"https://server.com/file.php",{
method: 'POST',
headers:{'Token':token["token"]},
body: "",
})
.then((response) => response.json())
.then((json)=>{
setJsonResults(json);
console.timeEnd("Tiempo de carga de LeerAgenda");
setSelected(null);
})
.catch((err) => {
toast.error(JSON.stringify(err["msg"]));
console.error("Se catcheo un error: "+err["msg"]);
})
},[reload])
return (
<Autocomplete
open={open}
onInputChange={(_, value) => {
if (value.length === 0) {
if (open) setOpen(false);
} else {
if (!open) setOpen(true);
}
}}
onClose={() => setOpen(false)}
value={selected}
id="agenda"
disableListWrap
ListboxComponent={ListboxComponent}
getOptionLabel={jsonResults => jsonResults?.nombre_contacto ?
jsonResults?.nombre_empresa +": "+jsonResults?.nombre_contacto
: jsonResults?.nombre_empresa
} //lo que aparece en la caja al seleccionar
options={jsonResults}
PopperComponent={StyledPopper}
renderInput={(params) => <TextField {...params} label="Busca aquí el contacto"/>}
renderOption={(props, jsonResults) => [props,
<>
<Chip label={jsonResults.nombre_empresa} color="primary" size="small" avatar={<Avatar>{jsonResults.nombre_empresa.charAt(0)}</Avatar>}/>
{jsonResults?.nombre_contacto}</>
]} // el texto de cada opcion desplegada
onChange={handleChange}
isOptionEqualToValue={(option,value) => option.id_contacto === value.id_contacto}
noOptionsText={"Tu texto no coincide con ningún resultado"}
/>
</Stack>)
}
export default Busqueda;

React Redirect Not Loading Page Correctly

When redirecting in react, the page url changes successfully, but nothing loads. In fact, I get a TypeError: n is not a function error. But if I reload the page I redirected to it loads perfectly fine. What am I missing here? Ever since I migrated to hooks, I've struggled to find a straight answer on this. The only links I currently use are located in my App.js and utilize Material-UI buttons.
"react-router-dom": "^5.2.0"
App.js
...
<main className={classes.content}>
<div className={classes.toolbar} />
<BrowserRouter>
<Switch>
<Route exact path='/portal/:stateName/data/' component={DataView} />
<Route exact path='/portal/data-comparison-tool' component={CompareTool} />
<Route exact path='/portal/regional-analysis' component={RegionalAnalysis} />
<Route exact path='/portal/reporting' component={Reporting} />
<Route path='/portal/upload' component={UploadCsv} /> <--- Route in question
<Route exact path='/portal/' component={Dashboard} />
</Switch>
</BrowserRouter>
</main>
...
Dashboard.js
Here is where I am attempting to redirect upon a button click. When the button is selected, the state changes enabling the below functionality.
if (bulkUpload) {
return (
<Redirect to={{ pathname: '/portal/upload', state: { from: props.location } }} />
)
}
UploadCsv
Per other's ask, here is the component I am trying to load.
...
export default function UploadCsv (props) {
const classes = useStyles()
const [doneLoading, setDoneLoading] = useState()
const [uploadData, setUploadData] = useState([])
const [dataSet, setDataSet] = useState(null)
const [columns, setColumns] = useState(tableHfcCols)
const [error, setError] = useState()
const [dbData, setDbData] = useState()
const [isProcessing, setIsProcessing] = useState()
const [stateName, setStateName] = useState()
const [existData, setExistData] = useState([])
const [newData, setNewData] = useState([])
const [openDialog, setOpenDialog] = useState()
const [checks, setChecks] = useState({
invalidCols: [],
endUse: [],
category: [],
state: [],
year: []
})
useEffect(async () => {
async function load () {
// load credential data
const { data } = await axios('/account/fetch')
await setStateName(data.user_state_name)
}
// Load
await load()
await setDoneLoading(true)
}, [])
const loadData = async () => {
// Endpoints
const scenarioEndpoint = `/api/measurements/${stateName}/`
const metricsEndpoint = `/api/metrics/${stateName}/`
// Gather measurements data for specific state
if (dataSet === 'hfc') {
const apiData = await axios(scenarioEndpoint).then(r => r.data.hfc_metrics)
await setDbData(apiData)
return apiData
} else if (dataSet === 'state') {
const apiData = await axios(metricsEndpoint).then(r => r.data.state_metrics)
await setDbData(apiData)
return apiData
}
}
// add file to upload box
const handleOnFileLoad = async (data) => {
const headerClean = await CleanHeader(data)
const dataSetName = await WhichData(headerClean)
if (dataSetName === 'hfc') {
const cleanData = await headerClean.map(d => ShapeHfcData(d))
await setDataSet(dataSetName)
await setColumns(tableHfcCols)
await setUploadData(cleanData)
// analyze data
const { invalidCols } = await CheckColumns({ setName: 'hfc', data: cleanData })
await setChecks({ ...checks, invalidCols: invalidCols })
// clean end_use fields
} else if (dataSetName === 'state') {
const cleanData = await headerClean.map(d => ShapeStateData(d))
await setDataSet(dataSetName)
await setColumns(tableStateCols)
await setUploadData(headerClean)
// analyze data
const { invalidCols } = await CheckColumns({ setName: 'state', data: cleanData })
await setChecks({ ...checks, invalidCols: invalidCols })
}
}
// Remove file from upload field
const handleOnRemoveFile = async (data) => {
setUploadData([])
setColumns([])
setDataSet()
}
const handlePrepSubmit = async () => {
setIsProcessing(true)
// get data
const apiData = await loadData()
// check which records exist and which don't
const { exist, notExist } = await parseData({ apiData: apiData, uploadData: uploadData })
// run checks and return results
const { endUseCheck, categoryCheck, yearCheck, stateCheck } = await runChecks(uploadData)
// update state
await setChecks({
...checks,
endUse: endUseCheck,
category: categoryCheck,
state: stateCheck,
year: yearCheck
})
await setExistData(exist)
await setNewData(notExist)
await setOpenDialog(true)
}
const runChecks = async (data) => {
// check if endUse values are correct
const endUseCheck = []
const categoryCheck = []
const yearCheck = []
const stateCheck = []
data.forEach(async function (row) {
const endUseStatus = await CheckEndUse(row.end_use)
const categoryStatus = await CheckCategory(row.category)
const stateStatus = await CheckState(row.state)
const yearStatus = await CheckYear(row.year)
if (endUseStatus === 'fail') {
endUseCheck.push(row)
} else if (categoryStatus === 'fail') {
categoryCheck.push(row)
} else if (stateStatus === 'fail') {
stateCheck.push(row)
} else if (yearStatus === 'fail') {
yearCheck.push(row)
}
})
return { endUseCheck, categoryCheck, yearCheck, stateCheck }
}
// Check to see which are updates and which are new records
const parseData = ({ apiData, uploadData }) => {
const exist = []
const notExist = []
const hfcHeader = [[...HfcHeader, ['id']].flat()][0]
uploadData.forEach(function (row) {
const dbRecordsYear = reducedFilter(apiData, hfcHeader, item => item.year === row.year)
const dbRecordsCategory = reducedFilter(dbRecordsYear, hfcHeader, item => item.category === row.category)
const record = reducedFilter(dbRecordsCategory, hfcHeader, item => item.end_use === row.end_use)
const apiRecord = reducedFilterId(dbRecordsCategory, hfcHeader, item => item.end_use === row.end_use)
if (record.length > 0) {
row.id = apiRecord[0].id
exist.push(row)
} else if (record.length < 1) {
notExist.push(row)
}
})
return { exist, notExist }
}
// Dialog Actions
const handleDialogClose = () => {
setOpenDialog(false)
}
if (!doneLoading) {
return <CircularProgress color='secondary' size='2.5rem' thickness='2.0' />
}
const submitData = async () => {
const location = dataSet === 'hfc' ? 'measurements' : 'metrics'
if (existData.length > 0) {
existData.forEach(function (row) {
updateRecord(row, location)
console.log('update', row)
})
}
if (newData.length > 0) {
newData.forEach(function (row) {
createRecord(row, location)
console.log('create', row)
})
}
}
const updateRecord = async (row, location) => {
// If the object exists, update it via API
const endpoint = `/api/${location}/record/${row.id}/`
await axios.put(endpoint, row)
}
const createRecord = async (row, location) => {
const endpoint = `/api/${location}/create`
await axios.post(endpoint, row)
}
console.log(uploadData)
return (
<>
<Grid container className={classes.container}>
<Toolbar className={classes.pageHeader}>
<Typography className={classes.title} variant='h3' color='secondary' align='left'>
Upload Data
</Typography>
</Toolbar>
<Grid item xs={12}>
<Paper className={classes.stagePaper}>
<ReactMarkdown
source={HelpText}
escapeHtml={false}
/>
</Paper>
</Grid>
<Grid item xs={6}>
<Paper className={classes.middlePaper}>
<Grid container className={classes.container}>
<Toolbar className={classes.pageHeader}>
<Typography className={classes.title} variant='h5' color='secondary' align='left'>
Sample Data Sets
</Typography>
</Toolbar>
<Grid item xs={12}>
<SampleDataText />
</Grid>
<Grid item xs={6} className={classes.innerGrid}>
<Button variant='outlined' fullWidth color='secondary' onClick={() => csvDownload(hfcData, 'scenario_sample_data.csv')}>Scenario Data</Button>
</Grid>
<Grid item xs={6} className={classes.innerGrid}>
<Button variant='outlined' fullWidth color='primary' onClick={() => csvDownload(stateData, 'state_sample_data.csv')}>State Data</Button>
</Grid>
</Grid>
</Paper>
</Grid>
<Grid item xs={6}>
<Paper className={classes.middlePaper}>
<Toolbar className={classes.pageHeader}>
<Typography className={classes.title} variant='h5' color='secondary' align='left'>
Upload .CSV File
</Typography>
</Toolbar>
<Upload
setError={setError}
error={error}
handleOnFileLoad={handleOnFileLoad}
handleOnRemoveFile={handleOnRemoveFile}
/>
</Paper>
</Grid>
{uploadData.length < 1
? ''
: <Grid item xs={12} className={classes.stagePaper}>
<MaterialButton
onClick={() => handlePrepSubmit()}
disabled={isProcessing}
text={isProcessing ? 'PROCESSING PRELIM CHECKS...' : 'BEGIN UPLOAD'}
color='accent'
width='100'
/>
</Grid>}
<Grid item xs={12}>
<Paper className={classes.stagePaper}>
<link rel='stylesheet' href='https://fonts.googleapis.com/icon?family=Material+Icons' />
<CustomMaterialTable
title='Preview Data'
columns={(uploadData.length > 0) ? columns : []}
data={(uploadData.length > 0) ? uploadData : []}
components={{
Container: props => props.children
}}
options={{
headerStyle: {
padding: '10px 4px 10px 4px',
fontSize: '.85rem'
},
cellStyle: {
padding: '14px 4px 14px 4px',
fontSize: '.8rem'
},
pageSize: uploadData.length,
selection: true,
rowStyle: rowData => ({ backgroundColor: rowData.tableData.checked ? '#37b15933' : '' })
}}
editable={{
onRowAddCancelled: rowData => console.log('Row adding cancelled'),
onRowUpdateCancelled: rowData => console.log('Row editing cancelled'),
onRowAdd: newData =>
new Promise((resolve, reject) => {
setTimeout(() => {
/* setData([...data, newData]); */
resolve()
}, 1000)
}),
onRowUpdate: (newData, oldData) =>
new Promise((resolve, reject) => {
setTimeout(() => {
const dataUpdate = [...uploadData]
const index = oldData.tableData.id
dataUpdate[index] = newData
setUploadData([...dataUpdate])
resolve()
}, 1000)
}),
onRowDelete: oldData =>
new Promise((resolve, reject) => {
setTimeout(() => {
const dataDelete = [...uploadData]
const index = oldData.tableData.id
dataDelete.splice(index, 1)
setUploadData([...dataDelete])
resolve()
}, 1000)
})
}}
/>
</Paper>
</Grid>
</Grid>
<Footer />
<UploadPop
openDialog={openDialog}
handleDialogClose={handleDialogClose}
existData={existData}
newData={newData}
submitData={submitData}
checks={checks}
/>
</>
)
}
Try changing the route path for /portal/upload/ to
<Route exact path='/portal/upload' component={UploadCsv} />
If this does not fix it, you might have to post the definition fro {UploadCsv}
I was never able to find a straight answer to this. It's apparently a pretty common problem, and from what I've seen most people seem to be working around it. My headaches around React-Router are partially why I enjoy working with Nextjs so much.
Here's my workaround since I was never able to get history.push(/portal/) to work correctly using React Router v5.
window.location.href = '/portal/'
It's not an ideal solution but solves 90% of my use case. Time to move on.

Resources