I am trying to create a timer window and I have created a seperate component for that. I am trying to pass the time in from the main component into the child component. The issue is the state for the timer input box is not changing. I have tried using useEffect but I have a feeling I may be implementing it wrongly.
Update
Found out that the event.target.value is returning an undefined which is why the state is not changing. Any ideas as to why it is returning an undefined?
Code
// Parent Component
const [targetTimeS, setTargetTimeS] = React.useState(60);
function handleMinuteSelectionChange(event) {
const minuteSelection = event.target.value;
if (isNaN(minuteSelection)) {
window.alert("Please select a number");
} else {
const minuteSelectionS = parseInt(minuteSelection);
if (minuteSelectionS > 59 || minuteSelectionS < 0) {
window.alert("Please choose a minutes value between 0 and 59");
} else {
console.log(`Minute selection changed: ${minuteSelectionS}`);
setTempTargetTimeS(minuteSelectionS * 60);
}
}
}
function timerWindow() {
return (
<TimerWindow
handleTargetTimeCancelButtonClicked={
handleTargetTimeCancelButtonClicked
}
handleMinuteSelectionChange={(e)=> handleMinuteSelectionChange(e)}
handleTargetTimeDoneButtonClicked={handleTargetTimeDoneButtonClicked}
value={targetTimeS / 60}
openModal={showTargetTimeWindow}
closeModal={hideTargetTimeWindow}
handlePresetButtonClicked={handlePresetButtonClicked}
handleHideTimerWindowClicked={hideTargetTimeWindow}
/>
);
}
Child Component
export default function TimerWindow(props: {
handleTargetTimeDoneButtonClicked: (e) => void;
handleTargetTimeCancelButtonClicked: (e) => void;
handleMinuteSelectionChange: (e) => void;
handlePresetButtonClicked?: (e: number) => void;
handleHideTimerWindowClicked?: (e) => void;
closeModal: (e) => void;
openModal: boolean;
value: number;
}): JSX.Element {
const [targetTimeS, setTargetTimeS] = useState(60);
useEffect(() => {
setTargetTimeS(props.value);
console.log(props.value);
}, [targetTimeS]);
const recorderClasses = speechRecorderStyles();
return (
<Modal open={props.openModal} onClose={props.closeModal}>
<div className={recorderClasses.targetTimeWindow}>
<CloseIcon
onClick={props.handleHideTimerWindowClicked}
className={recorderClasses.windowCloseButton}
/>
<Typography className={recorderClasses.selectRecordingTime}>
Select recording time
</Typography>
<Stack spacing={4} direction="column" alignItems="center" height="100%">
<Stack spacing={4} direction="row">
<TimerPresets onClick={props.handlePresetButtonClicked} />
</Stack>
</Stack>
<TextField
id="outlined-basic"
label="Minutes"
variant="outlined"
className={recorderClasses.targetTimeSelection}
value={targetTimeS}
onChange={(e) => props.handleMinuteSelectionChange(e.target.value)}
sx={{
mt: 6,
ml: 1,
}}
type="number"
/>
<Stack spacing={4} direction="row">
<Button
className={recorderClasses.targetTimeCancelButton}
onClick={props.handleTargetTimeCancelButtonClicked}
>
<Typography
className={recorderClasses.targetTimeCancelButtonTypography}
>
Cancel
</Typography>
</Button>
<Button
className={recorderClasses.targetTimeDoneButton}
onClick={props.handleTargetTimeDoneButtonClicked}
>
<Typography
className={recorderClasses.targetTimeDoneButtonTypography}
>
Done
</Typography>
</Button>
</Stack>
</div>
</Modal>
);
}
The problem is that you are doing the equivalent of event.target.value.target.value because you are calling event.target.value twice. That`s why it is returning undefined.
The first call is in your textField in the onChange callback:
onChange={(e) => props.handleMinuteSelectionChange(e.target.value)}
So you are already passing the value of the target component up and not the event. But then you are handling the value as it was the event in your handleMinuteSelectionChange
//Here in reality you have already the value, not the event
function handleMinuteSelectionChange(event) {
const minuteSelection = event.target.value;
//remaining code ...
}
Related
Trying to get an an onclick function to delete an item (that's been clicked) from an array.
However, it doesnt delete anything. setListOfDocs is supposed to set the listOfDocs array with the clicked item gone (onClick function is a filter)
It does not work. There's no error in the console. I don't know why it's not updating the state so that the listOfDocs is the new filtered array (with the clicked item removed from the array).
Im using material ui.
function NewMatterDocuments() {
const [listOfDocs, setListOfDocs] = useState(['item1', 'item2', 'item3', 'item4']);
const [disableButton, toggleDisableButton] = useState(false);
const [displayTextField, toggleDisplayTextField] = useState('none');
const [textInputValue, setTextInputValue] = useState();
const buttonClick = () => {
toggleDisableButton(true);
toggleDisplayTextField('box');
};
//this function works
const handleEnter = ({ target }) => {
setTextInputValue(target.value);
const newItem = target.value;
setListOfDocs((prev) => {
return [...prev, newItem];
});
setTextInputValue(null);
}; //
//this function does not work....which is weird because it pretty much
// does the same thing as the previous function except it deletes an item
// instead of adding it to the array. Why does the previous function work
// but this one doesnt?
const deleteItem = ({ currentTarget }) => {
const deletedId = currentTarget.id;
const result = listOfDocs.filter((item, index) => index !== deletedId);
setListOfDocs(result)
};
return (
<div>
<Typography variant="body2">
<FormGroup>
<Grid container spacing={3}>
<Grid item xs={6}>
<Grid item xs={6}>
Document Type
</Grid>
{listOfDocs.map((item, index) => {
return (
<ListItem id={index} secondaryAction={<Checkbox />}>
<ListItemAvatar>
<Avatar>
<DeleteIcon id={index} onClick={deleteItem} />
</Avatar>
</ListItemAvatar>
<ListItemButton>
<ListItemText id={index} primary={item} />
</ListItemButton>
</ListItem>
);
})}
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
<Button disabled={!disableButton ? false : true} color="inherit" onClick={buttonClick}>
Add
</Button>
<ListItem sx={{ display: `${displayTextField}` }} id={uuid()} key={uuid()}>
<TextField
id="standard-basic"
label="Additional Document"
variant="standard"
value={textInputValue}
onKeyDown={(e) => {
e.key === 'Enter' && handleEnter(e);
}}
></TextField>
</ListItem>
</List>
</Grid>
</Grid>
</FormGroup>
</Typography>
</div>
);
}
export default NewMatterDocuments;
I tried to change the function that wasnt working into the following:
const deleteItem = ({ currentTarget }) => {
setListOfDocs((prev) => {
prev.filter((item, index) => index != currentTarget.id);
});
};
It gives the error
"Uncaught TypeError: Cannot read properties of undefined (reading 'map')"
The next map function inside the jsx doesnt work...
You should return your data after filter:
const deleteItem = ({ currentTarget }) => {
setListOfDocs((prev) => {
return prev.filter((item, index) => index != currentTarget.id);
});
};
Update:
There is a problem with your first try. The currentTarget.id field is string but in your filter method, you're comparing it with the index (which is number) with !== which also checks types of the 2 variables.
So you can fix it by replacing !== with != or converting string to number.
I believe that you made a small mistake in your filtering function: you are comparing item index with deletedId, where you should compare item with deletedId
This should be correct:
const deleteItem = ({ currentTarget }) => {
const deletedId = currentTarget.id;
const result = listOfDocs.filter((item, index) => item !== deletedId);
setListOfDocs(result)
};
I'm trying to use this hook along with Material-UI's Autocomplete component, but am not having much success. Does anyone have an example of this scenario?
I receive a TS error for the value prop, stating
Type 'string' is not assignable to type 'AutocompletePrediction | null | undefined'.
The main issue though is that when I type into the input, results aren't being displayed for some reason, and neither the handleSelect nor the handleInput methods are being triggered.
Here's what I have so far:
import { useEffect, useRef, useState } from 'react';
import {
ClickAwayListener,
Grid,
TextField,
Typography,
} from '#material-ui/core';
import LocationOnIcon from '#material-ui/icons/LocationOn';
import parse from 'autosuggest-highlight/parse';
import Autocomplete from '#material-ui/lab/Autocomplete';
import usePlacesAutocomplete, {
getGeocode,
getLatLng,
} from 'use-places-autocomplete';
interface Props {
onSelect: (value: string) => void;
}
const EditLocation: React.FC<Props> = ({ onSelect }) => {
const {
ready,
value,
suggestions: { status, data },
setValue,
clearSuggestions,
} = usePlacesAutocomplete({
debounce: 300,
});
const handleInput = e => {
setValue(e.target.value);
};
const handleSelect =
({ description }: { description: string }) =>
() => {
console.log({ description });
// When user selects a place, we can replace the keyword without request data from API
// by setting the second parameter to "false"
setValue(description, false);
clearSuggestions();
// Get latitude and longitude via utility functions
getGeocode({ address: description })
.then(results => getLatLng(results[0]))
.then(({ lat, lng }) => {
console.log('📍 Coordinates: ', { lat, lng });
})
.catch(error => {
console.log('😱 Error: ', error);
});
};
return (
<ClickAwayListener onClickAway={() => clearSuggestions()}>
<Autocomplete
style={{ width: 300 }}
getOptionLabel={option =>
typeof option === 'string' ? option : option.description
}
filterOptions={x => x}
options={data}
autoComplete
includeInputInList
filterSelectedOptions
value={value} // <-- TS Error: "Type 'string' is not assignable to type 'AutocompletePrediction | null | undefined'."
onChange={handleInput}
renderInput={params => (
<TextField
{...params}
size="small"
label="Trip location"
variant="outlined"
fullWidth
/>
)}
renderOption={option => {
const matches =
option.structured_formatting.main_text_matched_substrings;
const parts = parse(
option.structured_formatting.main_text,
matches.map(match => [match.offset, match.offset + match.length])
);
return (
<Grid
container
alignItems="center"
onClick={() => handleSelect(option)}>
<Grid item>
<LocationOnIcon />
</Grid>
<Grid item xs>
{parts.map((part, index) => (
<span
key={index}
style={{ fontWeight: part.highlight ? 700 : 400 }}>
{part.text}
</span>
))}
<Typography variant="body2" color="textSecondary">
{option.structured_formatting.secondary_text}
</Typography>
</Grid>
</Grid>
);
}}
/>
</ClickAwayListener>
);
};
export default EditLocation;
The value property returned by usePlacesAutocomplete has type string while the data has type Suggestion[]. In order to make the Autocomplete stop complaining about the error, you need to pass a value with Suggestion type to the value prop of Autocomplete:
Autocomplete prop types: 1, 2.
<Autocomplete
options={data}
value={data.find(x => x.description === value)}
Validation error in my login form shows only upon clicking submit button a 2nd time with incorrect login credentials. Im noticing that even though the state is updating, error message is not being rendered the first time itself. It requires a 2nd submit click to be able to see the error message Snackbar.
I have defined a separate auth-context which has all the logic for making a post request for login and performing error handling, validations as well as setting user data to localStorage. It also has context providers to supply current state value. Login.js is using this auth-context to make a post request and based on the state either logs in or (is supposed to) displays validation errors.
Here's part of my Login.js page Component:
import { useAuth } from '../../context/auth-context';
export default function Login() {
const classes = useStyles();
const { handleChange, handleSubmit, formValues, formErrors } = useForm(submit, validate);
const [errorAlertMsg, setErrorAlertMsg] = useState('');
const authState = useAuth();
function submit() {
authState.login(formValues);
if(authState.error){
setErrorAlertMsg(authState.error);
}
/*authState initially shows error as null instead of the validation error. After submit is clicked it stays that way upon reaching here. On 2nd submit click it shows*/
console.log(authState);
}
function closeAlertHandler() {
setErrorAlertMsg('');
}
function Alert(props) {
return <MuiAlert elevation={6} variant="filled" {...props} />;
}
return (
<div>
<Grid container>
<Grid
item xs={4}
direction='row'
>
<UnauthenticatedSidebar/>
</Grid>
<Grid
item xs={8}
direction='row' >
<Container className={classes.mainContainer} component="main" maxWidth="xs">
<CssBaseline />
<Grid container justify='flex-end'>
<Grid item>
<Typography classes = {{ body2: classes.body2Style }} component="p" variant="body2">
Don't have an account?
<Link className={classes.routerLink} to='/signup'>
<Button color='primary' classes = {{ root: classes.buttonMarginRight }} variant="outlined" size="large">
Create Account
</Button>
</Link>
</Typography>
</Grid>
</Grid>
<div className={classes.paper}>
<Typography className={classes.welcomeHeading} component="h2" variant="h4">
Welcome Back!
</Typography>
<form onSubmit= { handleSubmit } className={classes.form} noValidate>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
InputProps = {{
classes: {root: classes.textFieldSelectLabel}
}}
InputLabelProps = {{
classes: {root: classes.textFieldSelectLabel}
}}
value={ formValues.email }
onChange = { handleChange }
error = { formErrors.email }
helperText = { formErrors.email || null }
/>
</Grid>
<Grid item xs={12}>
<TextField
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
InputProps = {{
endAdornment: <InputAdornment position='end'>
<Typography className={classes.adornmentStyle}>
Forgot?
</Typography>
</InputAdornment>,
classes: {root: classes.textFieldSelectLabel}
}}
InputLabelProps = {{
classes: {root: classes.textFieldSelectLabel}
}}
value={formValues.password}
onChange= { handleChange }
error = { formErrors.password }
helperText= { formErrors.password || null}
/>
</Grid>
</Grid>
<Button
type="submit"
variant="contained"
color="primary"
className={classes.submit}
>
Login
</Button>
</form>
<Snackbar open = {errorAlertMsg.length !== 0} autoHideDuration={5000} onClose = { closeAlertHandler }>
<Alert onClose={closeAlertHandler} severity="error">
{errorAlertMsg}
</Alert>
</Snackbar>
</div>
</Container>
</Grid>
</Grid>
</div>
);
}
Here's the auth-context.js file
import React, { useState } from 'react';
import axios from 'axios';
const AuthContext = React.createContext([{}, () => {}]);
function AuthProvider({children}) {
const [state, setState] = React.useState({
status: 'logged out',
error: null,
user: null
});
const [token, setToken] = useState(localStorage.getItem('authToken') ? localStorage.getItem('authToken') : null);
const [email, setEmail] = useState(localStorage.getItem('email') ? localStorage.getItem('email'): null);
//checking for token and email and then accordingly updating the state
const getUser = () => {
if(token) {
setState({status: 'success', error: null, user: email})
}
}
const logout = () => {
localStorage.removeItem('authToken');
setToken(null);
localStorage.removeItem('email');
setEmail(null);
setState({status: 'logged out', error: null, user: null})
}
const login = async(formValues) => {
try {
const res = await axios.post('http://localhost:3001/user/login', formValues);
if(res.data.token) {
setState({status:'success', error:null, user: formValues.email});
localStorage.setItem('authToken', res.data.token);
setToken(res.data.token);
localStorage.setItem('email', res.data.email);
setEmail(res.data.email);
}
}
catch(err) {
const validationError = err.response.data.validationError || null;
const missingDataError = err.response.data.missingData || null;
if(validationError){
setState({status: 'error', error: validationError, user: null})
}
else if(missingDataError) {
setState({status: 'error', error: missingDataError, user: null})
}
else {
console.error(err);
}
}
}
React.useEffect(() => {
getUser();
}, [token, email]);
let authState = {...state, logout, login}
/**
* Provider component is the place where you'd pass a prop called value to,
* which you can subsequently consume within the Consumer component
*/
return (
<AuthContext.Provider value={authState}>
{state.status === 'pending' ? (
'Loading...'
) : state.status === 'logged out' ? (
children
) : (
children
)}
</AuthContext.Provider>
)
}
//this seems simpler method to pass functions from context to consumers
function useAuth() {
const context = React.useContext(AuthContext)
if (context === undefined) {
throw new Error(`useAuth must be used within a AuthProvider`)
}
return context;
}
export {AuthProvider, useAuth};
There are two issues in here
login is an asynchronous function
State updates are bound by closure in functional component and do not reflect in the same render cycle.
Since you are using authState.error immediately after calling authState.login, you don't see the updated value.
The solution is to make use of useEffect and wait for the authState to change
function submit() {
authState.login(formValues);
}
useEffect(() => {
if(authState.error){
setErrorAlertMsg(authState.error);
}
console.log(authState);
}, [authState]);
What i'm trying to achieve is to have a BaseComponent which will be reuse in different ParentComponent.
My base card component props is ->
export type MSGameCardProps = {
title: string;
fetchGamesFn : (searchText: string) => Promise<IResultObject<any,any>>;
};
My base card render all the necessary basic logics and controls (inputs,autocomplete,title).
For example, it provide an autocomplete which have a simple search debounce functionality.
Me parent component will not necessary have props and will use the base card like so :
export type MSGameSrcCardProps = {};
export const MSGameSrcCard: React.FC<MSGameSrcCardProps> = () => {
const gameSvc = useGameService();
const fetchGame = async (searchText: string) => {
const rs = await result(gameSvc.getAll(searchText));
return rs;
};
return (
<MSGameCard title={"Convert From"} fetchGamesFn={fetchGame}></MSGameCard>
);
};
export default MSGameSrcCard;
The parent component will provide a fetchGames function which can be different.
It will also set the title and may later on set some other flags.
This pattern result with this error : Type '{}' is missing the following properties from type 'MSGameCardProps': title, fetchGamesFn when trying to use the parent component in my page like so : <MSGameSrcCard></MSGameSrcCard>
I don't understand why my parent should have those properties since they are only required in the child component and are fullfill in my parent component function.
I don't want to make them optional(?) since they are actually required; of course only for my base component
I did try to export my basecomponent AS ANY which remove the error but now my props.fetchGamesFn is always undefined even passing it in inside my parent component function.
Maybe i'm doing it wrong but is there a way to have a parent components with no props with child that required props?
EDIT : Here is my MSGameCard base component definition
export const MSGameCard: React.FC<MSGameCardProps> = props => {
const [games, setGames] = React.useState([
{
name: ""
}
]);
const [selectedGame, setSelectedGame] = React.useState<any>();
const [previousGame, setPreviousGame] = React.useState<any>();
const [isLoading, setIsLoading] = React.useState(false);
const [opacity, setOpacity] = React.useState(0);
const fetchGameBase = (searchText: string) => {
setIsLoading(true);
console.log(props.fetchGamesFn);
props.fetchGamesFn(searchText).then(rs =>{
if (rs.isSuccess) setGames(rs.result.data);
setIsLoading(false);
})
};
const searchDebounce = debounce(300, fetchGameBase);
React.useEffect(() => {
fetchGameBase("");
}, []);
const onGameChanged = (event: any, value: any) => {
if (selectedGame) setPreviousGame(selectedGame);
setOpacity(0);
if (value) {
setTimeout(() => {
setSelectedGame(value);
setOpacity(0.2);
}, 300);
}
};
const onInputChanged = (e: any) => {
let value = e.target.value;
if (!value) value = "";
searchDebounce(value);
};
const getSelectedGameImg = () => {
const bgUrl: string = selectedGame
? selectedGame.bg_url
: previousGame?.bg_url;
return bgUrl;
};
return (
<Card style={{ position: "relative", zIndex: 1 }} variant="outlined">
<CardContent style={{ zIndex: 1 }}>
<Typography variant="h5" gutterBottom>
{props.title}
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<Autocomplete
options={games}
getOptionLabel={option => option.name}
onChange={onGameChanged}
onInputChange={onInputChanged}
renderInput={params => (
<TextField
{...params}
label="Source game"
fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{isLoading ? (
<CircularProgress color="primary" size={30} />
) : null}
{params.InputProps.endAdornment}
</React.Fragment>
)
}}
/>
)}
/>
</Grid>
</Grid>
<Grid container direction="row" spacing={3}>
<Grid item xs={12} sm={6}>
<TextField label="DPI" fullWidth />
</Grid>
<Grid item xs={12} sm={6}>
<TextField label="Sensitivity" fullWidth />
</Grid>
</Grid>
</CardContent>
<img
style={{ opacity: opacity }}
className="gameImage"
src={getSelectedGameImg()}
/>
</Card>
);
};
export default MSGameCard;
Keep updating notice
After checked the minimum reproducible example you have provided.
I found no type error, am I missing something?
Since the error occurred in both two props, I would leave only the string for the check
import * as React from "react";
import "./styles.css";
export type MSGameCardProps = {
title: string;
};
export type MSGameSrcCardProps = {};
export const MSGameSrcCard: React.SFC<MSGameSrcCardProps> = () => {
return <MSGameCard title={"Convert From"} />;
};
const MSGameCard: React.SFC<MSGameCardProps> = (props: MSGameCardProps) => {
console.log(props); // Object {title: "Convert From"}
return <></>;
};
export default function App() {
return (
<div className="App">
<MSGameSrcCard />
</div>
);
}
Try it online here:
I am currently using Material ui v1.4.3 autocomplete. Material UI stated that this autocomplete is integrated with react-select.
I have followed the code here which is working like a charm but in order to handle fetching larger data in the future, I would like to implement the search function to call the database whenever the input changes so that I am able to narrow down the data that is being fetched from the database.
Has anyone had experience on this? Because the static method from this code is blocking me to call any reducer function that is passed from my parent component.
What would be an appropriate way that allows me to catch the input from the user so that I am able to call my function.
function NoOptionsMessage(props) {
return (
<Typography
color="textSecondary"
className={props.selectProps.classes.noOptionsMessage}
{...props.innerProps}
>
{props.children}
</Typography>
);
}
function inputComponent({ inputRef, ...props }) {
return <div ref={inputRef} {...props} />;
}
function Control(props) {
////console.dir(props.selectProps.inputValue); i am able to get the user input here
// so i was thinking i can call my reducer somewhere here but no luck
// my function is passed from my parent component so i wish to call this.props.theFunction here
return (
<TextField
fullWidth
InputProps={{
inputComponent,
inputProps: {
className: props.selectProps.classes.input,
ref: props.innerRef,
children: props.children,
...props.innerProps,
},
}}
onChange={(e) => IntegrationReactSelect.testing(e)}
/>
);
}
function Option(props) {
return (
<MenuItem
buttonRef={props.innerRef}
selected={props.isFocused}
component="div"
style={{
fontWeight: props.isSelected ? 500 : 400,
}}
{...props.innerProps}
>
{props.children}
</MenuItem>
);
}
function Placeholder(props) {
return (
<Typography
color="textSecondary"
className={props.selectProps.classes.placeholder}
{...props.innerProps}
>
{props.children}
</Typography>
);
}
function SingleValue(props) {
return (
<Typography className={props.selectProps.classes.singleValue} {...props.innerProps}>
{props.children}
</Typography>
);
}
function ValueContainer(props) {
return <div className={props.selectProps.classes.valueContainer}>{props.children}</div>;
}
function MultiValue(props) {
return (
<Chip
tabIndex={-1}
label={props.children}
className={classNames(props.selectProps.classes.chip, {
[props.selectProps.classes.chipFocused]: props.isFocused,
})}
onDelete={event => {
props.removeProps.onClick();
props.removeProps.onMouseDown(event);
}}
/>
);
}
const components = {
Option,
Control,
NoOptionsMessage,
Placeholder,
SingleValue,
MultiValue,
ValueContainer
};
class IntegrationReactSelect extends React.Component {
state = {
single: null,
multi: null,
};
handleChange = name => value => {
this.setState({
[name]: value,
});
this.props.getSelectMultipleValue(value);
};
render() {
const { classes } = this.props;
return (
<div className={classes.root}>
<Select
classes={classes}
options={this.props.theUserFromParentComponent}
components={components}
value={this.state.multi}
onChange={this.handleChange('multi')}
placeholder={this.props.reactSelectName}
isMulti
/>
</div>
);
}
}