React setting state to child component causing infinite loop - javascript

I have a page with some dropdown menu's used to search some content, the dropdown is a non-functional component. The page is a listsing page. Not important but gives some context.
I do some calculation on the listing page and update the state, then I pass this state into the Dropdown component. However, I'm getting an infinite loop and I'm not sure how to stop it or where I'm going wrong.
my listing page is here:
constructor(props){
super(props)
let industryList = this.createList(this.props.data.mainYaml.caseStudiesDropdowns[0].items)
let areaList = this.createList(this.props.data.mainYaml.caseStudiesDropdowns[1].items)
let techniqueList = this.createList(this.props.data.mainYaml.caseStudiesDropdowns[2].items)
this.state = {
industry: "All Industries",
area: "All Areas",
technique: [],
industries: industryList,
areas: areaList,
techniques: techniqueList
}
}
createList = (listItems) => {
let listArr = []
listItems.forEach((item) => {
let obj = {
name: item,
disabled: false
}
listArr.push(obj)
})
return listArr
}
filterCaseStudies = (caseStudies) => {
const filterIndustry = (caseStudies) => {
if (this.state.industry == "All Industries") {
return caseStudies
} else {
return caseStudies.filter((study) => study.node.industry == this.state.industry)
}
}
const filterArea = (caseStudies) => {
if (this.state.area == "All Areas") {
return caseStudies
} else {
return caseStudies.filter((study) => study.node.area == this.state.area)
}
}
const filterTechnique = (caseStudies) => {
if (this.state.technique.length === 0) {
return caseStudies
} else {
let matchedStudies = []
caseStudies.forEach((study) => {
let count = 0;
let techCount = study.node.technique.length - 1;
study.node.technique.forEach((item, i) => {
this.state.technique.forEach((selectedItems) => {
if (selectedItems == item) {
count++;
return
}
})
if (i == techCount && count > 0) {
study.node.count = count
matchedStudies.push(study)
}
})
})
matchedStudies.sort((a, b) => b.node.count - a.node.count);
return matchedStudies;
}
}
let industryMatches = filterIndustry(caseStudies)
let areaMatches = filterArea(industryMatches)
this.filterDropdowns(areaMatches)
let techniqueMatches = filterTechnique(areaMatches)
return techniqueMatches;
}
filterDropdowns = (filteredCaseStudies) => {
console.log(filteredCaseStudies)
let disabledIndustries = [];
let disabledAreas = [];
let disabledTechniques = [];
this.state.industries.forEach((industry) => {
let obj = {
name: industry.name
}
if (industry.name == "All Industries") {
console.log(industry.name)
obj.disabled = false;
disabledIndustries.push(obj);
} else {
obj.disabled = true;
filteredCaseStudies.forEach((study) => {
if (study.node.industry == industry.name) {
obj.disabled = false;
}
})
disabledIndustries.push(obj);
}
})
console.log(disabledIndustries)
this.setState({industries: disabledIndustries})
}
getCaseStudies = (caseStudies) => {
let filteredCaseStudies = this.filterCaseStudies(caseStudies)
if (filteredCaseStudies.length > 0) {
return filteredCaseStudies.map((study, i) => {
return (
<div key={i} className="col-lg-4 col-md-6 col-12 px-4 mb-5">
<CaseStudyListItem
data={study.node}
className="CaseStudyListItem--lg"
index={i}/>
</div>
)
})
} else {
return (
<div className="col-12 px-4 mb-5">
<h4>We're Sorry!</h4>
<p>We can't seem to find any case studies that match your search. Please try other search terms.</p>
</div>
)
}
}
dropdownChange = (selected, name) => {
this.setState({[name]: selected})
}
render () {
console.log(this.state)
return (
<Layout bodyClass="k-reverse-header">
<div className="CaseStudies">
<section className="CaseStudies__header k-bg--grey">
<div className="container-fluid">
<div className="d-flex k-row">
<div className="col-12 px-4">
<DropdownSelect className="CaseStudies__search-industry mb-4" data={this.props.data.mainYaml.caseStudiesDropdowns[0]} list={this.state.industries} selected={this.dropdownChange} />
<DropdownSelect className="CaseStudies__search-area mb-4" data={this.props.data.mainYaml.caseStudiesDropdowns[1]} list={this.state.areas} selected={this.dropdownChange} />
</div>
</div>
</div>
</section>
<section className="CaseStudies__list">
<div className="container-fluid">
<div className="d-flex flex-wrap k-row">
{this.getCaseStudies(this.props.data.allCaseStudiesYaml.edges)}
</div>
</div>
</section>
</div>
</Layout>
)
}
}
I believe the issue happens as I pass the state into the Dropdown component, it is also updated in the filterDropdowns function. The Dropdown component code is as follows.
const DropdownSelect = ({ data, className, list, selected}) => {
const [isActive, setActive] = useState(false);
const [activeItem, changeActiveItem] = useState(data.placeholder);
const ref = useRef();
useEffect(() => {
const checkIfClickedOutside = (e) => {
// If the menu is open and the clicked target is not within the menu,
// then close the menu
if (isActive && ref.current && !ref.current.contains(e.target)) {
setActive(false)
}
}
document.addEventListener("mousedown", checkIfClickedOutside)
return () => {
// Cleanup the event listener
document.removeEventListener("mousedown", checkIfClickedOutside)
}
}, [isActive])
const toggleClass = () => {
setActive(!isActive);
}
const buildDropdown = () => {
const splitArr = (arr, len) => {
let chunks = [], i = 0, n = arr.length;
while (i < n) {
chunks.push(arr.slice(i, i += len));
}
return chunks;
}
const buildList = (items) => {
return items.map((item, i) =>
<li
key={i}
className={`DropdownSelect__list-item ${activeItem == item.name ? "active" : ""} ${item.disabled ? "disabled" : ""}`}
onClick={() => itemClicked(item.name, selected, data.name)}
>
{item.name}
</li>
)
}
const itemClicked = (item, selected, search) => {
changeActiveItem(item)
selected(item, search)
}
const arrLen = list.length < 10 ? 3 : 4;
const listsArr = splitArr(list, arrLen);
return listsArr.map((list, i) =>
<ul key={i} className="DropdownSelect__list">
{buildList(list)}
</ul>
)
}
return (
<div className={`DropdownSelect ${className ? className : ''}`} ref={ref}>
<div
className={`DropdownSelect__button ${isActive ? "active" : ""}`}
onClick={toggleClass}
>
{activeItem == null ? data.placeholder : activeItem}
</div>
<div className={`DropdownSelect__list-wrapper ${isActive ? "active" : ""}`}>{buildDropdown}</div>
</div>
)
}
export default DropdownSelect
I feel like i could have all of my state in the listing page but then the Dropdown component is pointless as it wouldn't be self sufficient and usable elsewhere.
I guess I want to know how I break this loop but also what are my bad practices here? ie am I using state wrongly?
Any help greatly appreciated!
PS Here's the React error i get
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.

