state blank on first click - reactjs

I am trying to do a next button which will find the next vehicle in an array and display it.
However I cannot figure out why when I click on the next button the first time my console.log(nextVin) shows blank. On the second click it shows fine.
What is my best way to resolve this?
Please see my code below:
import React, {useState} from "react";
import { useParams } from "react-router-dom";
import { Link } from 'react-router-dom';
import {Typography, Container, Paper, Button} from "#material-ui/core/";
import ArrowBackIosIcon from '#material-ui/icons/ArrowBackIos';
import ArrowForwardIosIcon from '#material-ui/icons/ArrowForwardIos';
const CarDetail = (props) => {
let {id} = useParams();
const [nextVin, setNextVin] = useState("");
const nextCar = () => {
const carIndex = props.cars.findIndex(car => car.vin===id)
props.cars.map((car, index) => {
if(index === carIndex +1){
setNextVin(car.vin);
}
})
console.log(nextVin);
}
return (
<div>
<div>
<Typography color="primary" variant ="h3">Car details</Typography>
{props.cars.map((car, index) => {
if(car.vin === id){
return(
<div key = {car.vin}>
<Container maxWidth="sm" >
<Paper elevation={3} spacing={14} >
<Typography color="primary" variant ="h5">Car model</Typography>
<span>{car.model_variant}</span>
<Typography color="primary" variant ="h5">Fuel type</Typography>
<span>{car.fuel_type}</span>
<Typography color="primary" variant ="h5">Engine Size</Typography>
<span>{car.engine_size}</span>
<Typography color="primary" variant ="h5">Body type</Typography>
<span>{car.body_type}</span>
<Typography color="primary" variant ="h5">Reg number</Typography>
<span>{car.regno}</span>
<Typography color="primary" variant ="h5">Doors</Typography>
<span>{car.doors}</span>
<Typography color="primary" variant ="h5">Transmission type</Typography>
<span>{car.transmission_type}</span>
<Typography color="primary" variant ="h5">VIN</Typography>
<span>{car.vin}</span>
</Paper>
</Container>
{index!==0 ? <Button><ArrowBackIosIcon/> Previous car</Button> : null}
<Link
to={`/joes-garage/`}>
<Button variant ="contained" color="primary">Return to car list</Button>
</Link>
{index !== props.cars.length-1 ?
<Button onClick={nextCar}>Next car <ArrowForwardIosIcon/></Button>
:null }
</div>
);
}else return false;
})}
</div>
</div>
);
};
export default CarDetail;

try this
const nextCar = (props) => {
let {id} = useParams();
const [nextVin, setNextVin] = useState("");
const [lock , setLock] = useState("true")
const carIndex = props.cars.findIndex(car => car.vin===id)
props.cars.map((car, index) => {
if(index === carIndex +1)
if(!lock){
setNextVin(car.vin);
}else{
//set blanck at first render
setNextVin('');
setLock(false);
}
})
console.log(nextVin);
}

Related

Pass parameter to context menu

