How to conditionally update react list components - javascript

I have the React app below (jsfiddle):
const ListItem = (props) => {
return (
<div className={props.active ? "active" : ""}>Item {props.index}</div>
)
}
const initialItems = ["item1", "item2", "item3", "item4", "item5"]
const App = (props) => {
const [activeIndex, setActiveIndex] = React.useState(0);
const goUp = () => {
if(activeIndex <= 0) return;
setActiveIndex(activeIndex - 1);
}
const goDown = () => {
if(activeIndex >= initialItems.length - 1) return;
setActiveIndex(activeIndex + 1);
}
return (
<div>
<p>
<button onClick={goUp}>Up</button>
<button onClick={goDown}>Down</button>
</p>
<div>
{initialItems.map((item, index) => (
<ListItem active={index === activeIndex} index={index} key={index} />
))}
</div>
</div>
)
}
ReactDOM.render(
<App />,
document.getElementById('container')
);
Using buttons you can highlight the current list element. The issue with the current approach is that on every active index change it re-renders the full list. In my case, the list might be very big (hundreds of items) with a more complicated layout, which introduces performance problems.
How might this code be modified so it updates only specific list item components and doesn't trigger re-render of all others? I'm looking for a solution without third-party libraries and without direct DOM manipulations.

You can wrap ListItem with React.memo() as here.
This is your ListItem component,
const ListItem = (props) => {
return (
<div className={props.active ? "active" : ""}>Item {props.index}</div>
)
};
By using React.Memo(),
const ListItem = React.memo((props) => {
return (
<div className={props.active ? "active" : ""}>Item {props.index}</div>
)
});
In this case ListItem is only rendered when props gets changed.
See for updated JsFiddle and check with console.log() s.

Related

TextInput gets unfocused after typing each character