If I'm reading this right...
In your render, you have {this.getCaseStudies(this.props.data.allCaseStudiesYaml.edges)}. getCaseStudies calls filterCaseStudies, which calls filterDropdowns, which has a setState in it. When a setState occurs, the page re-renders, causing the page to go through all those function calls again, another setState occurs, the page re-renders again, forever, causing an infinite loop.
You'll have to re-write your code somewhat. You could possibly use that state to store the data in a different format, like an array, and map the data in your render?

Related

React - How to change className by index?

I created a simple logic: when you click on a certain block, the classname changes, but the problem is that when you click on a certain block, the classname changes and the rest of the blocks looks like this
I need to change only the name of the class that I clicked, I think I need to use the index, but I don't quite understand how to reolize it
export default function SelectGradientTheme(props) {
const resultsRender = [];
const [borderColor, setBorderColor] = useState(false);
const setBorder = () => {
setBorderColor(!borderColor)
}
const borderColorClassName = borderColor ? "selectBorder" : null;
for (var i = 0; i < GradientThemes.length; i += 3) {
resultsRender.push(
<div className={"SelectThemePictures_Separator"}>
{
GradientThemes.slice(i, i + 3).map((col, index) => {
return (
<div key={index} className={borderColorClassName} onClick={() => props.SideBarPageContent(col)|| setBorder()}>
</div>
);
})
}
</div>
)
}
return (
<div className="SelectThemeWrapper">
{resultsRender}
</div>
);
};
You can remember the selected index
Please reference the following code:
export default function SelectGradientTheme(props) {
const resultsRender = [];
const [selectedIndex, setSelectedIndex] = useState(false);
const setBorder = (index) => {
setSelectedIndex(index);
};
for (var i = 0; i < GradientThemes.length; i += 3) {
resultsRender.push(
<div className={"SelectThemePictures_Separator"}>
{
GradientThemes.slice(i, i + 3).map((col, index) => {
return (
<div key={index}
className={index === selectedIndex ? 'selectBorder' : null}
onClick={() => props.SideBarPageContent(col)|| setBorder(index)}>
</div>
);
})
}
</div>
)
}
return (
<div className="SelectThemeWrapper">
{resultsRender}
</div>
);
};

Array not getting cleared to null or empty in setState on click in react