I am currently studying Material UI 5 and specifically Context Menu subject - https://mui.com/material-ui/react-menu/#context-menu.
Can someone explain me why the handleClose function always print the last index in the array - 4?
import * as React from "react"
import Menu from "#mui/material/Menu"
import MenuItem from "#mui/material/MenuItem"
import Typography from "#mui/material/Typography"
export default function ContextMenu() {
const [contextMenu, setContextMenu] = React.useState(null)
const handleContextMenu = (event) => {
event.preventDefault()
setContextMenu(
contextMenu === null
? {
mouseX: event.clientX + 2,
mouseY: event.clientY - 6,
}
: null
)
}
const handleClose = (i) => {
setContextMenu(null)
console.log(i)
}
return (
<>
{["a", "b", "c", "d", "e"].map((item, index) => (
<div
key={index}
onContextMenu={handleContextMenu}
style={{ cursor: "context-menu" }}
>
<Typography variant='h6' component='h1' sx={{ padding: 4 }}>
{item}
</Typography>
<Menu
open={contextMenu !== null}
onClose={handleClose}
anchorReference='anchorPosition'
anchorPosition={
contextMenu !== null
? { top: contextMenu.mouseY, left: contextMenu.mouseX }
: undefined
}
>
<MenuItem onClick={() => handleClose(index)}>one</MenuItem>
<MenuItem onClick={() => handleClose(index)}>two</MenuItem>
<MenuItem onClick={() => handleClose(index)}>three</MenuItem>
</Menu>
</div>
))}
</>
)
}
The way you currently have the on clicks setup is that it accesses the value index has at the time you click it, not the value it had when the function was created.
Change
onClick={() => handleClose(index)}
To
onClick={((index) => () => handleClose(index))(index)}
check the following code and see how you should manage user's options :
import * as React from "react";
import Menu from "#mui/material/Menu";
import MenuItem from "#mui/material/MenuItem";
import Typography from "#mui/material/Typography";
export default function ContextMenu() {
const initialState = {
element: null,
item: null
};
const [state, setState] = React.useState(initialState);
const handleListItemClick = (element, item) => {
setState({
element,
item
});
};
const handleMenuItemClick = (option) => {
setState(initialState);
switch (option) {
case "one":
console.log(state.item, "one");
break;
case "two":
console.log(state.item, "two");
break;
default:
break;
}
};
return (
<div>
{React.Children.toArray(
["a", "b", "c", "d", "e"].map((item) => (
<Typography
onClick={(e) => handleListItemClick(e.target, item)}
sx={{ margin: 4 }}
>
{item}
</Typography>
))
)}
<Menu open={state.element !== null} anchorEl={state.element}>
<MenuItem onClick={() => handleMenuItemClick("one")}>one</MenuItem>
<MenuItem onClick={() => handleMenuItemClick("two")}>two</MenuItem>
</Menu>
</div>
);
}
codesandbox: https://codesandbox.io/s/great-microservice-w5jh00

How to return to the same scroll position when going back using useNavigate() or useLocation() react-router