I'm using React to build a form and I'm trying to filter a list with the SearchInput (which works the same as TextInput) located in the child component Header. But everytime I type a character the SearchInput gets unfocused
function index() {
const list = [//data\\]
const [search, setSearch] = useState("");
const [filteredResults, setFilteredResults] = useState([]);
const searchItems = (searchValue) => {
setSearch(searchValue);
if (search !== "") {
const filteredData = partners.filter((item) => {
return Object.values(item)
.join("")
.toLowerCase()
.includes(search.toLowerCase());
});
setFilteredResults(filteredData);
} else {
setFilteredResults(partners);
}
};
const Header = () => (
<Box>
<SearchInput
placeholder="Search"
value={search}
onChange={(e) => searchItems(e.target.value)}
/>
</Box>
);
return (
<Parent
headerContent={<Header />}
>
<Box>
<Table data={search.length > 1 ? filteredResults : list} />
</Box>
</Parent>
);
}
export default index;
Oh, I think I can see the problem now - it's the way you're rendering the <SearchInput /> component. You're inadvertantly creating a new functional component on every render. Either inline the Header directly into the Parent control's headerContent property, or create an entirely separate component:
const Header = ({ search, onSearchChange }) => {
const handleChange = (e) => onSearchChange(e.target.value);
return (
<Box>
<SearchInput
placeholder="Search"
value={search}
onChange={handleChange}
/>
</Box>
);
}
function index() {
// ----- 8< -----
return (
<Parent
headerContent={<Header search={search} onSearchChange={searchItems} />}
>
{/* ... */}
</Parent>
);
}
While you're there, you have a subtle bug with your comparison - it looks like you're searching your partners effectively as a list of strings; but, since you're joining them, if you had partners with the names:
'one'
'two'
You're creating a search string as 'onetwo' - so searching for 'et' would match, even though you don't actually have a partner matching that. You can fix that by just checking each partner individually... something like:
const searchItems = (searchValue) => {
setSearch(searchValue);
if (search !== "") {
const searchValueLower = searchValue.toLowerCase();
const filteredData = partners.filter((item) => {
return Object.values(item)
.some(item => item.toLowerCase().includes(searchValueLower);
});
setFilteredResults(filteredData);
} else {
setFilteredResults(partners);
}
};

React: How can I remove a specific div element after clicking its associated button?

I have this component which adds a div and its elements to the dom on button click. The adding part works fine as expected but the issue arises when I want to delete.
Right now when I click on the delete button, it does remove the item but it doesn't remove that specific item which the button is associated with. It just removes the div from the top or bottom.
I have been trying to remove that specific div whose button has been clicked to remove. How can I achieve that?
Here's the CodeSandbox.
And here's the code:
import { useState } from "react";
const App = () => {
const [ counter, setCounter ] = useState( 1 );
const handleAddDiv = () => {
setCounter( counter + 1 );
};
const handleRemoveDiv = () => {
setCounter( counter - 1 );
};
return (
<div className="App">
{
Array.from(Array( counter )).map(( item, idx ) => (
<div>
<div>
<input type="text" />
<button onClick={handleRemoveDiv}>Remove</button>
</div>
</div>
))
}
<button onClick={handleAddDiv}>Add</button>
</div>
);
}
export default App;
This is not prefered react way of doing things, but this will work:
import "./styles.css";
import { useState } from "react";
const App = () => {
const [counter, setCounter] = useState(1);
const handleAddDiv = () => {
setCounter(counter + 1);
};
const removeNode = (idx) => document.getElementById(`id-${idx}`).remove();
return (
<div className="App">
{Array.from(Array(counter)).map((item, idx) => (
<div key={idx} id={`id-${idx}`}>
<div>
<input type="text" />
<button onClick={() => removeNode(idx)}>Remove</button>
</div>
</div>
))}
<button onClick={handleAddDiv}>Add</button>
</div>
);
};
export default App;
Generaly if you would like to have it made correactly then you would want to map on a real array and have every item in array eighter having an unique id or simply use map index and then based on which item you click write a function to remove from that array our specific element.
Map over an array of unique Ids
First of all, you should map over an array of items instead of an integer value.
So, on click of add button, you should push a unique ID to the array of items where each ID would denote an item being rendered in your app.
Now, when you click on remove button, you would need to remove that ID from the array of items, which would result in "deletion" of that div from the app.
In my case, I have considered timestamp as a unique ID but should explore other options for generating unique IDs. Working with indices is anti pattern in React especially when you are mapping over an array in JSX as you would encounter issues at one point of time. So, it's a good idea to maintain unique Ids.
Note: Damian's solution is not ideal as DOM Manipulation is avoided in React.
const { useState, useCallback } = React;
const Item = ({ id, removeDiv }) => {
const clickHandler = useCallback(() => {
removeDiv(id);
}, [id, removeDiv]);
return (
<div>
<input type="text" />
<button onClick={clickHandler}>Remove</button>
</div>
);
};
const App = () => {
const [items, setItems] = useState([]);
const addDiv = useCallback(() => {
// using timestamp as a unique ID
setItems([...items, new Date().getTime()]);
}, [items]);
const removeDiv = useCallback((itemId) => {
// filter out the div which matches the ID
setItems(items.filter((id) => id !== itemId));
}, [items]);
return (
<div className="app">
{items.map((id) => (
<Item key={id} id={id} removeDiv={removeDiv} />
))}
<button onClick={addDiv}>Add</button>
</div>
);
};
ReactDOM.render(<App />,document.getElementById("react"));
.app {
text-align: center;
font-family: sans-serif;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I would use an array state instead of a counter state, because otherwise you don't know which element has to be removed.
import { useState } from "react";
import "./styles.css";
let counter = 1;
export default function App() {
const [array, setArray] = useState([0]);
const handleAddDiv = () => {
setArray((prev) => [...prev, counter++]);
};
const handleRemoveDiv = (idx) => {
var arrayCopy = [...array];
arrayCopy.splice(idx, 1);//remove the the item at the specific index
setArray(arrayCopy);
};
return (
<div className="App">
{array.map((item, idx) => (
<div key={item}>
<div>
<input type="text" />
<button onClick={()=>handleRemoveDiv(idx)}
>Remove</button>
</div>
</div>
))}
<button onClick={handleAddDiv}>Add</button>
</div>
);
}
When I am adding a new item, I give it the value counter++, because I will use it as a key, and a key should be unique.

React JS pass the data or child component to parent component

Is it possible to pass the data from the child component to the parent component using props?
-Parent component
--- ItemList component.
--- DisplatSelect component from the itemList component
I have a list of item in the child component which came from to the parent component, then I want to send the index of the selected data to the other child component located in the parent component.
Can't example well, kindly see the attached screenshot for other references.
Thanks a lot!
enter image description here
You can keep the data in the Parent component and use a function to pass the props from the child to the Parent. This concept is called Lifting State Up where you define the state at the highest common ancestor so all the child components are using the same data which in this case is the selecetd item
function Parent() {
const [selectedItem, setSelectedItem] = useState(null);
const data = []; // Your Data
return (
<>
<h1>Your selected Item = {selectedItem}</h1>
{data.map((item) => {
<Child item={item} setSelectedItem={setSelectedItem} />;
})}
</>
);
}
function Child({ item, setSelectedItem }) {
return <Button onClick={() => setSelectedItem(item.id)}> {item} </Button>;
}
The simplest way, I think, is for the child component where the selection is made to accept a function properly, something like onSelectionChanged. If you had a button for each item passed to the child you could do something like:
Child Component A
const ChildA = ({ items, onSelectionChanged }) => {
return (
<div>
{items.map((item, index) => (
<button onClick={() => onSelectionChanged(index)}>Item</button>
))}
</div>
)
}
Child Component B
const ChildB = ({ selectedItem }) => {
return (
<div>
Selected {selectedItem}
</div>
)
}
Parent Component
const Parent = () => {
const [selection, sets election] = useState({});
const onSelectionChanged = index => {
console.log(`ChildA selection changed: ${index}`);
}
return (
<div>
<ChildA items={items} onSelectionChanged={onSelectionChanged} />
<ChildB selectedItem={selection} />
</div>
)
}
So when your child component handles a change in the selection, it invokes the function passed as a prop onSelectionChanged. You can pass whatever data you want from ChildA to that function.
Note that the parent Component keeps the selected value (from ChildA) in local state, then passes that value to ChildB via a prop.
You can have a state variable in the parent component and pass it to child components to share data between them. I'll post a sample code block on how you can do this for your case.
export default function ParentComponent (props) {
const data = ['image_1_url', 'image_2_url', ...] // Data for the images
const [selectedIndex, setSelectedIndex] = useState(-1); // Selected index (-1 represents no selection)
return (
<ImageList data={data} selectImage={setSelectedIndex} />
{(selectedIndex !== -1) ? (<SelectedImage data={data[selectedIndex]} />) : (<No ImageSelected/>)}
);
}
And the image list component can then use the selectImage prop to select the image
export default function ImageList (props) {
return (
<div>
props.data.map((imageUrl, index) => (
<div onClick={() => {props.setSelected(index)}}>
<img src={imageUrl}/>
</div>
))
</div>
);
}
Yes it's possible. We have one parent state value and update every on click child component to the component.
import React, { useState } from "react";
const Child1 = (props) => {
return (
props.items.map( (item, index) => (
<button key={index.toString()} onClick={() => { props.updateIndex(item.id) }}>
{item.name}
</button>
) )
)
}
const Child2 = (props) => {
return (
<h1>Item selected: {props.selectItem}</h1>
)
}
const ParentComponent = () => {
const listItems = [
{
id:1,
name: "sample name 1"
},
{
id:2,
name: "sample name 2"
}
]
const [selectItem, setSelectItem] = useState('None');
return (
<>
<Child1 items={listItems} updateIndex={setSelectItem}/>
<Child2 selectItem={selectItem}/>
</>
)
}
export default function App() {
return (
<div className="App">
<ParentComponent/>
</div>
);
}

How to.close accordion when clicked for the second time?

import React, { useState } from 'react';
const Accordion = ({ items }) => {
const [activeIndex, setActiveIndex] = useState(null);
const onTitleClick = (index) => {
setActiveIndex(index);
};
const renderedItems = items.map((item, index) => {
const active = index === activeIndex ? 'active' : '';
return (
<React.Fragment key={item.title}>
<div className={`title ${active}`} onClick={() => onTitleClick(index)}>
<i className='dropdown icon'></i>
{item.title}
</div>
<div className={`content ${active}`}>
<p>{item.content}</p>
</div>
</React.Fragment>
);
});
return <div className='ui styled accordion'>{renderedItems}</div>;
};
export default Accordion;
I created an accordion using Semantic UI library.
I was able to set class of dropdown "active" so anything that I click will expand.
I am trying to implement "Close on second click" functionality to the below code,
so I try to implement by adding following code under onTitleClick function:
const onTitleClick = (index) => {
if (index === activeIndex) {
setActiveIndex(null); // in case 'clicked index' is the same as what's active, then configure it to "null" and rerender
}
setActiveIndex(index);
};
My understanding is whenever the state updates, react will run its script again, so in this particular situation, that variable "active" should return empty string if clicked for the same Index.
Rather than my expectation, nothing happened when I clicked it for the second time.
I tried to console.log in the if statement and it will show that I have clicked the item for the second time.
Please advise.
The issue is here:
const onTitleClick = (index) => {
if (index === activeIndex) {
setActiveIndex(null); // in case 'clicked index' is the same as what's active, then configure it to "null" and rerender
}
setActiveIndex(index); <-- here
}
what happening here is if if condition matches then it sets to setActiveIndex null but code will run so again it sets to setActiveIndex(index). That's why click is not working . You need to do this:
const onTitleClick = (index) => {
if (activeIndex === index) {
setActiveIndex(null);
} else {
setActiveIndex(index);
}
};
Here is the demo: https://codesandbox.io/s/musing-poitras-bkiwh?file=/src/App.js:270-418
Try the below approach,
const onTitleClick = (index) => {
setActiveIndex(activeIndex !== index ? index : null);
};
I have tried with react class component so worked it.
import React, { Component } from 'react';
class Accordion extends Component {
state = {
activeIndex: null
}
onTitleClick = (index) => event => {
const { activeIndex } = this.state;
this.setState({ activeIndex: activeIndex == index ? null : index })
};
render() {
const { activeIndex } = this.state;
const{items}=this.props;
return <div className='ui styled accordion'>
{
items.map((item, index) => {
const active = index === activeIndex ? 'active' : '';
return <React.Fragment key={item.title}>
<div className={`title ${active}`} onClick={this.onTitleClick(index)}>
<i className='dropdown icon'></i>
{item.title}
</div>
<div className={`content ${active}`}>
<p>{item.content}</p>
</div>
</React.Fragment>
})
}
</div>
}
}
export default Accordion;

Changing the order of an item in a list using ReactJS

I have created a list using material UI and reactjs, and when a new element is added to the list the new element will go on top of the list.
I have a requirement where when I click on an element on the list the element should be strike-through and that now that element should be listed on the bottom of the list.
I was able to strike-through the element when clicked, but I am confused as to how to bring the element to the bottom of the list
How should I approach this problem?
The code of the listlayout.js is presented here, In this code, the added items are listed, and I need to find the way to change the list order when an element is stricked
app.js
class App extends Component {
constructor(props) {
super(props);
this.state={
items:[],
newItem:{
id:'',
itemText:''
},
updateItem:false
};
this.handleInput = this.handleInput.bind(this);
this.addItem = this.addItem.bind(this);
}
handleInput = e =>{
this.setState({
newItem:{
id:1 + Math.random(),
itemText: e.target.value
}
});
};
addItem = e =>{
e.preventDefault();
const typedItem = this.state.newItem;
if(typedItem.itemText !==""){
const typedItems=[...this.state.items,typedItem];
this.setState({
items:typedItems,
newItem:{
id:'',
itemText: ''
},
updateItem:false
})
}
};
render() {
return (
<div >
<HeaderBar/>
<ListLayout items={this.state.items}
/>
</div>
);
}
}
export default App;
ListLayout.js
const ToDoList = props => {
const clearList = props.clearList;
const deleteItem = props.deleteItem;
const updateItem = props.updateItem;
const strikeList = props.strikeList;
const listItems = item => {
return <div key={item.id}>{item.itemText}</div>;
};
const completed = id => {
document.getElementById(id).style.textDecoration = "line-through";
return true;
};
const strikeTextMethod = id => {
completed(id);
};
return (
<div>
<Grid container justify="center" alignContent="center">
<Grid item xs={12} md={6}>
<Typography variant="h6" className={classes.title}>
To do List
</Typography>
<div className={classes.demo}>
<List dense={dense}>
{items
.slice(0)
.reverse()
.map(x => (
<ListItem
key={x.id}
button
id={x.id}
onClick={() => strikeTextMethod(x.id)}
divider
>
<ListItemText primary={listItems(x)} />
<ListItemSecondaryAction></ListItemSecondaryAction>
</ListItem>
))}
</List>
</div>
</Grid>
</Grid>
<br />
</div>
);
};
export default ToDoList;
You have to mainatain the strike event for each item in the array. You can add an additional property to the array items, like isStriked or status.. something like that.
Then you can sort them accordingly..
Your code doesn't seem to be the entire solution. I don't see the definition of items as an example.
but something like this could be a workaround.
const ToDoList = props => {
const [items, setItems] = React.useState(props.items || []); // Initial values
// Maybe you need to these lines to sync the items state.
React.useEffect(
() => {
setItems(items)
},
[props.items]
)
const completed = id => {
document.getElementById(id).style.textDecoration = "line-through";
return true;
};
const strikeTextMethod = id => {
const index = items.findIndex(x => x.id === id);
const newItems = [items[index], ...items.slice(0, index - 1), ...items.slice(index + 1)]
setItems(newItems);
completed(id);
};
return (
)
}

Categories

Resources