React component not re-rendering on component state change - reactjs

I am working on a sidebar using a recursive function to populate a nested list of navigation items.
Functionally, everything works except for the re-render when I click on one of the list items to toggle the visibility of the child list.
Now, when I expand or collapse the sidebar (the parent component with its visibility managed in its own state), the list items then re-render as they should. This shows me the state is being updated.
I have a feeling this possibly has something to do with the recursive function?
import React, { useState } from "react";
import styles from "./SidebarList.module.css";
function SidebarList(props) {
const { data } = props;
const [visible, setVisible] = useState([]);
const toggleVisibility = (e) => {
let value = e.target.innerHTML;
if (visible.includes(value)) {
setVisible((prev) => {
let index = prev.indexOf(value);
let newArray = prev;
newArray.splice(index, 1);
return newArray;
});
} else {
setVisible((prev) => {
let newArray = prev;
newArray.push(value);
return newArray;
});
}
};
const hasChildren = (item) => {
return Array.isArray(item.techniques) && item.techniques.length > 0;
};
const populateList = (data) => {
return data.map((object) => {
return (
<>
<li
key={object.name}
onClick={(e) => toggleVisibility(e)}
>
{object.name}
</li>
{visible.includes(object.name) ? (
<ul id={object.name}>
{hasChildren(object) && populateList(object.techniques)}
</ul>
) : null}
</>
);
});
};
let list = populateList(data);
return <ul>{list}</ul>;
}
export default SidebarList;

There are many anti patterns with this code but I will just focus on rendering issue. Arrays hold order. Your state does not need to be ordered so it's easier to modify it, for the case of demo I will use object. Your toggle method gets event, but you want to get DOM value. That's not necessary, you could just sent your's data unique key.
See this demo as it fixes the issues I mentioned above.

Related

React.js - Using one component multiple times with different settings

I'm trying to create a re-usable component that wraps similar functionality where the only things that change are a Title string and the data that is used to populate a Kendo ComboBox.
So, I have a navigation menu that loads (at the moment) six different filters:
{
context && context.Filters && context.Filters.map((item) => getComponent(item))
}
GetComponent gets the ID of the filter, gets the definition of the filter from the context, and creates a drop down component passing in properties:
function getComponent(item) {
var filterDefinition = context.Filters.find(filter => filter.Id === item.Id);
switch (item.DisplayType) {
case 'ComboBox':
return <DropDownFilterable key={item.Id} Id={item.Id} Definition={filterDefinition} />
default:
return null;
}
}
The DropDownFilterable component calls a service to get the data for the combo box and then loads everything up:
const DropDownFilterable = (props) => {
const appService = Service();
filterDefinition = props.Definition;
console.log(filterDefinition.Name + " - " + filterDefinition.Id);
React.useEffect(() => {
console.log("useEffect: " + filterDefinition.Name + " - " + filterDefinition.Id);
appService.getFilterValues(filterDefinition.Id).then(response => {
filterData = response;
})
}, []);
return (
<div>
<div className="row" title={filterDefinition.DisplayName}>{filterDefinition.DisplayName}</div>
<ComboBox
id={"filterComboBox_" + filterDefinition.Id}
data={filterData}
//onOpen={console.log("test")}
style={{zIndex: 999999}}
dataItemKey={filterDefinition && filterDefinition.Definition && filterDefinition.Definition.DataHeaders[0]}
textField={filterDefinition && filterDefinition.Definition && filterDefinition.Definition.DataHeaders[1]}
/>
</div>
)
}
Service call:
function getFilterValues(id) {
switch(id) {
case "E903B2D2-55DE-4FA3-986A-8A038751C5CD":
return fetch(Settings.url_getCurrencies).then(toJson);
default:
return fetch(Settings.url_getRevenueScenarios).then(toJson);
}
};
What's happening is, the title (DisplayName) for each filter is correctly rendered onto the navigation menu, but the data for all six filters is the data for whichever filter is passed in last. I'm new to React and I'm not 100% comfortable with the hooks yet, so I'm probably doing something in the wrong order or not doing something in the right hook. I've created a slimmed-down version of the app:
https://codesandbox.io/s/spotlight-react-full-forked-r25ns
Any help would be appreciated. Thanks!
It's because you are using filterData incorrectly - you defined it outside of the DropDownFilterable component which means it will be shared. Instead, set the value in component state (I've shortened the code to include just my changes):
const DropDownFilterable = (props) => {
// ...
// store filterData in component state
const [filterData, setFilterData] = React.useState(null);
React.useEffect(() => {
// ...
appService.getFilterValues(filterDefinition.Id).then(response => {
// update filterData with response from appService
setFilterData(response);
})
}, []);
// only show a ComboBox if filterData is defined
return filterData && (
// ...
)
}
Alternatively you can use an empty array as the default state...
const [filterData, setFilterData] = React.useState([]);
...since the ComboBox component accepts an array for the data prop. That way you won't have to conditionally render.
Update
For filterDefinition you also need to make sure it is set properly:
const [filterDefinition, setFilterDefinition] = React.useState(props.Definition);
// ...
React.useEffect(() => {
setFilterDefinition(props.Definition);
}, [props.Definition]);
It may also be easier to keep filterDefinition out of state if you don't expect it to change:
const filterDefinition = props.Definition || {};