When clicking on a specific pokemon, the user can show details of the pokemon. When the user clicks the back button to see all pokemon, I want them to continue in the exact same scroll position/pokemon, where they first clicked. How to achieve this?
Here is the pokedex component, where the user can click on each pokemon:
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { ScrollArrow } from './utils/scrollArrow';
import { Container, Card, Col, Row, Spinner } from 'react-bootstrap';
const Pokedex = () => {
const [pokemon, setPokemon] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
getPokedex();
}, []);
const getPokedex = async () => {
try {
const res = await fetch('https://pokeapi.co/api/v2/pokemon?limit=151');
const { results } = await res.json();
const pokedex = results.map((pokemon: any, index: number) => {
const paddedId = ('00' + (index + 1)).slice(-3);
const image = `https://assets.pokemon.com/assets/cms2/img/pokedex/detail/${paddedId}.png`;
return { ...pokemon, image };
});
setPokemon(pokedex);
setLoading(false);
} catch (err) {
console.error(err);
}
};
return (
<Container fluid className='pokedex'>
{loading ? (
<Spinner animation='border' role='status'>
<span className='visually-hidden'>Fetching Pokemon...</span>
</Spinner>
) : (
<Row>
{pokemon.map((pokemon: any, index: number) => (
<Col key={index} xs={12} sm={6} lg={4} xl={2} className='col'>
<Card>
<Link to={`/pokemon/${index + 1}`}>
<Card.Img src={pokemon.image} alt={pokemon.name} />
<Card.Body>
<Card.Text>
#{(index + 1).toString().padStart(3, '0')}
</Card.Text>
<Card.Title>{pokemon.name}</Card.Title>
</Card.Body>
</Link>
</Card>
</Col>
))}
</Row>
)}
<ScrollArrow />
</Container>
);
};
export default Pokedex;
Here is the pokemon component, where the user can go back to the pokedex to see all pokemon:
import { useEffect, useState } from 'react';
import { colors } from './utils/bgColor';
import {
Button,
Col,
Container,
Image,
Row,
Spinner,
ListGroup,
ProgressBar,
Tab,
Tabs,
TabContainer,
} from 'react-bootstrap';
import { useNavigate, useParams } from 'react-router-dom';
const Pokemon = () => {
const [pokemonDetails, setPokemonDetails] = useState<any>([]);
const [loading, setLoading] = useState(true);
const { id } = useParams();
const getPokemon = async (id: string | undefined) => {
try {
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
const pokemon = await res.json();
const paddedId = ('00' + id).slice(-3);
pokemon.image = `https://assets.pokemon.com/assets/cms2/img/pokedex/detail/${paddedId}.png`;
setPokemonDetails(pokemon);
setLoading(false);
} catch (err) {
console.error(err);
}
};
useEffect(() => {
getPokemon(id);
}, [id]);
let navigate = useNavigate();
const handleClick = () => {
navigate('/');
};
let typeName = pokemonDetails.types && pokemonDetails.types[0].type.name;
const bgColor: string = colors[typeName];
return (
<Container fluid className='pokemon' style={{ backgroundColor: bgColor }}>
{loading ? (
<Spinner animation='border' role='status'>
<span className='visually-hidden'>Fetching Pokemon...</span>
</Spinner>
) : (
<div className='details' style={{ position: 'relative' }}>
<Row>
<Col className='header'>
<h1>{pokemonDetails.name}</h1>
<h3>#{pokemonDetails.id.toString().padStart(3, '0')}</h3>
</Col>
</Row>
<Row>
<Col>
<ListGroup className='type'>
{pokemonDetails.types.map((type: any, index: number) => (
<ListGroup.Item key={index}>{type.type.name}</ListGroup.Item>
))}
</ListGroup>
</Col>
</Row>
<Row>
<Image
src={pokemonDetails.image}
alt={pokemonDetails.name}
className='pokemon-img'
/>
</Row>
<TabContainer>
<Row className='clearfix'>
<Col sm={12} className='box'>
<Tabs defaultActiveKey='stats'>
<Tab eventKey='abilities' title='Abilities'>
<ListGroup>
{pokemonDetails.abilities.map(
(ability: any, index: number) => (
<ListGroup.Item key={index}>
{ability.ability.name}
</ListGroup.Item>
)
)}
</ListGroup>
</Tab>
<Tab eventKey='stats' title='Stats'>
<ListGroup>
{pokemonDetails.stats.map((stat: any, index: number) => (
<ListGroup.Item key={index}>
{stat.stat.name}
<ProgressBar
now={stat.base_stat}
label={stat.base_stat}
/>
</ListGroup.Item>
))}
</ListGroup>
</Tab>
<Tab eventKey='moves' title='Moves'>
<ListGroup className='moves'>
{pokemonDetails.moves
.slice(0, 62)
.map((move: any, index: number) => (
<ListGroup.Item key={index}>
{move.move.name}
</ListGroup.Item>
))}
</ListGroup>
</Tab>
<Tab eventKey='evolutions' title='Evolutions' disabled>
{/* <p className='possible evolution'>
{pokemonDetails.stats.map((type: any, index: number) => (
<p key={index}>{type.type.name}</p>
))}
</p> */}
</Tab>
</Tabs>
</Col>
</Row>
</TabContainer>
<Button variant='dark' onClick={handleClick}>
Catch another Pokémon
</Button>
</div>
)}
</Container>
);
};
export default Pokemon;
I found an answer in this thread:
How do people handle scroll restoration with react-router v4?
this is in v4 and TS but still have a good solution for this
Hopefully it will help you

multi step form with React , Formik and Material UI, issue with checkboxes and autocomplete

