I have a simple slider component from Material-UI v4, mostly taken from this example.
I want to update the UI, both the slider and the number (which is displayed in the UI) 'instantly', but debounce the dispatch to redux, so it doesn't update the store unnecessarily.
This is what I've done so far to modify the example:
import { debounce } from 'lodash';
import { useDispatch } from 'redux';
import { updatingAction } from 'action/foo';
export default function InputSlider() {
const dispatch = useDispatch();
const isReduxInSyncRef = useRef(false);
const reduxValue = useSelector(getReduxValue);
const [value, setValue] = React.useState(30);
const debouncedDispatch = debounce((value) => {
dispatch(updatingAction(value));
isReduxInSyncRef.current = true;
}, 200);
const updateStateAndRedux = (newValue) => {
setValue(newValue);
isReduxInSyncRef.current = false;
}
const handleSliderChange = (event, newValue) => {
updateStateAndRedux(newValue);
};
const handleInputChange = ({ target: { value } }) => {
updateStateAndRedux(value === '' ? '' : Number(value));
debouncedDispatch(value);
};
useEffect(() => {
if (reduxValue !== value && isReduxInSyncRef.current) {
setValue(reduxValue);
}
}, [value, reduxValue]);
return (
<div className={classes.root}>
<Typography id="input-slider" gutterBottom>
Volume
</Typography>
<Grid container spacing={2} alignItems="center">
<Grid item>
<VolumeUp />
</Grid>
<Grid item xs>
<Slider
value={typeof value === 'number' ? value : 0}
onChange={handleSliderChange}
onChangeCommitted={(event, value) => debouncedDispatch(value)}
aria-labelledby="input-slider"
/>
</Grid>
<Grid item>
<Input
className={classes.input}
value={value}
margin="dense"
onChange={handleInputChange}
inputProps={{
step: 10,
min: 0,
max: 100,
type: 'number',
'aria-labelledby': 'input-slider',
}}
/>
</Grid>
</Grid>
</div>
);
}
Because the slider and value need to move together, when I debounced the whole thing (redux + useState), I couldn't move the slider at all.
Using onChangeCommitted for the dispatch only helps the slider, but it seems like the number input onChange (handleInputChange) is firing the dispatch for every value, without being debounced.
What have I done wrong, and is there an easier/better way to do this?
Related
I'm trying to use Slider/Select to get user query parameters, which will make further changes to the URL (through handleChange) and make api calls through fetch hooks.
However, when I change the Slider value from 1 to 0.1, nothing happens, but if I change the slider value again to 0.1 to 0.2, it will return me the result of 0.1 ( therefore lagging by 1 click). Similar behavior is observed with the Select value.
Website here:https://thisorthatstockfrontend.herokuapp.com/rankStockSharpeFiltered
I don't have form control but after trying form control it didn't seem to fix the issue. I really do not want to have a submit button, any help is appreciated!
import React from 'react'
import { NavLink } from 'react-router-dom'
import { Button,InputLabel } from '#mui/material'
import { Typography,MenuItem } from '#mui/material'
import { DataGrid } from '#mui/x-data-grid'
import Slider from '#mui/material/Slider';
import { useEffect } from 'react'
import {FormControl} from '#mui/material';
import { useState } from 'react'
import { useFetch } from '../hooks/useFetch';
import { useForm,Controller } from "react-hook-form";
import Select,{SelectChangeEvent} from '#mui/material/Select';
import Grid from '#mui/material/Grid'
import { Box } from '#mui/system';
export default function StockListedFiltered()
{
const [values, setValues] = useState(
{
sharpeYear1: 1,
sharpeYear2: 5,
sharperatio1Cutoff: 1,
sharperatio2Cutoff: 1,
}
);
const [url,seturl]=useState('https://thisorthatstock.herokuapp.com/multipleStockPerformanceFilteredByYear?shortYearNum=' + values.sharpeYear1+'&LongYearNum='+values.sharpeYear2+'&sharpeRatio1='+values.sharperatio1Cutoff+'&sharpeRatio2='+values.sharperatio2Cutoff)
const handleChange = (propertyName) => (event) => {
console.log('hello')
event.preventDefault();
setValues((values) => ({
...values,
[propertyName]: event.target.value
}));
console.log('end')
console.log(values)
seturl('https://thisorthatstock.herokuapp.com/multipleStockPerformanceFilteredByYear?shortYearNum=' + values.sharpeYear1+'&LongYearNum='+values.sharpeYear2+'&sharpeRatio1='+values.sharperatio1Cutoff+'&sharpeRatio2='+values.sharperatio2Cutoff)
console.log(url)
};
const{data:stockPerformance ,isPending,error}=useFetch(url)
const columns=[
{field:'stockName', headerName:'ticker',width:200 },
{field:'years', headerName:'years' ,width:200},
{field:'SharpeRatio', headerName:'sharpe ratio',width:200},
{field:'AnnualReturn', headerName:'annual return',width:200 },
{field:'AnnualVolatility', headerName:'annual volatility',width:200 }
];
return (
<div>
<h2 style={{paddingTop:'3rem',textAlign:'center',paddingBottom:'2rem'}}> Filtered Stocks Ranked </h2>
<NavLink to="/rankStockSharpe">
<Button>
<Typography>All stock </Typography>
</Button>
</NavLink>
<NavLink to="/rankStockSharpeFiltered">
<Button>
<Typography>Filtered </Typography>
</Button>
</NavLink>
<Grid container style={{paddingTop:'2rem',textAlign:'center'}}>
<Grid item xs={2}></Grid>
<Grid item xs={4} style={{display:'flex',alignItems:'center',justifyContent:'center' }}>
<Grid item xs={6}>
<Typography>
Years of Data
</Typography>
<FormControl sx={{ m: 1, minWidth: 120 }}>
<Select
defaultValue={1}
labelId="sharpeYear1"
id="sharpeYear1-select"
value={values.sharpeYear1}
label="Year"
onChange={handleChange("sharpeYear1")}
>
<MenuItem value={1}>1</MenuItem>
<MenuItem value={2}>2</MenuItem>
<MenuItem value={3}>3</MenuItem>
<MenuItem value={4}>4</MenuItem>
<MenuItem value={5}>5</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={6}>
<Box sx={{ width: '80%' }}>
<Typography>
Sharpe Ratio cut off
</Typography>
<Slider
defaultValue={1}
value={values.sharperatio1Cutoff}
onChange={ handleChange("sharperatio1Cutoff")}
step={0.05}
valueLabelDisplay="auto"
min={0}
max={2}
/>
</Box>
</Grid>
</Grid >
<Grid item xs={4} style={{display:'flex',alignItems:'center',justifyContent:'center' }}>
<Grid item xs={6}>
<Typography>
Years of Data
</Typography>
<Select
defaultValue={5}
labelId="sharpeYear2"
id="sharpeYear2-select"
value={values.sharpeYear2}
label="Year"
onChange={handleChange("sharpeYear2")}
>
<MenuItem value={1}>1</MenuItem>
<MenuItem value={2}>2</MenuItem>
<MenuItem value={3}>3</MenuItem>
<MenuItem value={4}>4</MenuItem>
<MenuItem value={5}>5</MenuItem>
</Select>
</Grid>
<Grid item xs={6}>
<Box sx={{ width: '80%' }}>
<Typography>
Sharpe Ratio cut off
</Typography>
<Slider
defaultValue={1}
value={values.sharperatio2Cutoff}
onChange={handleChange("sharperatio2Cutoff")}
step={0.05}
valueLabelDisplay="auto"
min={0}
max={2}
/>
</Box>
</Grid>
</Grid>
</Grid>
<div style={{display:'flex',alignItems:'center',justifyContent:'center' }}>
{isPending&&<div>Loading Data...</div>}
{error &&<div>"This is awkard..."{error}</div>}
{!isPending && stockPerformance&&
<div style={{height:500,width:'70%'}}>
<DataGrid
rows={stockPerformance}
columns={columns}
></DataGrid>
</div>
}
</div>
</div>
)
}
React useState is asynchronous, so updating state & then accessing it immediately in next line will not work. Updated state is only available on the next render cycle.
The below will not console log the updated state. Same for setUrl.
setValues((values) => ({
...values,
[propertyName]: event.target.value
}));
console.log('end')
console.log(values) // state is not yet updated
If you want to perform an action on state update, you need to use the useEffect hook, much like using componentDidUpdate in class components since the setter returned by useState doesn't have a callback pattern
useEffect(() => {
// action on update of values
console.log(values) // updated state is available here
}, [values]);
You can clone the values state, make your changes by mutating and update your state. Then use the modified object to calculate the url so it has the new values.
const handleChange = (propertyName) => (event) => {
event.preventDefault();
const newValues = { ...values };
newValues[propertyName] = event.target.value;
setValues(newValues);
seturl(
'https://thisorthatstock.herokuapp.com/multipleStockPerformanceFilteredByYear?shortYearNum=' +
newValues.sharpeYear1 +
'&LongYearNum=' +
newValues.sharpeYear2 +
'&sharpeRatio1=' +
newValues.sharperatio1Cutoff +
'&sharpeRatio2=' +
newValues.sharperatio2Cutoff
);
};
Or even better, calculate url in a second time (not directly in your handler), plus it doesn't have to be in the state.
const handleChange = (propertyName) => (event) => {
event.preventDefault();
const newValues = { ...values };
newValues[propertyName] = event.target.value;
setValues(newValues);
};
const url = useMemo(() => {
return (
'https://thisorthatstock.herokuapp.com/multipleStockPerformanceFilteredByYear?shortYearNum=' +
values.sharpeYear1 +
'&LongYearNum=' +
values.sharpeYear2 +
'&sharpeRatio1=' +
values.sharperatio1Cutoff +
'&sharpeRatio2=' +
values.sharperatio2Cutoff
);
}, [values]);
By the way, event could be null in this callback, destructure it so it's assigned to a variable or use event.persist().
setValues((values) => ({
...values,
[propertyName]: event.target.value
}));
I am new to react and I am making a simple todo app using react js and material ui. I have separate components to take in user input (TodoInput.js) which sends props to a component that renders individual todo tasks and displays a checkbox (TodoCards.js). What I want to do is display the total number of completed tasks onto the page which is updated when the user completes a todo by checking a checkbox. To achieve this, I have an array that stores all the user's completed tasks. At the moment whenever a checkbox is checked, all tasks are added to this array. I ran into a problem where I am unsure of how to only push values into this new array when the checkbox of that specific task is checked. Any guidance or explanations towards the right direction is greatly appreciated.
TodoInput.js
import React, { useState } from 'react';
import { makeStyles } from '#material-ui/core/styles';
import { TextField, Button } from '#material-ui/core';
import { TodoCards } from '../UI/TodoCards';
import { Progress } from '../UI/Progress';
const useStyles = makeStyles((theme) => ({
root: {
'& > *': {
margin: theme.spacing(1),
width: '25ch',
textAlign: 'center'
},
},
}));
export default function TodoInput() {
const classes = useStyles();
const [userInput, setUserInput] = useState({
id: '',
task: ''
});
const [todos, setTodos] = useState([])
//state for error
const [error, setError] = useState({
errorMessage: '',
error: false
})
//add the user todo with the button
const submitUserInput = (e) => {
e.preventDefault();
//add the user input to array
//task is undefined
if (userInput.task === "") {
//render visual warning for text input
setError({ errorMessage: 'Cannot be blank', error: true })
console.log('null')
} else {
setTodos([...todos, userInput])
console.log(todos)
setError({ errorMessage: '', error: false })
}
console.log(loadedTodos)
}
//set the todo card to the user input
const handleUserInput = function (e) {
//make a new todo object
setUserInput({
...userInput,
id: Math.random() * 100,
task: e.target.value
})
//setUserInput(e.target.value)
//console.log(userInput)
}
const loadedTodos = [];
for (const key in todos) {
loadedTodos.push({
id: Math.random() * 100,
taskName: todos[key].task
})
}
return (
<div>
<Progress taskCount={loadedTodos.length} />
<form className={classes.root} noValidate autoComplete="off" onSubmit={submitUserInput}>
{error.error ? <TextField id="outlined-error-helper-text" label="Today's task" variant="outlined" type="text" onChange={handleUserInput} error={error.error} helperText={error.errorMessage} />
: <TextField id="outlined-basic" label="Today's task" variant="outlined" type="text" onChange={handleUserInput} />}
<Button variant="contained" color="primary" type="submit">Submit</Button>
{userInput && <TodoCards taskValue={todos} />}
</form>
</div>
);
}
TodoCards.js
import React, { useState } from 'react'
import { Card, CardContent, Typography, FormControlLabel, Checkbox } from '#material-ui/core';
import { CompletedTasks } from './CompletedTasks';
export const TodoCards = ({ taskValue }) => {
const [checked, setChecked] = useState(false);
//if checked, add the task value to the completed task array
const completedTasks = [];
const handleChecked = (e) => {
setChecked(e.target.checked)
for (const key in taskValue) {
completedTasks.push(taskValue[key])
}
console.log(completedTasks.length)
}
return (
< div >
<CompletedTasks completed={completedTasks.length} />
<Card>
{taskValue.map((individual, i) => {
return (
<CardContent key={i}>
<Typography variant="body1">
<FormControlLabel
control={
<Checkbox
color="primary"
checked={checked[i]}
onClick={handleChecked}
/>
}
label={individual.task} />
</Typography>
</CardContent>
)
})}
</Card>
</div >
)
}
CompletedTasks.js (displays the total number of completed tasks)
import React from 'react'
import InsertEmoticonOutlinedIcon from '#material-ui/icons/InsertEmoticonOutlined';
import { Typography } from '#material-ui/core';
import { makeStyles } from '#material-ui/core/styles';
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
},
paper: {
padding: theme.spacing(2),
marginTop: '20px',
textAlign: 'center',
color: theme.palette.text.secondary,
},
}));
export const CompletedTasks = ({ completed }) => {
const classes = useStyles();
return (
<div className={classes.root}>
<InsertEmoticonOutlinedIcon fontSize="large" />
<Typography variant="h6">
Completed tasks:{completed}
</Typography>
</div>
)
}
One issue I see here is that you start with a boolean type checked state in TodoCards, and only ever store a single boolean value of the last checkbox interacted with. There's no way to get a count or to track what's previously been checked.
Use an object to hold the completed "done" checked values, then count the number of values that are checked (i.e. true) after each state update and rerender. Use the task's id as the key in the checked state.
export const TodoCards = ({ taskValue = [] }) => {
const [checked, setChecked] = useState({});
const handleChecked = id => e => {
const { checked } = e.target;
setChecked((values) => ({
...values,
[id]: checked
}));
};
return (
<div>
<CompletedTasks
completed={Object.values(checked).filter(Boolean).length}
/>
<Card>
{taskValue.map(({ id, task }) => {
return (
<CardContent key={id}>
<Typography variant="body1">
<FormControlLabel
control={
<Checkbox
color="primary"
checked={checked[id]}
onClick={handleChecked(id)}
/>
}
label={task}
/>
</Typography>
</CardContent>
)
})}
</Card>
</div >
)
}
To do this, you first need to only push the checked key to your array:
First send your key to the eventHandler:
{taskValue.map((individual, i) => {
return (
<CardContent key={i}>
<Typography variant="body1">
<FormControlLabel
control={
<Checkbox
color="primary"
checked={checked[i]}
onClick={() => handleChecked(individual)}
/>
}
label={individual.task} />
</Typography>
</CardContent>
)
})}
Then in your Handler, push it into the array:
const handleChecked = (key) => {
//setChecked(e.target.checked)
completedTasks.push(key)
console.log(completedTasks.length)
}
BUT
Because you are not modifying any state so the changes won't be updated to the UI, you need to use a state to store your completedTasks.
const [completedTasks, setCompletedTasks] = useState([]);
const handleChecked = (key) => {
setCompletedTasks([...completedTasks, key])
}
Please note that this is only a guide so you can get to the right way, not a complete working example
In the hopes that someone else may find this useful, I was able to come up with a solution thanks to the suggestions given. Below is the updated code for the TodoCards.js component:
import React, { useState } from 'react'
import { Card, CardContent, Typography, FormControlLabel, Checkbox } from '#material-ui/core';
import { CompletedTasks } from './CompletedTasks';
export const TodoCards = ({ taskValue }) => {
const [checked, setChecked] = useState(false);
//if checked, add the task value to the completed task array
const [completedTasks, setCompletedTasks] = useState([]);
const handleChecked = key => {
setCompletedTasks([...completedTasks, key])
completedTasks.push(key)
console.log(completedTasks.length)
setChecked(true)
};
if (taskValue.length === completedTasks.length) {
console.log('all tasks complete')
}
return (
<div>
<CompletedTasks completed={completedTasks.length} />
<Card>
{taskValue.map((individual, i) => {
return (
<CardContent key={i}>
<Typography variant="body1">
<FormControlLabel
control={
<Checkbox
color="primary"
checked={checked[i]}
onClick={() => handleChecked(individual)}
/>
}
label={individual.task}
/>
</Typography>
</CardContent>
)
})}
</Card>
</div >
)
}
Only the checked todo items are pushed into the new array (completedTasks) and this is updated using useState.
This works but the when i console log the object it gives
{ "week":undefined, "name":undefined, "code":undefined }
Moreover does wrapping all the material ui component in form tag and treating the whole code as a form, is it appropriate?
here is my code:
const ExamSimulatorForm = () => {
const weekNumber = useRef();
const examSub = useRef();
const examCode = useRef();
const handleSubmit = (event) =>{
event.preventDefault()
const week = weekNumber.current.value
const subject = examSub.current.value
const code = examCode.current.value
const examSimulatorPayload = {
week:week,
subject:subject,
code:code
}
console.log(examSimulatorPayload)
}
const [code, setCode] = useState('Quiz');
const [examSubject, setExamSubject] = useState('');
const [field, setField] = useState(1)
const handleESubjectChange = (event) => {
setExamSubject(event.target.value);
};
const handleCode = (event) => {
setCode(event.target.value);
};
return (
<form >
<CardActions onSubmit={handleSubmit}>
<Grid container spacing={2} justifyContent='center' alignItems='center' direction='column'>
<Grid item>
<TextField
InputProps={{
inputProps: {
max: 12, min: 1
}
}}
label='Week'
type='number'
onChange={(event)=>setField(parseInt(event.target.value))}
style={{minWidth:250}}
ref = {weekNumber}
required
/>
</Grid>
<Grid item>
<FormControl style={{minWidth:250}}>
<InputLabel>Subject</InputLabel>
<Select
value={examSubject}
onChange={handleESubjectChange}
ref={examSub}
required
>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item>
<FormControl style={{minWidth:250}}>
<InputLabel id="exam-code" >Exam Code</InputLabel>
<Select
labelId="exam-code"
id="exam-code-select"
value={code}
onChange={handleCode}
ref={examCode}
required
>
<MenuItem value={'Q'}>Q</MenuItem>
<MenuItem value={'M'}>M</MenuItem>
<MenuItem value={'F'}>F</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item>
<Button variant='contained' color='primary' style={{marginTop:94}} >Take Exam</Button>
</Grid>
</Grid>
</CardActions>
</form>
)
}
export default ExamSimulatorForm;
I have a similar form for attendance simulation, attendance dataset generation and exam dataset generation
I think this is good for you.
Please try this.
const ExamSimulatorForm = () => {
const [state, setState] = useState({
code: 'Quiz',
subject: '',
week: 1
});
const handleSubmit = (event) =>{
event.preventDefault()
const examSimulatorPayload = state;
console.log(examSimulatorPayload)
}
const handleChange = (evt, name) {
const { value } = evt.target;
setState({
...state,
[name]: value
});
}
return (
<form onSubmit={handleSubmit}>
<CardActions>
<Grid container spacing={2} justifyContent='center'
alignItems='center' direction='column'>
<Grid item>
<TextField
InputProps={{
inputProps: {
max: 12, min: 1
}
}}
label='Week'
type='number'
value={state.week}
onChange={(event)=>handleChange( event, "week")}
style={{minWidth:250}}
required
/>
</Grid>
<Grid item>
<FormControl style={{minWidth:250}}>
<InputLabel>Subject</InputLabel>
<Select
value={state.subject}
onChange={(event)=>handleChange( event, "subject")}
required
>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item>
<FormControl style={{minWidth:250}}>
<InputLabel id="exam-code" >Exam Code</InputLabel>
<Select
labelId="exam-code"
id="exam-code-select"
value={state.code}
onChange={(event)=>handleChange( event, "code")}
required
>
<MenuItem value={'Q'}>Q</MenuItem>
<MenuItem value={'M'}>M</MenuItem>
<MenuItem value={'F'}>F</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item>
<Button variant='contained' color='primary' style=
{{marginTop:94}} >Take Exam</Button>
</Grid>
</Grid>
</CardActions>
</form>
)
}
export default ExamSimulatorForm;
Please check it above code, and let me know your idea.
State updates in react are asynchronus, which means they don't occur as soon as you call them. You have to wait until the state updates to console.log because otherwise, nothing has changed and you are getting the initial value. You could do something like this:
useEffect(() => {
const subject = examSub.current.value
console.log(subject)
}, [examSubject])
useEffect() executes an action every time a state changes. Notice how at the end there is an array, with [examSubject]. This signifies the state that will trigger the effect. So when examSubject changes (when you assign a value to it), the effect will execute (in this case, it will log the subject).
When writing code, at least in my case, you don't need to worry about this. You can chnage the state and write your code as normal, but react might take a second or two to update the state. The only time I really notice this is when I console.log.
You can define the state for your inputs :
const [code, setCode] = React.useState('');
const [subject, setSubject] = React.useState('');
const [week, setWeek] = React.useState('');
const setExamCode = event => {
setCode(event.target.value);
};
const setSubject = event => {
setSubject(event.target.value);
};
const setWeek = event => {
setWeek(event.target.value);
};
Then call these methods from onChange event like : onChange={setExamCode} or {setSubject}
In case you want to handle data from single event and defining it's initial state try below approach :
const initialData = Object.freeze({
code: "",
subject: "",
week: ""
});
const [data, updateData] = React.useState(initialData );
const handleChange = (e) => {
updateData({
...data,
[e.target.name]: e.target.value
});
};
const handleSubmit = (e) => {
e.preventDefault()
console.log(data);
};
Then call these methods from onChange event like onChange={handleSubmit}
I am new to Reactjs
I am trying to build an address form with 3 Select ( country, State, City )
I used React hock
so when the page first load it will fetch countries list to country select after that when user select country it will fetch states list to state select and after that when user select state it will fetch cities list to city select
my issue with state hock I store the value of the user selected in the state but it did not update the value in the state in many locations I keep getting " undefined " like when the page load I get countries list as an array and I get the first country in the list as the default select item in country select but I still get keep getting " undefined " I tried many ways but still getting the same result and bellow is my code
import React,{ useEffect , useState , useCallback } from 'react';
import { InputLabel, Select , MenuItem , Grid , Typography } from '#material-ui/core';
import { useForm, FormProvider } from 'react-hook-form';
import CityHandler from 'countrycitystatejson';
export const TempAddresForm = () =>
{
const methods = useForm();
const [countries, setCountries] = useState([]);
const [countryCode, setCountryCode] = useState('');
const [country, setCountry] = useState('');
const [states, setStates] = useState([]);
const [stateName, setStateName] = useState('');
const [cities, setCities] = useState([]);
const [city, setCity] = useState('');
const fetchCounters = () => {
setCountries(CityHandler.getCountries());
setFirstCountry();
};
const countryChangeHandler = (event) => {
let tempCountryCode = event.target.value;
setCountry(CityHandler.getCountryByShort(tempCountryCode));
setCountryCode(tempCountryCode);
fetchStates(tempCountryCode);
setCities([]);
}
const fetchStates = (countryCode) =>
{
setStates(CityHandler.getStatesByShort(countryCode));
}
const stateChangeHandler = (even) =>
{
let tempState = even.target.value;
setStateName(even.target.value);
fetchCities(tempState);
}
const fetchCities = (stateName) => {
let tempCities = CityHandler.getCities(countryCode, stateName);
setCities(tempCities);
};
const cityChangeHandler = (event) =>
{
let tempCity = event.target.value;
setCity(tempCity);
console.log("Temp City Name : " + tempCity)
console.log("City Name : " + city)
}
const setFirstCountry = useCallback( () =>
{
if(countries)
{
let firstCountry = CityHandler.getCountries()[0];
console.log ("[setFirstCountry] : First Country " + JSON.stringify(firstCountry.name));
setCountry(firstCountry);
console.log ("[setFirstCountry] : Country name " + JSON.stringify(country.name));
}
}, []);
useEffect(() => {
fetchCounters();
//setFirstCountry();
}, []);
return (
<>
<Typography variant="h6" gutterBottom>Shipping Address</Typography>
<FormProvider {...methods}>
<form onSubmit={''}>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<InputLabel>Country</InputLabel>
<Select value={country.name} fullWidth onChange={(event) => {countryChangeHandler(event)}}>
{countries.map((countryLoop) => (
<MenuItem key={countryLoop.shortName} id={countryLoop.shortName} value={countryLoop.shortName}>
{countryLoop.name}
</MenuItem>
))}
</Select>
</Grid>
<Grid item xs={12} sm={6}>
<InputLabel>State</InputLabel>
<Select value={stateName} fullWidth onChange={(event) => {stateChangeHandler(event)}}>
{states.map((state, index) => {
return(
<MenuItem key={index} id={state} value={state}>
{state}
</MenuItem>
);
})}
</Select>
</Grid>
<Grid item xs={12} sm={6}>
<InputLabel>City</InputLabel>
<Select value={city} fullWidth onChange={(event) => {cityChangeHandler(event)}}>
{cities.map((city, index) => {
return(
<MenuItem key={index} id={city} value={city}>
{city}
</MenuItem>
);
})}
</Select>
</Grid>
</Grid>
</form>
</FormProvider>
</>
)
}
export default TempAddresForm;
if anyone could help me with this
*Note: I use this package to get the Country list countrycitystatejson
country is actually always updating but you are logging it in a useCallback hook and did not add country to its dependency array. So it only captures the initial value of country which is an empty string "" and JSON.stringify("".name) is undefined. If you add country to the dependency array of useCallback you will see it updating.
console.log( JSON.stringify("".name) )
You don't need to use useCallback here. Read this article to understand where and when to use useCallback and useMemo
The main problem is that you are mapping your country Select to country.name but your select options have country.shortName as their value - Try changing the Select value to country.shortName.
Also, you have too many state variables that are interdependent to each other. Here moving all your state variables to a single state object will make it a little bit easier to handle.
Ex Like below
{
countries: [...],
states: [...],
cities: [...],
stateName: "..",
...
...
}
countries is always constant & states, cities are just derived values. So your actual state just needs these 3 values countryShortCode, stateName and city.
Here is a snippet with all the above-mentioned changes
import React, { useState } from "react";
import {
InputLabel,
Select,
MenuItem,
Grid,
Typography
} from "#material-ui/core";
import { useForm, FormProvider } from "react-hook-form";
import CityHandler from "countrycitystatejson";
// This countriesList doesn't change so it can just be a constant outside your component
const countriesList = CityHandler.getCountries();
// Your initial state
const initialState = {
countryShortName: countriesList[0].shortName,
stateName: "",
city: ""
};
const TempAddresForm = () => {
const methods = useForm();
const [state, setState] = useState(initialState);
const changeCountry = (e) => {
setState((prevState) => ({
...prevState,
countryShortName: e.target.value,
stateName: "",
city: ""
}));
};
const changeState = (e) => {
setState((prevState) => ({
...prevState,
stateName: e.target.value,
city: ""
}));
};
const changeCity = (e) => {
setState((prevState) => ({
...prevState,
city: e.target.value
}));
};
// Derive both states and cities here ( No need to store it in state :) )
const states = CityHandler.getStatesByShort(state.countryShortName);
const cities = state.stateName
? CityHandler.getCities(state.countryShortName, state.stateName)
: [];
return (
<>
<Typography variant="h6" gutterBottom>
Shipping Address
</Typography>
<FormProvider {...methods}>
<form onSubmit={() => console.log("Submitted")}>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<InputLabel>Country</InputLabel>
<Select
value={state.countryShortName || ""}
fullWidth
onChange={changeCountry}
>
{countriesList.map((countryLoop) => (
<MenuItem
key={countryLoop.shortName}
id={countryLoop.shortName}
value={countryLoop.shortName}
>
{countryLoop.name}
</MenuItem>
))}
</Select>
</Grid>
<Grid item xs={12} sm={6}>
<InputLabel>State</InputLabel>
<Select value={state.stateName} fullWidth onChange={changeState}>
{states.map((state, index) => {
return (
<MenuItem key={index} id={state} value={state}>
{state}
</MenuItem>
);
})}
</Select>
</Grid>
<Grid item xs={12} sm={6}>
<InputLabel>City</InputLabel>
<Select value={state.city || ""} fullWidth onChange={changeCity}>
{cities.map((city, index) => {
return (
<MenuItem key={index} id={city} value={city}>
{city}
</MenuItem>
);
})}
</Select>
</Grid>
</Grid>
</form>
</FormProvider>
</>
);
};
export default TempAddresForm;
Comment if you need to understand anything else
Hello iI have a project in react + graphql + node and mongod, and iI tried to upload images to a folder in my app and i have a problem with the reload of app after the file save. I tried to manage a local state in the component when performing the upload process, but my local variable changed to the initial state because of a reload app, and cant it does not show a preview of my file. My goal is show a preview of img upload.
I think my problem is in the resolver, in the function storeUpload
Code.
API-----------Resolver------
const Product = require('../models/Product');
const { createWriteStream } = require("fs");
async function createProduct(root, args) {
console.log(args.file)
let product = await new Product(args)
console.log(product);
product.save()
if(args.file){
const { stream, filename, mimetype, encoding } = await args.file;
await storeUpload({ stream, product });
}
return product
}
const storeUpload = ({ stream, product }) =>
new Promise((resolve, reject) =>
stream
.pipe(createWriteStream("./images/" + product._id +".png"))
.on("finish", () => resolve())
.on("error", reject)
);
module.exports = {
createProduct
}
---React--- Component----
import React , { useState, useEffect } from 'react'
import {Grid} from '#material-ui/core'
import { Mutation } from "react-apollo"
import {PRODUCTOS} from './Productos'
import gql from "graphql-tag";
import {StyleStateConsumer} from '../../../Context/StylesStateContext'
/** #jsx jsx */
import { jsx, css } from '#emotion/core'
const borde = css({
borderStyle: 'solid'
})
const contendorForm = css({
borderRadius: 15,
backgroundColor: '#ffffe6'
},borde)
const itemsForm = css({
padding: 15
})
const inputStyle = css({
borderRadius: 5,
padding: 3
})
const ADD_PRODUCT = gql`
mutation CreateProduct($title: String, $price: Float, $file: Upload) {
createProduct(title: $title, price: $price, file: $file) {
_id
title
}
}
`;
const CrearProducto = props =>{
const [titleInput, settitleInput] = useState('')
const [priceInput, setpriceInput] = useState(0)
const [fileInput, setfileInput] = useState('')
const [imgName, setimgName] = useState('default')
const handleChange = e =>{
const {name, type, value} = e.target
if(name === 'titleInput'){settitleInput(value)}
if(name === 'priceInput'){setpriceInput(parseFloat(value))}
}
const imgUpdate = (imgConxt) => {
setimgName(imgConxt)
}
useEffect( imgUpdate )
const handleFile = (obj) =>{console.log(obj)}
return(
<StyleStateConsumer>
{({imagenID, updateHState})=>(
<Grid container css={borde}
direction="row"
justify="center"
alignItems="center">
<Grid item css={contendorForm}>
{imgUpdate(imagenID)}
<Grid container
direction="row"
justify="center"
alignItems="center"><Grid item >Crear Producto:</Grid>
</Grid>
<Grid container>
<Mutation mutation={ADD_PRODUCT} refetchQueries={[{
query: PRODUCTOS
}]}>
{(createProduct, { data, loading })=>{
if (loading) return <div>Loading...</div>
console.log(data)
return(
<div>
<form css={itemsForm}
onSubmit={ (e) => {
e.preventDefault()
createProduct({
variables: {
title: titleInput,
price: priceInput,
file: fileInput
}
})
//
updateHState(res.data.createProduct._id
=== undefined ? '' :
res.data.createProduct._id)
settitleInput('')
setpriceInput(0)
//
console.log(res.data.createProduct._id)
}}>
<Grid item>
<input
css={inputStyle}
type="file"
onChange={({
target: { validity, files:
[file] } }) => validity.valid &&
setfileInput(file)
}
/>
</Grid>
<Grid item>
<label>
Titulo:
</label>
</Grid>
<Grid item>
<input css={inputStyle} type="text"
name="titleInput"
placeholder="Ingresar TÃtulo"
value={titleInput}
onChange={handleChange}
/>
</Grid>
<Grid item>
<label>
Precio:
</label>
</Grid>
<Grid item>
<input css={inputStyle} type="number"
name="priceInput"
placeholder="Ingresar Precio"
value={priceInput}
onChange={handleChange}/>
</Grid>
<Grid item>
<input type="submit" name="Crear"/>
</Grid>
</form>
</div>
)
}}
</Mutation>
</Grid>
</Grid>
<Grid item css={[borde,
{
backgroundImage: `url('${require('../../../../../back-
end/images/' + imgName + '.png')}')`,
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
height: 300,
width: 300}]}>
</Grid>
</Grid>
)}
</StyleStateConsumer>
)
}
export default CrearProducto
Well, first the imgUpdate function you're passing to useEffect won't ever get its imgConxt parameter. This hook just calls an empty-parameter function to execute a bunch of code after render.
For your main problem, do you have any warnings or errors in the console ?
One quick but dirty fix would be to change your <input type='submit'> to type button and do your logic in an onClick handler there.