Managing Button State and Resultant Processing with React Hooks

I've got some toggles that can be turned on/off. They get on/off state from a parent functional component. When a user toggles the state, I need to update the state in the parent and run a function.
That function uses the state of all the toggles to filter a list of items in state, which then changes the rendered drawing in a graph visualization component.
Currently, they toggle just fine, but the render gets out of sync with the state of the buttons, because the processing function ends up reading in old state.
I tried using useEffect(), but because the function has a lot of dependencies it causes a loop.
I tried coupling useRef() with useState() in a custom hook to read out the current state of at least the newest filter group that was set, but no luck there either.
Any suggestions on how I could restructure my code in a better way altogether, or a potential solution to this current problem?
Gross function that does the filtering:
function filterItems(threshold, items = {}) {
const { values } = kCoreResult;
const { coloredItems } = rgRef.current;
let itemsForUse;
let filteredItems;
if (Object.entries(items).length === 0 && items.constructor === Object) {
itemsForUse = baseItemsRef.current;
} else {
itemsForUse = items;
}
const isWithinThreshold = id => has(values, id) && values[id] >= threshold;
// filter for nodes meeting the kCoreValue criterion plus all links
filteredItems = pickBy(
itemsForUse,
(item, id) => !isNode(item) || isWithinThreshold(id)
);
filteredItems = pickBy(
filteredItems,
item =>
!has(item, 'data.icon_type') || !filterRef.current[item.data.icon_type]
);
setRg(rg => {
rg.filteredItems = leftMerge(filteredItems, coloredItems);
return {
...rg,
};
});
setMenuData(menuData => {
menuData.threshold = threshold;
return {
...menuData,
};
});
}
Function that calls it after button is pressed that also updates button state (button state is passed down from the filter object):
function changeCheckBox(id, checked) {
setFilter(filter => {
filter[id] = !checked;
return {
...filter,
};
});
filterItems(menuData.threshold);
}
It seems calling your filterItems function in the handler is causing the stale state bug, the state update hasn't been reconciled yet. Separate out your functions that update state and "listen" for updates to state to run the filter function.
Here's a demo that should help see the pattern:
export default function App() {
const [filters, setFilters] = useState(filterOptions);
const onChangeHandler = e => {
setFilters({ ...filters, [e.target.name]: e.target.checked });
};
const filterItems = (threshold, items = {}) => {
console.log("Gross function that does the filtering");
console.log("threshold", threshold);
console.log("items", items);
};
useEffect(() => {
filterItems(42, filters);
}, [filters]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
{Object.entries(filters).map(([filter, checked]) => {
return (
<Fragment key={filter}>
<label htmlFor={filter}>{filter}</label>
<input
id={filter}
name={filter}
type="checkbox"
checked={checked}
onChange={onChangeHandler}
/>
</Fragment>
);
})}
</div>
);
}
This works by de-coupling state updates from state side-effects. The handler updates the filters state by always returning a new object with next filter values, and the effect hook triggers on value changes to filters.

How to store a link into the variable using react?