I hope you could help me with this part, so I have no problem with the stepper and no problem with the forms too when it comes to getting data input ​​and validating fields with yup .
but I have a problem with the checkboxes and autocomplete. when I go back to a previous step, I lose the values ​​I have already entered and validation no longer works with checkbox and autocomplete field.
There is an example of a hook that I made for the checkboxes that I will use later in a form :
const CheckBoxField = (props) => {
const { label, ...rest } = props;
const [field, meta, helper] = useField(props);
const { setValue } = helper;
const [touched, error] = at(meta, "touched", "error");
const isError = error && touched && true;
function __renderHelperText() {
if (isError) {
return <FormHelperText>{error}</FormHelperText>;
}
}
function _onChange(event) {
setValue(event.target.checked);
}
const configFormControl = {
...field,
...rest,
onChange: _onChange,
};
return (
<FormControl
component="fieldset"
{...configFormControl}
error={Boolean(isError)}
>
<FormControlLabel
// value={field.checked}
checked={field.checked}
label={label}
onChange={_onChange}
control={
<BpCheckbox
{...configFormControl}
// checked={field.checked}
color="primary"
/>
}
color="primary"
/>
{__renderHelperText()}
</FormControl>
);
};
And there is the code for validation :
import * as yup from "yup";
import signUpFormModel from "../signUpFormModel";
const {
formField: {
terms //Boolean , used in checkboxes, should be true
},
} = signUpFormModel;
const signUpValidationSchema = [
yup.object().shape({
[terms.name]: yup.boolean().oneOf([true], `${terms.requiredErrMsg}`),
//other fields ...
}),
//other forms ...
];
export default signUpValidationSchema;
My Initial value :
import signUpFormModel from "../signUpFormModel";
const {
formField: {
terms,
},
} = signUpFormModel;
const formInitialValue = {
[terms.name]: false,
};
export default formInitialValue;
and some props helper (used as model for my forms)
import React, { Fragment } from "react";
import { Link } from "#mui/material";
const signUpFormModel = {
formId: "registration",
formField: {
terms: {
type: "checkbox",
name: "terms",
id: "terms",
label: () => (
<Fragment>
J'accepte les{" "}
<Link href="#" variant="body2">
termes et conditions générales d'utilisation
</Link>{" "}
et la{" "}
<Link href="#" variant="body2">
politique de confidentialité
</Link>
.
</Fragment>
),
requiredErrMsg: "Vous devez accepter les termes et conditions",
},
},
};
export default signUpFormModel;
Finaly the form itself :
import React from "react";
import { Grid } from "#mui/material";
import CheckBoxField from "../CheckBoxField";
const PrimarySignUpForm = (props) => {
const {
formField: {
terms,
},
} = props;
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<CheckBoxField name={terms.name} label={terms.label()} />
</Grid>
</Grid>
);
};
export default PrimarySignUpForm;
And this is how I made the stepper :
import React, { Fragment, useState } from "react";
import SignUpFormLogs from "../forms/SignUpFormLogs";
import { Formik, Form } from "formik";
import formInitialValuefrom from "../formModel/formInitialValue";
import signUpFormModel from "../formModel/signUpFormModel";
import signUpValidationSchema from "../formModel/signUpValidationSchema";
import {
Button,
Link,
Step,
StepLabel,
Stepper,
Typography,
} from "#mui/material";
import { Box } from "#mui/system";
const steps = ["step1"];
const { formId, formField } = signUpFormModel;
function _renderSteps(step) {
switch (step) {
case "step1":
return <SignUpFormLogs formField={formField} />;
default:
return <div>Not Found</div>;
}
}
function _sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const SignUp = () => {
const [activeStep, setActiveStep] = useState(0);
const currentValidationSchema = signUpValidationSchema[activeStep]; //activeStep
const isLastStep = activeStep === steps.length - 1;
async function _submitForm(values, actions) {
await _sleep(1000);
console.log(JSON.stringify(values, null, 2));
actions.setSubmitting(false);
setActiveStep(activeStep + 1);
}
function _handleSubmit(values, actions) {
if (isLastStep) {
_submitForm(values, actions);
} else {
setActiveStep(activeStep + 1);
actions.setTouched({});
actions.setSubmitting(false);
}
}
function _handleBack() {
setActiveStep(activeStep - 1);
}
return (
<Fragment>
<Stepper activeStep={activeStep} sx={{ pt: 3, pb: 5 }}>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
{activeStep === steps.length ? (
<Typography variant="h6">Last step</Typography>
) : (
<Formik
initialValues={formInitialValuefrom}
validationSchema={currentValidationSchema}
onSubmit={_handleSubmit}
>
{({ isSubmitting }) => (
<Form id={formId}>
{_renderSteps(steps[activeStep])}
{activeStep !== 0 && (
<Button
onClick={_handleBack}
disabled={isSubmitting}
variant="outlined"
color="primary"
sx={{ mt: 4, mb: 2 }}
>
Back
</Button>
)}
<Button
type="submit"
disabled={isSubmitting}
variant="contained"
color="primary"
sx={{ mt: 4, mb: 2 }}
>
{isLastStep ? "Enregistrer" : "Suivant"}
</Button>
{isSubmitting && (
<Typography variant="h6">submitting...</Typography>
)}
</Form>
)}
</Formik>
)}
</Fragment>
);
};
export default SignUp;