Array not getting cleared to null or empty in setState on click in react.
When I click on the submit button, the array must be set to []. It is setting to [], but on change the previous array of items comes into the array.
let questions = [];
let qns = [];
class App extends Component {
constructor(props) {
super(props);
this.state = {
btnDisabled: true,
//questions: [],
};
}
changeRadioHandler = (event, j) => {
this.setState({ checked: true });
const qn = event.target.name;
const id = event.target.value;
let idVal = this.props.dat.mat.opts;
let text = this.props.dat.mat.opt;
let userAnswer = [];
for (let j = 0; j < text.length; j++) {
userAnswer.push(false);
}
const option = text.map((t, index) => ({
text: t.text,
userAnswer: userAnswer[index],
}));
const elIndex = option.findIndex((element) => element.text === id);
const options = { ...option };
options[elIndex] = {
...options[elIndex],
userAnswer: true,
};
const question = {
options,
id: event.target.value,
qn,
};
questions[j] = options;
qns = questions.filter((e) => {
return e != null;
});
console.log(qns, qns.length);
this.setState({ qns });
if (qns.length === idVal.length) {
this.setState({
btnDisabled: false,
});
}
};
submitHandler = () => {
console.log(this.state.qns, this.state.questions);
this.setState({ qns: [] }, () =>
console.log(this.state.qns, this.state.questions)
);
};
render() {
return (
<div class="matrix-bd">
{this.props.dat.mat && (
<div class="grid">
{this.props.dat.mat.opts.map((questions, j) => {
return (
<div class="rows" key={j}>
<div class="cell main">{questions.text}</div>
{this.props.dat.mat.opt.map((element, i) => {
return (
<div class="cell" key={i}>
<input
type="radio"
id={j + i}
name={questions.text}
value={element.text}
onChange={(event) =>
this.changeRadioHandler(event, j)
}
></input>
<label htmlFor={j + i}>{element.text}</label>
</div>
);
})}
</div>
);
})}
</div>
)}
<div>
<button
type="button"
class="btn btn-primary"
disabled={this.state.btnDisabled}
onClick={this.submitHandler}
>
SUBMIT
</button>
</div>
</div>
);
}
}
export default App;
On button click submit, the array must be set to [] and when on change, the value must be set to the emptied array with respect to its index.
changeRadioHandler = (event, j) => {
// the better way is use local variable
let questions = [];
let qns = [];
...
}
submitHandler = () => {
console.log(this.state.qns, this.state.questions);
this.setState({ qns: [] }, () =>
console.log(this.state.qns, this.state.questions)
)}
// clear the previous `qns`
// if u use local variable. you don't need those lines
// this.qns = []
// this.questions = []
}
Finally, found out the solution.
After adding componentDidMount and setting questions variable to null solved my issue.
componentDidMount = () => {
questions = [];
};
Thanks all for your efforts and responses!

Why i'll get null pointer from Query results when come back from child component?

Hi i have problem with my react app. I have page with list of Subjects which data i get with react-apollo query. This page has action which links me to another component.
And then in that Child component i have Button Back which when i click it send me back to that lists of views... BUT this time it throws me null pointer error and i don't why it is happened.
const getSubject = `query GetSubject($id: ID!) {
getSubject(id: $id) {
id
name
description
packages(limit:999) {
items {
id
name
description
order
themes(limit:999){
items {
id
name
order
}
}
}
}
}
}
`;
function SubjectView(props) {
const classes = useStyles();
let width = window.innerWidth;
let years = [];
const [rocnikValue, setRocnik] = useState(0);
const [mobile, setMobile] = useState(0);
useEffect(() => {
function changeSize() {
if ((window.innerWidth < 960) && (mobile === false)) {
setMobile(true);
}
else if ((window.innerWidth > 960) && (mobile === true)) {
setMobile(false);
}
else return;
}
window.addEventListener("resize", changeSize.bind(this));
return function cleanup() {
window.removeEventListener("resize", changeSize.bind(this));
};
});
const handleSelect = event => {
setRocnik(event.target.value);
};
return (
<>
<Query
query={gql(getSubject)}
variables={{ id: props.match.params.subjectId }}
>
{result => {
if (result.loading) {
return (
<LinearProgress />
);
}
if (result.error) {
return (
<Typography color="error" variant="body1">
{result.error}
</Typography>
);
}
/* HERE I GET NULL POINTER ERROR */
result.data.getSubject.packages.items
.sort((a, b) => a.order - b.order)
.map((item,i) => years[i] = item.name)
if (!rocnikValue.length) {
setRocnik(years[0]);
return null;
}
if (width < 960) {
if (!mobile.length) setMobile(true);
return (
<div className={classes.page}>
<SubjectHeader
subject = {result.data.getSubject}
years = {years}
handleSelect = {handleSelect}
rocnik = {rocnikValue}
/>
{result.data.getSubject.packages.items
.sort((a, b) => a.order - b.order)
.map((pkg, pkgIndex) => (
<Fragment key={pkgIndex}>
{pkg.name === rocnikValue &&
<MobileView
key = {pkgIndex}
rocnik = {pkg}
/>
}
</Fragment>
))}
</div>
);
}
else {
if (!mobile.length) setMobile(false);
return (
<div className={classes.page}>
<SubjectHeader
subject = {result.data.getSubject}
years = {years}
handleSelect = {handleSelect}
rocnik = {rocnikValue}
/>
<DesktopView
subject = {result.data.getSubject}
rocnik = {rocnikValue}
/>
</div>
);
}
}}
</Query>
</>
);
}
Child component with back button is not important i think.
Anyway why is this happening ?
You have checked variables loading and error. I would also check the data before using that. Something like this:
if (result.data && result.data.hasOwnProperties('getSubject') && result.data.getSubject) {
...insert your actions here
} else {
return null <== If it's Ok...
}

