I'm working in react, and for one part of the project I need x amount of dropdown menus. Each dropdown menu will have y amount of options to choose from.
Usually with things like this, I can just use a simple map function and I am good to go, however the syntax gets a little tricky since one DropdownMenu has many Dropdown.Items. My code looks like this, no errors pop up and the console.log statements return exactly what is to be expected, but absolutely nothing renders.
const renderDefaultScheduling = () => {
return allDevices.map( (device, superIndex) => {
<Card>
{renderAllOfThisDevice(device, superIndex)}
</Card>
})
}
const renderAllOfThisDevice = (deviceObj, superIndex) => {
let oneDeviceDropdowns = []
console.log(deviceObj)
for (let i = 1; i <= deviceObj.amount; i = i + 1){
let thisOptions = renderDropDownOptionsFromList(deviceObj, superIndex)
oneDeviceDropdowns.push(
<DropdownButton title={deviceObj[i]}>
{thisOptions}
</DropdownButton>
)
}
return(
<Card>
<Card.Title>{deviceObj.name}</Card.Title>
<Card.Body>
{oneDeviceDropdowns}
</Card.Body>
</Card>
)
}
const renderDropDownOptionsFromList = (deviceObj, superIndex) => {
console.log(deviceObj)
return deviceObj.remaining_drivers.map( (driver, index) => {
<Dropdown.Item key={index} onClick={() => handleDriverSelection(deviceObj, driver, index, superIndex)}>
{driver.firstname} {driver.lastname}
</Dropdown.Item>
})
}
What gets me, is not even the <Card.Title>{deviceObj.name}</Card.Title> line renders, which is not inside the nested map, only the first layer of it... So if deviceObj is logging properly, I see legitimately no reason why that line wouldn't be rendering. Does anyone have any ideas, am I evenm able to do this with DropDown Menus?
no data is showing beacuse you are not returning it from the map function callback in renderDefaultScheduling and renderDropDownOptionsFromList
i have marked the return statement with return
const renderDefaultScheduling = () => {
return allDevices.map( (device, superIndex) => {
****return**** <Card>
{renderAllOfThisDevice(device, superIndex)}
</Card>
})
}
const renderAllOfThisDevice = (deviceObj, superIndex) => {
let oneDeviceDropdowns = []
console.log(deviceObj)
for (let i = 1; i <= deviceObj.amount; i = i + 1){
let thisOptions = renderDropDownOptionsFromList(deviceObj, superIndex)
oneDeviceDropdowns.push(
<DropdownButton title={deviceObj[i]}>
{thisOptions}
</DropdownButton>
)
}
return(
<Card>
<Card.Title>{deviceObj.name}</Card.Title>
<Card.Body>
{oneDeviceDropdowns}
</Card.Body>
</Card>
)
}
const renderDropDownOptionsFromList = (deviceObj, superIndex) => {
console.log(deviceObj)
return deviceObj.remaining_drivers.map( (driver, index) => {
****return**** <Dropdown.Item key={index} onClick={() => handleDriverSelection(deviceObj, driver, index, superIndex)}>
{driver.firstname} {driver.lastname}
</Dropdown.Item>
})
}
Related
I am using Material UI accordion my issue is if I click on the arrow accordion will get open but again I click on the arrow it will not get closed I need to set it when the user clicks on the arrow according will close and open based on the arrow click check code sandbox link for better understanding.
export default function ControlledAccordions() {
const [expanded, setExpanded] = React.useState(false);
// const handleChange = (panel) => (event, isExpanded) => {
// setExpanded(isExpanded ? panel : false);
// };
const handleChange = (pannel) => {
setExpanded(pannel);
};
const panaalData = ["panel1", "panel2", "panel3", "panel4"];
return (
<div>
{panaalData.map((value, i) => {
return (
<Accordion expanded={expanded === `panel${i}`}>
<AccordionSummary
expandIcon={
<ExpandMoreIcon
onClick={() => {
handleChange(`panel${i}`);
}}
style={{ cursor: "pointer" }}
/>
}
aria-controls="panel1d-content"
id="panel1d-header"
>
fdsfdsf
</AccordionSummary>
<AccordionDetails>dfdf</AccordionDetails>
</Accordion>
);
})}
</div>
);
}
Code SandBox Link
you need to reset panel in that case. You can do that in change handler.
const handleChange = (pannel) => {
setExpanded(expended === pannel ? '' : pannel);
};
when you click the already expanded panel, it just sets it to be expanded again.
you need to check whether the clicked panel is already expanded and if so collapse it instead of expanding it:
const handleChange = (pannel) => {
if (expanded === pannel) setExpanded(false);
else setExpanded(pannel);
};
Create another component called MyAccordian and keep toggling accordion logic in that component. That way you don't need to handle toggling for each and every component separately.
export default function ControlledAccordions() {
const panaalData = ["panel1", "panel2", "panel3", "panel4"];
return (
<div>
{panaalData.map((value, i) => {
return <MyAccordian value={value} />;
})}
</div>
);
}
const MyAccordian = ({ value }) => {
const [expanded, setExpanded] = React.useState(false);
return (
<Accordion expanded={expanded}>
<AccordionSummary
expandIcon={
<ExpandMoreIcon
onClick={() => {
setExpanded((prev) => !prev);
}}
style={{ cursor: "pointer" }}
/>
}
aria-controls="panel1d-content"
id="panel1d-header"
>
{value}
</AccordionSummary>
<AccordionDetails>{value}</AccordionDetails>
</Accordion>
);
};
Working Demo
export default function ControlledAccordions() {
// initial state, everything is closed,
const [expandedIndex, setExpandedIndex] = React.useState(-1);
// this should be handleClic
const handleChange = (index) => {
// in useState, current expandedIndex is passed as the argument
// whatever we return will be set as the expandedIndex
setExpandedIndex((currentIndex) => {
// if any box is open, currentIndex will be that index
// when I click on the open box, it will set the expandedIndex=-1
if (currentIndex === index) {
return -1;
} else {
// If I reached here, that means I am on a closed box
// when I click I swithc the expandedIndex to current box's index
return index;
}
});
};
const panaalData = ["panel1", "panel2", "panel3", "panel4"];
return (
<div>
{panaalData.map((value, i) => {
// when handleChange runs on AccordionSummary expandedIndex===i
// that means when i click on the current box, it will be open
const isExpanded = expandedIndex === i;
return (
<Accordion expanded={isExpanded}>
<AccordionSummary
onClick={() => handleChange(i)}
expandIcon={
// I dont know #mui/material too much.
// main question is "I need to open and close accordion based on arrow click"
<ExpandMoreIcon
onClick={() => handleChange(i)}
style={{ cursor: "pointer" }}
/>
}
aria-controls="panel1d-content"
id="panel1d-header"
>
{value}
</AccordionSummary>
<AccordionDetails
style={{ backgroundColor: "green" }}
>{`box index ${i} is open`}</AccordionDetails>
</Accordion>
);
})}
</div>
);
}
proof of work:
const handleChange = (pannel) => {
setExpanded(!pannel);
};
I'm building a React stepper with MUI and I wanted to have it be dynamic depending on the data coming in and also to be able to add/remove steps within that dynamic section. I was not able to find any examples or posts on here regarding what I had in mind, and the docs for MUI's stepper don't go anywhere near touching dynamic/responsive steps like this. I've been able to get it pretty far (actually a bit surprised I got it as far as I have), but I'm stuck right at the end. I've set up a generic example on CSB (link below), with the stepper pulling in data objects, displaying them on the dynamic steps, and the add/remove functionality works for the step labels, but not for the content. This is where I can't figure it out, I set up the structure the same for the labels and content, and since is basically just pushing to/filtering out arrays, I'm not finding where the difference is. I figure I'd post it here before messing with it any further. Also, if anyone has any suggestions on how to clean it up, I'd be happy to hear those as well, I'm sure it is a bit messy and heavy handed in getting the job done.
Heres the main Stepper component:
const DynamicStepper = () => {
const [open, setOpen] = useState(false);
const [activeStep, setActiveStep] = useState(0);
const [copiedObjs, setCopiedObjs] = useState([]);
const [copiedLabels, setCopiedLabels] = useState([]);
const [middleContent, setMiddleContent] = useState([]);
const [stepsContent, setStepsContent] = useState([])
//Dialog actions
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
//Stepper actions
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
}
//Setting up dynamic labels (actual code involves conditional api calls)
useEffect(() => {
if(data) {
setCopiedObjs(data)
}
}, [])
useEffect(() => {
if(copiedObjs.length) {
let labeling = copiedObjs.map((i) => (
i.title
))
setCopiedLabels(labeling)
}
}, [copiedObjs])
//Set labels for stepper
const initialLabels = ["First", "Second", "Third"]
const finalLabels = ["One from the End", "Final"]
const steps = initialLabels.concat(copiedLabels).concat(finalLabels)
//Set content for stepper
//moved into useEffect
// function getMiddleContent(){
// const content = copiedObjs.map((obj, idx) => (
// <StepDynamic key={idx} props={obj} />
// ))
// setMiddleContent(content)
// }
useEffect(() => {
const content = copiedObjs.map((obj, idx) => (
<StepDynamic key={idx} props={obj} />
))
setMiddleContent(content)
}, [middleContent, copiedObjs])
useEffect(() => {
//add/delete steps
function addStep(n = 1){
let newSteps = [...copiedLabels];
let newContent = [...middleContent];
let newLabel = ["new obj"]
newSteps.push(...newLabel);
console.log("midContent pre push: ", middleContent)
let content = [<StepDynamic key={Math.random()*3} props={null} />]
newContent.push(...content)
console.log("postPush, newContent: ", content)
setCopiedLabels(newSteps)
setMiddleContent(content)
}
function removeStep(idx){
let newSteps = [...copiedLabels];
let newContent = [...middleContent];
let steps = newSteps.filter((item, i) => i !== idx);
let content = newContent.filter((item, i) => i !== idx)
setCopiedLabels(steps)
setMiddleContent(content)
}
const initialContent = [<StepOne key={1} />, <StepTwo key={2} />, <StepThree key={3} addStep={addStep} removeStep={removeStep} titles={copiedLabels} />]
const finalContent = [<StepPenUltimate key={4} />, <StepFinal key={5} />]
const content = initialContent.concat(middleContent).concat(finalContent)
setStepsContent(content)
}, [middleContent, copiedLabels])
function getStepsContent(stepIndex) {
return stepsContent[stepIndex]
}
//Moved this section into useEffect to see if I got better results, but responds the same as before
// //add/delete steps
// function addStep(n = 1){
// let newSteps = [...copiedLabels];
// let newContent = [...middleContent];
// let newLabel = ["new obj"]
// newSteps.push(...newLabel);
// console.log("midContent pre push: ", middleContent)
// let content = [<StepDynamic key={Math.random()*3} props={null} />]
// newContent.push(...content)
// console.log("postPush, newContent: ", content)
// setCopiedLabels(newSteps)
// setMiddleContent(content)
// }
// function removeStep(idx){
// let newSteps = [...copiedLabels];
// let newContent = [...middleContent];
// let steps = newSteps.filter((item, i) => i !== idx);
// let content = newContent.filter((item, i) => i !== idx)
// setCopiedLabels(steps)
// setMiddleContent(content)
// }
return (
<>
<Button variant="contained" color="primary" onClick={handleClickOpen}>
New Stepper
</Button>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Stepper Guide</DialogTitle>
<DialogContent>
<DialogContentText>
Just some words of guidance
</DialogContentText>
<div>
<Stepper activeStep={activeStep} alternativeLabel>
{steps && steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
<br />
</Stepper>
<>
{activeStep === steps.length ? (
"Complete"
) : (
<>
{getStepsContent(activeStep)}
<Button color='warning' onClick={handleBack}>
{activeStep === steps[0] ? "" : "Back"}
</Button>
<Button color="primary" onClick={handleNext}>
{activeStep === steps.length ? "Submit" : "Next"}
</Button>
</>
)}
</>
</div>
<DialogActions>
<Button color="error" onClick={handleClose}>
Cancel
</Button>
</DialogActions>
</DialogContent>
</Dialog>
</>
)
}
And heres the CSB: https://codesandbox.io/s/eager-cerf-tsrbci?file=/src/Stepper.js:463-5442
UPDATE
After playing around and a lot of console.logs I think I've figured out where the issue is, but I'm not sure how to resolve it. When adding/ removing a step, the functions are wrapped within the same useEffect as the logic to set the content for the steps. This was done to be able to provide the functions to the StepThree component where the adding/ removing takes place on the UI. However, it seems the state update and the logic setting the content are not on the same time cycle and so the content setting does not have access to the new updated state. I've tried breaking apart the useEffect into multiple and tried just writing separate functions, but because there is a lot of conditional state being utilized, and in order for the different pieces to correctly have access, as well as to avoid infinite re-render loops, I keep coming back to the same setup. The label setting works fine because the setting of the array for the labels happens outside the useEffect that compiles the conditional state.
If I'm wrong on my assessment please let me know, and if you have any ideas on how to better structure this, I'd love to hear it. I've looked at useReducer as a possible alternative, but I'm not that familiar with the react hooks beyond useState and useEffect, and so far haven't determined how to write out the reducer, again I keep coming back to needing to have the conditional pieces of state available at the right place in the code.
I converted a class component into a function component using hooks. Currently, I'm struggling to figure out why the checkboxes within this map is not updating with checked value, despite the onChange handler firing, and updating the array as necessary. (The onSubmit also works, and updates the value within the DB properly).
import {
Container,
Typography,
Grid,
Checkbox,
FormControlLabel,
Button
} from "#material-ui/core";
import Select from "react-select";
import localeSelect from "../services/localeSelect";
import {
linkCharactersToGame,
characterLinked,
linkCharacters
} from "../data/locales";
import dbLocale from "../services/dbLocale";
import { LanguageContext } from "../contexts/LanguageContext";
import { UserContext } from "../contexts/UserContext";
import { GameContext } from "../contexts/GameContext";
import { CharacterContext } from "../contexts/CharacterContext";
import { Redirect } from "react-router-dom";
export default function LinkCharacter() {
const { language } = useContext(LanguageContext);
const { user } = useContext(UserContext);
const { games, loading, error, success, connectCharacters } = useContext(
GameContext
);
const { characters } = useContext(CharacterContext);
const [game, setGame] = useState("");
const [selectedCharacters, setSelectedCharacters] = useState([]);
if (!user) {
return <Redirect to="/" />;
}
return (
<section className="link-character">
<Container maxWidth="sm">
<Typography variant="h5">
{localeSelect(language, linkCharactersToGame)}
</Typography>
{error && (
<p className="error">
<span>Error:</span> {error}
</p>
)}
{success && <p>{localeSelect(language, characterLinked)}</p>}
<Select
options={games.map(game => {
return {
label: dbLocale(language, game),
value: game._id
};
})}
onChange={e => {
setGame(e.value);
const selected = [];
const index = games.findIndex(x => x._id === e.value);
games[index].characters.forEach(character => {
selected.push(character._id);
});
setSelectedCharacters(selected);
}}
/>
</Container>
<Container maxWidth="md">
{game !== "" && (
<>
<Grid container spacing={2}>
{characters.map((character, index) => {
return (
<Grid item key={index} md={3} sm={4} xs={6}>
<FormControlLabel
control={
<Checkbox
value={character._id}
onChange={e => {
const index = selectedCharacters.indexOf(
e.target.value
);
if (index === -1) {
selectedCharacters.push(e.target.value);
} else {
selectedCharacters.splice(index, 1);
}
}}
color="primary"
checked={
selectedCharacters.indexOf(character._id) !== -1
}
/>
}
label={dbLocale(language, character)}
/>
</Grid>
);
})}
</Grid>
<Button
variant="contained"
color="primary"
onClick={e => {
e.preventDefault();
connectCharacters(game, selectedCharacters);
}}
>
{localeSelect(language, linkCharacters)}
</Button>
</>
)}
</Container>
</section>
);
}
I feel like there's something I'm missing within Hooks (or there's some sort of issue with Hooks handling something like this). I have been searching and asking around and no one else has been able to figure out this issue as well.
The state returned by [state, setState] = useState([]) is something that you should only be reading from. If you modify it, React won't know that the data has changed and that it needs to re-render. When you need to modify data, you have to use setState, or in your case setSelectedCharacters.
Also, modifying the data by reference might lead to unpredictable results if the array is read elsewhere, later on.
In addition to that, if you give the same value to setState, that the hook returned you in state, React will skip the update entirely. It is not a problem when using numbers or strings, but it becomes one when you use arrays, because the reference (the value React uses to tell if there is a difference) can be the same, when the content might have changed. So you must pass a new array to setState.
With that in mind, your onChange function could look like:
onChange={e => {
const index = selectedCharacters.indexOf(
e.target.value
);
if (index === -1) {
// creating a new array with [], so the original one stays intact
setSelectedCharacters([...selectedCharacters, e.target.value]);
} else {
// Array.filter also creates new array
setSelectedCharacters(selectedCharacters.filter((char, i) => i !== index));
}
}}
Doc is here https://en.reactjs.org/docs/hooks-reference.html#usestate
Why isn't my Chart component re-rendered when I change the state with setState in componentDidMount?
I want to fetch the data from the database and when they are loaded, render the chart. Instead, the chart is rendered with empty data and the data from the database isn't shown.
changeJoystick = () => {
this.setState({robustActive: !this.state.robustActive, compactActive: !this.state.compactActive});
};
async fetchHeatMapData() {
let robustData = [];
let compactData = [];
try {
let response = await getDoubleAxisSegmentAverage();
robustData = response.data;
let {seriesRobust} = this.state;
robustData = robustData.slice(1, 37);
for (let i = 0; i < 6; i++) {
seriesRobust[i].data = robustData.slice(6 * i, 6 * (i + 1));
}
return seriesRobust;
} catch (err) {
console.log(err);
}
}
componentDidMount() {
this.fetchHeatMapData()
.then(seriesRobust => {
this.setState({seriesRobust});
console.log(this.state.seriesRobust);
}
)
}
render() {
let robust_variant = this.state.robustActive ? 'contained' : 'outlined';
let compact_variant = this.state.compactActive ? 'contained' : 'outlined';
return (
<Fragment>
<Grid container direction='row' justify='flex-start'>
<Grid item>
<Button variant={robust_variant} color='secondary' onClick={this.changeJoystick.bind(this)}>Robust
Joystick</Button>
</Grid>
<Grid item>
<Button variant={compact_variant} color='secondary'
onClick={this.changeJoystick.bind(this)}>Compact
Joystick</Button>
</Grid>
</Grid>
<br/>
<Grid container justify='space-evenly'>
<Grid item>
<Chart
options={this.state.options}
series={this.state.robustActive ? this.state.seriesRobust :
this.state.seriesCompact}
type='heatmap'
width={1000}
height={1000}/>
</Grid>
</Grid>
</Fragment>
componentDidMount() {
this.fetchHeatMapData()
.then(seriesRobust => {
this.setState({seriesRobust});
console.log(this.state.seriesRobust);
}
)
}
You should not expect updated state value just after setState call!! Mayority of 'setState not rerendered' questions is about this.
You can do just
componentDidMount() {
this.fetchHeatMapData()
}
and setState() inside fetchHeatMapData() instead of return
let {seriesRobust} = this.state;
this code uses the same ref for object, it's enough to
const seriesRobust = [...this.state.seriesRobust]
this.state.seriesRobust is almost NOT USED in render, it's used conditionally (only if robustActive is true)
series={this.state.robustActive ? this.state.seriesRobust :
I changed my code like this:
componentDidMount() {
this.fetchHeatMapData().then(() => this.forceUpdate());
}
In the fetchHeatMapData() function I set the state with
this.setState({seriesRobust});
When the data has been fetched I'm doing a forceUpdate in componentDidMount() and now it's working as I intended.
I know that you should usually avoid using forceUpdate() but this is the only solution I can think of right now.
I have been tasked to add a copy and delete button on one of our tables. I am trying to pass the index from the map to the delete and copy onclick and this does delete but...
Problem: If you copy a row it will have the exact same "i" as the original, and it moves the position of all the ones below it messing up the delete tremendously
I was under the impression that if I setRows() to something new it would run the mapping again and give them all the correct i's in each function but this doesn't seem to be the case, why?
const AdvancedTable = () => {
const [rows, setRows] = useState(tableRows); ///tableRows is basically an array of divs
const deleteOnClick = (i: number) => {
setRows(() => {
const myRows = [...rows];
myRows.splice(i, 1);
return myRows;
});
}
const copyOnClick = (i: number) => {
setRows(() => {
const myRows = [...rows];
myRows.splice(i, 0, rows[i]);
return myRows;
});
}
return (
<Paper>
{
rows.map((row: any, i: number) => (
<div>
<IconButton onClick={() => { deleteOnClick(i) }} size="small">
<ClearIcon />
</IconButton>
<IconButton onClick={() => { copyOnClick(i) }} size="small">
<FileCopyIcon />
</IconButton>
</div>
</TableCell>
{row}
))}
</Paper>
);
}
export default AdvancedTable;
Do you want the copy method to copy the row below the one you clicked or to send it to then end of the array?
maybe this can help you assuming you can call the i value from your row object with .i
const copyOnClick = (i: number) => {
setRows(() => {
const myRows = [...rows];
// send the new row to the end of the array
myRows.splice(rows.length, 0, rows[i]);
// change the new row i value to + 1 of the last object that was previously in the array
myRows[myRows.pop().i].i = rows.pop().i + 1
return myRows;
});
}