i am new to programming. I want to implement the below,
when i click a list item i want to navigate to another page.
What i am trying to do?
I have list items in a side panel. when i click on the list item it should navigate to another page.
So i have a listpanel component which renders the listitem component. On click list item, based on item_data_type it should take to the link got from get_type1_link or get_type2_link methods. however, it returns an object. I am not sure where i am making mistake.
class ListPanel extends react.purecomponent {
get_type1_link = () => {
const item_data = this.props.item_data;
const itemss = this.props.items;
const {itemname, item_id} = item_data.properties;
const filtered_item = items && items.find(item => item.id ===
item_id);
const item_name = (filtered_item) ? filtered_item.itemname :
(itemname ? itemname : item_id);
if (filtered_item) {
return (<Link to={`/someurl/${item_data.properties.item_id}`}>
{item_name}</Link>);
} else {
return <span>{item_name}</span>;
}
};
get_type1_link = () => {
const item_data = this.props.item_data;
let link;
switch (item_data.type) {
case 'type1':
link = this.get_type1_link();
break;
case 'type2':
link = this.get_type2_link();
break;
default:
return link=window.location.href;
}
return link;
};
render = () => {
const list_item = this.props.;
return (
<ListItem
key={list_item.id}
text={this.get_text}
link={this.get_link}/>
);
}
class ListItem extends react.purecomponent {
render = () => {
<li onClick={props.link}>
<div className="text">
{this.props.text}
</div>
</li>
}
}
I think there is a problem in the way i am storing the value returned from get_type1_link method into variable link. since get_type1_link returns a jsx (Link). Could someone help me with this thanks.
That's because link is an object, presumably a string although its hard to tell without the get_link helpers. onClick wants a callback. To interact with the browser, you should use the javascript window api https://developer.mozilla.org/en-US/docs/Web/API/Window.
So to change the window, change <li onClick={props.link}> to <li onClick={{() => window.location.href = props.link}}> as shown here How to get the browser to navigate to URL in JavaScript.

How to target a specific item to toggleClick on using React Hooks?

I have a navbar component with that actual info being pulled in from a CMS. Some of the nav links have a dropdown component onclick, while others do not. I'm having a hard time figuring out how to target a specific menus index with React Hooks - currently onClick, it opens ALL the dropdown menus at once instead of the specific one I clicked on.
The prop toggleOpen is being passed down to a styled component based on the handleDropDownClick event handler.
Heres my component.
const NavBar = props => {
const [links, setLinks] = useState(null);
const [notFound, setNotFound] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const fetchLinks = () => {
if (props.prismicCtx) {
// We are using the function to get a document by its uid
const data = props.prismicCtx.api.query([
Prismic.Predicates.at('document.tags', [`${config.source}`]),
Prismic.Predicates.at('document.type', 'navbar'),
]);
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
});
}
return null;
};
const checkForLinks = () => {
if (props.prismicCtx) {
fetchLinks(props);
} else {
setNotFound(true);
}
};
useEffect(() => {
checkForLinks();
});
const handleDropdownClick = e => {
e.preventDefault();
setIsOpen(!isOpen);
};
if (links) {
const linkname = links.map(item => {
// Check to see if NavItem contains Dropdown Children
return item.items.length > 1 ? (
<Fragment>
<StyledNavBar.NavLink onClick={handleDropdownClick} href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
<Dropdown toggleOpen={isOpen}>
{item.items.map(subitem => {
return (
<StyledNavBar.NavLink href={subitem.sub_nav_link.url}>
<span>{subitem.sub_nav_link_label[0].text}</span>
</StyledNavBar.NavLink>
);
})}
</Dropdown>
</Fragment>
) : (
<StyledNavBar.NavLink href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
);
});
// Render
return (
<StyledNavBar>
<StyledNavBar.NavContainer wide>
<StyledNavBar.NavWrapper row center>
<Logo />
{linkname}
</StyledNavBar.NavWrapper>
</StyledNavBar.NavContainer>
</StyledNavBar>
);
}
if (notFound) {
return <NotFound />;
}
return <h2>Loading Nav</h2>;
};
export default NavBar;
Your problem is that your state only handles a boolean (is open or not), but you actually need multiple booleans (one "is open or not" for each menu item). You could try something like this:
const [isOpen, setIsOpen] = useState({});
const handleDropdownClick = e => {
e.preventDefault();
const currentID = e.currentTarget.id;
const newIsOpenState = isOpen[id] = !isOpen[id];
setIsOpen(newIsOpenState);
};
And finally in your HTML:
const linkname = links.map((item, index) => {
// Check to see if NavItem contains Dropdown Children
return item.items.length > 1 ? (
<Fragment>
<StyledNavBar.NavLink id={index} onClick={handleDropdownClick} href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
<Dropdown toggleOpen={isOpen[index]}>
// ... rest of your component
Note the new index variable in the .map function, which is used to identify which menu item you are clicking.
UPDATE:
One point that I was missing was the initialization, as mention in the other answer by #MattYao. Inside your load data, do this:
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
setIsOpen(navlinks.map((link, index) => {index: false}));
});
Not related to your question, but you may want to consider skipping effects and including a key to your .map
I can see the first two useState hooks are working as expected. The problem is your 3rd useState() hook.
The issue is pretty obvious that you are referring the same state variable isOpen by a list of elements so they all have the same state. To fix the problems, I suggest the following way:
Instead of having one value of isOpen, you will need to initialise the state with an array or Map so you can refer each individual one:
const initialOpenState = [] // or using ES6 Map - new Map([]);
In your fetchLink function callback, initialise your isOpen state array values to be false. So you can put it here:
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
// init your isOpen state here
navlinks.forEach(link => isOpen.push({ linkId: link.id, value: false })) //I suppose you can get an id or similar identifers
});
In your handleClick function, you have to target the link object and set it to true, instead of setting everything to true. You might need to use .find() to locate the link you are clicking:
handleClick = e => {
const currentOpenState = state;
const clickedLink = e.target.value // use your own identifier
currentOpenState[clickedLink].value = !currentOpenState[clickedLink].value;
setIsOpen(currentOpenState);
}
Update your component so the correct isOpen state is used:
<Dropdown toggleOpen={isOpen[item].value}> // replace this value
{item.items.map(subitem => {
return (
<StyledNavBar.NavLink href={subitem.sub_nav_link.url}>
<span>{subitem.sub_nav_link_label[0].text}</span>
</StyledNavBar.NavLink>
);
})}
</Dropdown>
The above code may not work for you if you just copy & paste. But it should give you an idea how things should work together.