How to add an arrow to menu elements that have children?

I am trying to add a FontAwesome arrow next to each item in my menu that has children (i.e. I want to indicate that you can click the element to display more data within that category). The menu is populated with json data from an API, and because it is so many nested objects, I decided to use recursion to make it work. But now I am having trouble adding an arrow only to the elements that have more data within it, instead of every single element in the menu.
Does anyone have an idea of how I could change it so the arrow only shows up next to the elements that need it? See below for image
class Menu extends React.Component {
state = {
devices: [],
objectKey: null,
tempKey: []
};
This is where I'm currently adding the arrow...
createMenuLevels = level => {
const { objectKey } = this.state;
const levelKeys = Object.entries(level).map(([key, value]) => {
return (
<ul key={key}>
<div onClick={() => this.handleDisplayNextLevel(key)}>{key} <FontAwesome name="angle-right"/> </div>
{objectKey[key] && this.createMenuLevels(value)}
</ul>
);
});
return <div>{levelKeys}</div>;
};
handleDisplayNextLevel = key => {
this.setState(prevState => ({
objectKey: {
...prevState.objectKey,
[key]: !this.state.objectKey[key]
}
}));
};
initializeTK = level => {
Object.entries(level).map(([key, value]) => {
const newTemp = this.state.tempKey;
newTemp.push(key);
this.setState({ tempKey: newTemp });
this.initializeTK(value);
});
};
initializeOK = () => {
const { tempKey } = this.state;
let tempObject = {};
tempKey.forEach(tempKey => {
tempObject[tempKey] = true;
});
this.setState({ objectKey: tempObject });
};
componentDidMount() {
axios.get("https://www.ifixit.com/api/2.0/categories").then(response => {
this.setState({ devices: response.data });
});
const { devices } = this.state;
this.initializeTK(devices);
this.initializeOK();
this.setState({ devices });
}
render() {
const { devices } = this.state;
return <div>{this.createMenuLevels(devices)}</div>;
}
}
This is what it looks like as of right now, but I would like it so items like Necktie and Umbrella don't have arrows, since there is no more data within those items to be shown
You could check in the map loop from createMenuLevels if the value is empty or not and construct the div based on that information.
createMenuLevels = level => {
const { objectKey } = this.state;
const levelKeys = Object.entries(level).map(([key, value]) => {
//check here if childs are included:
var arrow = value ? "<FontAwesome name='angle-right'/>" : "";
return (
<ul key={key}>
<div onClick={() => this.handleDisplayNextLevel(key)}>{key} {arrow} </div>
{objectKey[key] && this.createMenuLevels(value)}
</ul>
);
});
return <div>{levelKeys}</div>;
};
Instead of just checking if the value is set you could check if it is an array with: Array.isArray(value)

storing index in state on click , strange behavior

I'm mapping an array, and displaying them as a button. When users clicks on button i'm storing index and displaying data depending on that index. But there is some strange behavior. When I click on first button it does not store index in state. Only after clicking on other buttons , it's starts saving , but wrong index. What could be the problem?
displayButtons() {
const { data } = this.state
let sortedButtons = data.map((items, idx) => {
return (
<Button
key={idx}
className="project-btn"
primary
onClick={() => this.setState({ index: idx })}
>
{items.title}
</Button>
)
})
return sortedButtons
}
displayData() {
const { data, index } = this.state
let sortedData = data[index].settings.map((item, id) => {
const { _init_ } = item.settings
return _init_.map((message, index) => {
const { message_content } = message
return message_content === undefined ? null : (
<div key={index}>
<div>
<div className="settings-message">{message_content}</div>
</div>
<div>yes</div>
</div>
)
})
})
return sortedData
}

Categories