How to unit test stateless material-ui components? functions are not accessible from the outside

In some of the material-ui examples they have internal functions inside the functional component. Those are unaccessible for unit testing, am I losing something?
What I've could done is just either convert it into a class component or extract those functions outside and bind the params inside...
IE: https://material-ui.com/components/steppers/
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import Stepper from '#material-ui/core/Stepper';
import Step from '#material-ui/core/Step';
import StepLabel from '#material-ui/core/StepLabel';
import Button from '#material-ui/core/Button';
import Typography from '#material-ui/core/Typography';
const useStyles = makeStyles(theme => ({
...
}));
function getSteps() {
return ['Select campaign settings', 'Create an ad group', 'Create an ad'];
}
function getStepContent(step) {
switch (step) {
case 0:
return 'Select campaign settings...';
case 1:
return 'What is an ad group anyways?';
case 2:
return 'This is the bit I really care about!';
default:
return 'Unknown step';
}
}
export default function HorizontalLinearStepper() {
const classes = useStyles();
const [activeStep, setActiveStep] = React.useState(0);
const [skipped, setSkipped] = React.useState(new Set());
const steps = getSteps();
function isStepOptional(step) {
return step === 1;
}
function isStepSkipped(step) {
return skipped.has(step);
}
function handleNext() {
let newSkipped = skipped;
if (isStepSkipped(activeStep)) {
newSkipped = new Set(newSkipped.values());
newSkipped.delete(activeStep);
}
setActiveStep(prevActiveStep => prevActiveStep + 1);
setSkipped(newSkipped);
}
...
return (
<div className={classes.root}>
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (isStepOptional(index)) {
labelProps.optional = <Typography variant="caption">Optional</Typography>;
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={label} {...stepProps}>
<StepLabel {...labelProps}>{label}</StepLabel>
</Step>
);
})}
</Stepper>
<div>
{activeStep === steps.length ? (
<div>
<Typography className={classes.instructions}>
All steps completed - you&apos;re finished
</Typography>
<Button onClick={handleReset} className={classes.button}>
Reset
</Button>
</div>
) : (
<div>
<Typography className={classes.instructions}>{getStepContent(activeStep)}</Typography>
<div>
<Button disabled={activeStep === 0} onClick={handleBack} className={classes.button}>
Back
</Button>
{isStepOptional(activeStep) && (
<Button
variant="contained"
color="primary"
onClick={handleSkip}
className={classes.button}
>
Skip
</Button>
)}
<Button
variant="contained"
color="primary"
onClick={handleNext}
className={classes.button}
>
{activeStep === steps.length - 1 ? 'Finish' : 'Next'}
</Button>
</div>
</div>
)}
</div>
</div>
);
}
in this example functions like isStepOptional, isStepSkipped,
handleNext are not accessible from the outside of the component
from a test file would be necessary to access those functions in order to unit-test them.
And another good question would be if those functions are not re-defining on every prop change of that component...
Isn't it a little bit anti-pattern?

If/Else in .map function react