Filtering an icon from an array of icon strings for re-render

I'm trying to take an e.target.value which is an icon and filter it out from an array in state, and re-render the new state minus the matching icons. I can't seem to stringify it to make a match. I tried pushing to an array and toString(). CodeSandbox
✈ ["✈", "♘", "✈", "♫", "♫", "☆", "♘", "☆"]
Here is the code snippet (Parent)
removeMatches(icon) {
const item = icon;
const iconsArray = this.props.cardTypes;
const newIconsArray =iconsArray.filter(function(item) {
item !== icon
})
this.setState({ cardTypes: newIconsArray });
}
This is a function in the parent component Cards, when the child component is clicked I pass a value into an onClick. Below is a click handler in the Child component
handleVis(e) {
const item = e.target.value
this.props.removeMatches(item)
}
First of all, there's nothing really different about filtering an "icon" string array from any other strings. Your example works like this:
const icons = ["✈", "♘", "✈", "♫", "♫", "☆", "♘", "☆"]
const icon = "✈";
const filteredIcons = icons.filter(i => i !== icon);
filteredIcons // ["♘", "♫", "♫", "☆", "♘", "☆"]
Your CodeSandbox example has some other issues, though:
Your Card.js component invokes this.props.removeMatches([item]) but the removeMatches function treats the argument like a single item, not an array.
Your Cards.js removeMatches() function filters this.props.cardTypes (with the previously mentioned error about treating the argument as a single item not an array) but does not assign the result to anything. Array.filter() returns a new array, it does not modify the original array.
Your Cards.js is rendering <Card> components from props.cardTypes, this means that Cards.js is only rendering the cards from the props it is given, so it cannot filter that prop from inside the component. You have a few options:
Pass the removeMatches higher up to where the cards are stored in state, in Game.js as this.state.currentCards, and filter it in Game.js which will pass the filtered currentCards back down to Cards.js.
// Game.js
removeMatches = (items) => {
this.setState(prevState => ({
currentCards: prevState.currentCards.filter(card => items.indexOf(card) == -1)
}));
}
// ...
<Cards cardTypes={this.state.currentCards} removeMatches={this.removeMatches} />
// Cards.js
<Card removeMatches={this.props.removeMatches}/>
// Card.js -- same as it is now
Move Cards.js props.cardTypes into state (ex state.currentCards) within Cards.js, then you can filter it out in Cards.js and render from state.currentCards instead of props.cardTypes. To do this you would also need to hook into componentWillReceiveProps() to make sure that when the currentCards are passed in as prop.cardTypes from Game.js that you update state.currentCards in Cards.js. That kind of keeping state in sync with props can get messy and hard to follow, so option 1 is probably better.
// Cards.js
state = { currentCards: [] }
componentWillReceiveProps(nextProps) {
if (this.props.cardTypes !== nextProps.cardTypes) {
this.setState({ currentCards: nextProps.cardTypes });
}
}
removeMatches = (items) => {
this.setState(prevState => ({
currentCards: prevState.currentCards.filter(card => items.indexOf(card) == -1)
}));
}
render() {
return (
<div>
{ this.state.currentCards.map(card => {
// return rendered card
}) }
</div>
);
}
Store all the removed cards in state in Cards.js and filter cardTypes against removedCards before you render them (you will also need to reset removedCards from componentWillReceiveProps whenever the current cards are changed):
// Cards.js
state = { removedCards: [] }
componentWillReceiveProps(nextProps) {
if (this.props.cardTypes !== nextProps.cardTypes) {
this.setState({ removedCards: [] });
}
}
removeMatches = (items) => {
this.setState(prevState => ({
removedCards: [...prevState.removedCards, ...items]
}));
}
render() {
const remainingCards = this.props.cardTypes.filter(card => {
return this.state.removedCards.indexOf(card) < 0;
});
return (
<div>
{ remainingCards.map(card => {
// return rendered card
})}
</div>
);
}
As you can see, keeping state in one place in Game.js is probably your cleanest solution.
You can see all 3 examples in this forked CodeSandbox (the second 2 solutions are commented out): https://codesandbox.io/s/6yo42623p3

Resources