i have a question . I have to build an app where people can search for music from "lastFm" . So far so good , i already made few things to works normal , but i have a problem with if/else in map function , i've try to show user "no result found" if there are any , but with no luck .If there is 1+ results , will be displayed on the screen , but if there are any , nothing happen . Here is my code .
import React, { Component } from 'react';
import AppBar from '#material-ui/core/AppBar';
import Toolbar from '#material-ui/core/Toolbar';
import Typography from '#material-ui/core/Typography';
import {
TextField,
Button,
List,
ListItem,
ListItemAvatar,
ListItemText,
Avatar,
Card,
CardContent
} from '#material-ui/core';
import axios from 'axios';
import './App.css';
const API_URL = 'http://ws.audioscrobbler.com/2.0/?limit=5&format=json&method=artist.search&api_key=' + process.env.REACT_APP_LASTFM_APPKEY;
const isEmpty = (str) => str.length === 0;
class App extends Component {
state = {
searchTerm: '',
savedArtists: []
}
componentDidMount() {
const existing = localStorage.getItem('savedArtists')
if (existing) {
this.setState({ savedArtists: JSON.parse(existing) })
}
}
onTextChange = (event) => {
const value = event.target.value;
this.setState({ searchTerm: value });
}
search = (terms) => {
const request = API_URL + '&artist=' + terms;
axios.get(request).then((response) => {
const results = response.data.results;
const artists = results.artistmatches.artist.map((artist) => {
const avatarImage = artist.image.find(image => image.size === 'medium');
const imageUrl = avatarImage['#text'];
return { ...artist, avatar: imageUrl }
});
this.setState({ artists });
})
}
onSearchClick = () => {
this.search(this.state.searchTerm);
}
clearSearch = () => {
this.setState({
searchTerm: '',
artists: []
})
}
onResultClick = (artist) => {
this.clearSearch();
const savedArtists = this.state.savedArtists;
savedArtists.push(artist)
this.setState({ savedArtists: savedArtists })
localStorage.setItem('savedArtists', JSON.stringify(savedArtists));
}
render() {
const results = this.state.artists || [];
return (
<div className="App">
<header className="App-header">
<AppBar position="static" color="primary">
<Toolbar className="search-bar">
<Typography variant="h6" color="inherit">
Photos
</Typography>
<TextField
placeholder="Search on Last.fm"
className="search-input"
onChange={this.onTextChange}
value={this.state.searchTerm}
/>
<Button
onClick={this.onSearchClick}
variant="contained"
color="secondary"
disabled={isEmpty(this.state.searchTerm)}
>
Search
</Button>
{!isEmpty(this.state.searchTerm) && (
<Button
onClick={this.clearSearch}
variant="contained"
>
Clear
</Button>)
}
</Toolbar>
</AppBar>
</header>
//****Here is where i've try to use if/else
<List className="search-results">
{
results.map((artist ,results) => {
if(results.length === 0)
return (<ListItem> Not Found</ListItem>
); else {
return ( <ListItem
button
key={artist.name}
className="result"
onClick={() => this.onResultClick(artist)}
>
<ListItemAvatar>
<Avatar src={artist.avatar} alt={artist.name} />
</ListItemAvatar>
<ListItemText primary={artist.name} />
<Button
variant="outlined"
color="secondary"
size="small"
className="add-button"
>
Add to favorites
</Button>
</ListItem>);
}
})
}
</List>
<div className="artist-container">
{
this.state.savedArtists.map((artist, i) => {
return (
<Card className="artist-card"
key={i}
>
<CardContent>
{artist.name}
</CardContent>
</Card>
)
})
}
</div>
</div>
);
}
}
export default App;
You're having an error there. It's .map(result: any, index: number, original: []), so you're referring to an index number with argument results:
results.map((artist, results) => {
if(results.length === 0) { ... }
});
So fix it just by not referring to results as a argument of .map
The problem is that you're trying to do an if/else in the map of the array. But if the array has no items, then there is nothing to map.
What to do is to use a ternary to check if the array has any results:
{ results && result.length ?
<List className="search-results">
{
results.map((artist) => {
return (
<ListItem button key={artist.name} className="result" onClick={() => this.onResultClick(artist)} >
<ListItemAvatar>
<Avatar src={artist.avatar} alt={artist.name} />
</ListItemAvatar>
<ListItemText primary={artist.name} />
<Button
variant="outlined"
color="secondary"
size="small"
className="add-button"
>
Add to favorites
</Button>
</ListItem>
);
})
}
</List>
: <div>No Results</div>
}
Here, we're checking if results.length is considered truthy or not, if it's 1 or higher, then it will render your list, otherwise it will render our div informing the user there is no results, which you can change out to be whatever you want.

Resources