How to avoid rerendering all list items - javascript

On every click react rerender every item. How to avoid it? I want react to only render items that have changed. I tried using react memo and usecallback but it didn't help. I can't understand what is the reason. What are the ways to solve this problem? Thank you.
App.ts
import React, { useState } from "react";
import "./styles.css";
import ListItem from "./ListItem";
const items = [
{ id: 1, text: "items1" },
{ id: 2, text: "items2" },
{ id: 3, text: "items3" },
{ id: 4, text: "items4" },
{ id: 5, text: "items5" },
{ id: 6, text: "items6" },
{ id: 7, text: "items7" }
];
export default function App() {
const [activeIndex, setActiveIndex] = useState(1);
const onClick = (newActiveIndex: number) => {
setActiveIndex(newActiveIndex);
};
return (
<div className="App">
{items.map(({ id, text }) => (
<ListItem
key={id}
id={id}
text={text}
activeId={activeIndex}
onClick={onClick}
/>
))}
</div>
);
}
Item.ts
import React from "react";
interface ListItemProps {
id: number;
activeId: number;
text: string;
onClick: (newActiveIndex: number) => void;
}
const ListItem: React.FC<ListItemProps> = ({ id, activeId, text, onClick }) => {
const isActive = activeId === id;
const style = {
marginRight: "42px",
marginTop: "24px",
color: isActive ? "green" : "red"
};
console.log(`update id: ${id}`);
return (
<div>
<span style={style}>id: {id}</span>
<span style={style}>is active: {isActive ? "active" : "inactive"}</span>
<span style={style}>text: {text}</span>
<button onClick={() => onClick(id)}>setActive</button>
</div>
);
};
export default ListItem;

activeIndex changes with each click, so all of the components have new prop values and will need to re-render.
Instead of passing the activeIndex to every component every time:
const ListItem: React.FC<ListItemProps> = ({ id, activeId, text, onClick }) => {
const isActive = activeId === id;
Pass an isActive to each component:
const ListItem: React.FC<ListItemProps> = ({ id, isActive, text, onClick }) => {
Effectively moving the calculation of the bool from the component to the consuming code:
<ListItem
key={id}
id={id}
text={text}
isActive={activeIndex === id}
onClick={onClick}
/>
Then memoization (coupled with useCallback for the onClick handler) should work because most components (the ones which aren't changing their "active" state) won't receive new props and won't need to re-render.

Even with memoization it won't work, because your onClick function changes your components state, and your children components depend on activeIndex value, so every time you click you change the components state, and when state changes, the component re-renders itself and it's children, if your children didn't depend on your state, they wouldn't re-render (if you use memoization).

Plus to David's answer i would return ListItem in React.memo React.memo(ListItem)

Related

Side Navigation (Shopify Polaris Library) active item selection

I'm trying to show the active side navigation items in Shopify Polaris Navigation components.
Here I have tried in few ways, problem is when I'm clicking nav items 2 times then showing the currently active nav link!
Can you suggest me better efficient code to solve this problem? If any more dynamic solutions available please suggest!
import React, { Fragment, useState } from "react";
import { Frame, Navigation, Page } from "#shopify/polaris";
import { GiftCardMinor } from "#shopify/polaris-icons";
import { useNavigate } from "react-router-dom";
const LeftNavigation = ({ pageTitle, loading, children }: any) => {
const [selected, setSelected] = useState([false, false]);
const navigate = useNavigate();
const handleSelect = (index) => {
// const newFilter = [...selected];
// newFilter.fill(false, 0, newFilter.length);
// newFilter[index] = true;
// setSelected(newFilter);
const newArray = selected.map((item, itemIndex) => {
return itemIndex === index
? (selected[index] = true)
: (selected[itemIndex] = false);
});
setSelected(newArray);
};
return (
<Fragment>
<Frame
navigation={
<div style={{ marginTop: "4rem" }}>
<Navigation location="/">
<Navigation.Section
title="nav items"
items={[
{
label: "one",
icon: icon1,
selected: selected[0],
onClick: () => {
handleSelect(0);
navigate("/one");
},
},
{
label: "two",
icon: icon2,
selected: selected[1],
onClick: () => {
handleSelect(1);
navigate("/two");
},
},
]}
/>
</Navigation>
</div>
}
></Frame>
</Fragment>
);
};
export default LeftNavigation;
In order to make the "active" state more dynamic it should probably only store the id of the active menu item. When mapping/rendering the menu items check explicitly against the current state value. The onClick handler should pass the currently clicked-on item's "id" value to the callback handler.
Example using the "target path" as the id value*:
const LeftNavigation = ({ pageTitle, loading, children }: any) => {
const [selected, setSelected] = useState<string | null>(null);
const navigate = useNavigate();
const handleSelect = (selectedId) => {
setSelected(selected => selected === selectedId ? null : selectedId);
};
return (
<Fragment>
<Frame
navigation={
<div style={{ marginTop: "4rem" }}>
<Navigation location="/">
<Navigation.Section
title="nav items"
items={[
{
label: "one",
icon: icon1,
selected: selected === "/one",
onClick: () => {
handleSelect("/one");
navigate("/one");
},
},
{
label: "two",
icon: icon2,
selected: selected === "/two",
onClick: () => {
handleSelect("/two");
navigate("/two");
},
},
]}
/>
</Navigation>
</div>
}
></Frame>
</Fragment>
);
};
*Note: the id value can be anything, but you should ensure it's unique per menu item if you want up to only one active at-a-time.

How to build a react button that stores the selection in an array

I am trying to create a list of buttons with values that are stored in a state and user is only allowed to use 1 item (I dont want to use radio input because I want to have more control over styling it).
import React from "react";
import { useEffect, useState } from "react";
import "./styles.css";
const items = [
{ id: 1, text: "Easy and Fast" },
{ id: 2, text: "Easy and Cheap" },
{ id: 3, text: "Cheap and Fast" }
];
const App = () => {
const [task, setTask] = useState([]);
const clickTask = (item) => {
setTask([...task, item.id]);
console.log(task);
// how can I make sure only 1 item is added to task
// and remove the other items
// only one option is selectable all the time
};
const chosenTask = (item) => {
if (task.find((v) => v.id === item.id)) {
return true;
}
return false;
};
return (
<div className="App">
{items.map((item) => (
<li key={item.id}>
<label>
<button
type="button"
className={chosenTask(item) ? "chosen" : ""}
onClick={() => clickTask(item)}
onChange={() => clickTask(item)}
/>
<span>{item.text}</span>
</label>
</li>
))}
</div>
);
};
export default App;
https://codesandbox.io/s/react-fiddle-forked-cvhivt?file=/src/App.js
I am trying to only allow 1 item to be added to the state at all the time, but I dont know how to do this?
Example output is to have Easy and Fast in task state and is selected. If user click on Easy and Cheap, select that one and store in task state and remove Easy and Fast. Only 1 item can be in the task state.
import React from "react";
import { useEffect, useState } from "react";
import "./styles.css";
const items = [
{ id: 1, text: "Easy and Fast" },
{ id: 2, text: "Easy and Cheap" },
{ id: 3, text: "Cheap and Fast" }
];
const App = () => {
const [task, setTask] = useState();
const clickTask = (item) => {
setTask(item);
console.log(task);
// how can I make sure only 1 item is added to task
// and remove the other items
// only one option is selectable all the time
};
return (
<div className="App">
{items.map((item) => (
<li key={item.id}>
<label>
<button
type="button"
className={item.id === task?.id ? "chosen" : ""}
onClick={() => clickTask(item)}
onChange={() => clickTask(item)}
/>
<span>{item.text}</span>
</label>
</li>
))}
</div>
);
};
export default App;
Is this what you wanted to do?
Think of your array as a configuration structure. If you add in active props initialised to false, and then pass that into the component you can initialise state with it.
For each task (button) you pass down the id, and active state, along with the text and the handler, and then let the handler in the parent extract the id from the clicked button, and update your state: as you map over the previous state set each task's active prop to true/false depending on whether its id matches the clicked button's id.
For each button you can style it based on whether the active prop is true or false.
If you then need to find the active task use find to locate it in the state tasks array.
const { useState } = React;
function Tasks({ config }) {
const [ tasks, setTasks ] = useState(config);
function handleClick(e) {
const { id } = e.target.dataset;
setTasks(prev => {
// task.id === +id will return either true or false
return prev.map(task => {
return { ...task, active: task.id === +id };
});
});
}
// Find the active task, and return its text
function findSelectedItem() {
const found = tasks.find(task => task.active)
if (found) return found.text;
return 'No active task';
}
return (
<section>
{tasks.map(task => {
return (
<Task
key={task.id}
taskid={task.id}
active={task.active}
text={task.text}
handleClick={handleClick}
/>
);
})};
<p>Selected task is: {findSelectedItem()}</p>
</section>
);
}
function Task(props) {
const {
text,
taskid,
active,
handleClick
} = props;
// Create a style string using a joined array
// to be used by the button
const buttonStyle = [
'taskButton',
active && 'active'
].join(' ');
return (
<button
data-id={taskid}
className={buttonStyle}
type="button"
onClick={handleClick}
>{text}
</button>
);
}
const taskConfig = [
{ id: 1, text: 'Easy and Fast', active: false },
{ id: 2, text: 'Easy and Cheap', active: false },
{ id: 3, text: 'Cheap and Fast', active: false }
];
ReactDOM.render(
<Tasks config={taskConfig} />,
document.getElementById('react')
);
.taskButton { background-color: palegreen; padding: 0.25em 0.4em; }
.taskButton:not(:first-child) { margin-left: 0.25em; }
.taskButton:hover { background-color: lightgreen; cursor: pointer; }
.taskButton.active { background-color: skyblue; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Toggle selected/active state with react-bootstrap ListGroup

I'm building a multiple selection in React with a react-bootstrap ListGroup.
Despite having the active property of an item set to false though, the last clicked item still gets active in its class list.
I've been considering getting a reference to the list item and manipulate the class list, but that is too dirty.
I tried changing the variant attribute of the ListGroup.Item, but the active class overrides that. I'd prefer to not modify the css class definition.
I prefer using the onSelect handler of the ListGroup instead of using the ToggleButton's onClick event from a usability perspective.
Have you tried using a ListGroup to manipulate multiple selection?
Here is stripped version of the code:
import { useState } from "react";
import "./styles.css";
import ListGroup from "react-bootstrap/ListGroup";
import Container from "react-bootstrap/Container";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import ToggleButton from "react-bootstrap/ToggleButton";
import "bootstrap/dist/css/bootstrap.min.css";
import { cloneDeep } from "lodash";
export default function App() {
const allItems = [
{ id: 1, title: "A" },
{ id: 2, title: "B" },
{ id: 3, title: "C" },
{ id: 4, title: "D" },
{ id: 5, title: "E" }
];
const [selectedItems, setSelectedItems] = useState([
{ id: 2, title: "B" },
{ id: 4, title: "D" }
]);
function toggleSelection(eventKey, e) {
const itemId = parseInt(eventKey?.replace(/^itemSelect_/, ""), 10);
const newSelectedItems = cloneDeep(selectedItems);
const indexInSelection = selectedItems.findIndex(
(sitm) => sitm.id === itemId
);
if (indexInSelection >= 0) {
newSelectedItems.splice(indexInSelection, 1);
} else {
const newItem = allItems.find((itm) => itm.id === itemId);
newSelectedItems.push(newItem);
}
setSelectedItems(newSelectedItems);
}
return (
<div className="App">
<ListGroup onSelect={toggleSelection}>
{allItems.map((itm) => {
return (
<ListGroup.Item
eventKey={`itemSelect_${itm.id}`}
key={`itemSelect${itm.id}`}
active={
selectedItems.find((sitm) => sitm.id === itm.id) !== undefined
}
>
<Container>
<Row>
<Col sm="2">
{" "}
<ToggleButton
className="mx-1"
type="checkbox"
size="sm"
key={`check${itm.id}`}
checked={
selectedItems.find((sitm) => sitm.id === itm.id) !==
undefined
}
value={itm.id}
></ToggleButton>
</Col>
<Col sm="10">{itm.title}</Col>
</Row>
</Container>
</ListGroup.Item>
);
})}
</ListGroup>
</div>
);
}
And a live preview is available at Code Sandbox
Thanks in advance for your help!
It's something to do with eventKey and onSelect - ListGroup and ListGroup.Item have some internal implementations on various user actions as you can see in docs. Try this implementation instead. I removed onSelect on ListGroup and added onClick event on ListGroup.Item where we don't need eventKey anymore and pass itm.id directly.

Why checkboxes dont change from checked to unchecked on click?

My React App doesn't work like it should be. The problem is that the checkboxes dont change at all.
I managed to show the checked boxes (the ones with the property of completed=true) and debugging it seems that it works fine when I click but for some reason the box that needs to be changed automatically re-changes on its own.
Do you have any idea why ?
//APP.JS
import React from "react"
import './App.css';
import Header from "./Header"
import TodoItem from "./todoItem";
import todosData from "./todosData"
class App extends React.Component {
constructor() {
super()
this.state = {
todos: todosData
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(id) {
this.setState((prevState) => {
const newArray = prevState.todos.map((elem) => {
if(elem.id === id) {
elem.completed = !(elem.completed)
}
return elem
})
return {
todos: newArray
}
})
}
render() {
const todosArray = this.state.todos.map(item =>
<TodoItem
key={item.id}
item={item}
handleChange={this.handleChange}
/>)
return (
<div className="App">
<Header />
<div className="container">
{todosArray}
</div>
</div>
)
}
}
export default App;
//TODOITEM.JS
import React from "react"
function TodoItem(props) {
return (
<div className="elem-container">
<input type="checkbox"
checked={props.item.completed}
onChange={() => props.handleChange(props.item.id)}
/>
<span className="span-container">{props.item.text}</span>
</div>
)
}
export default TodoItem
//TODOSDATA.JS
const todosData = [
{
id: 1,
text: "Take out the trash",
completed: true
},
{
id: 2,
text: "Grocery shopping",
completed: false
},
{
id: 3,
text: "Clean gecko tank",
completed: false
},
{
id: 4,
text: "Mow lawn",
completed: true
},
{
id: 5,
text: "Catch up on Arrested Development",
completed: false
}
]
export default todosData
Thank you for the help in advance
you need to change two things and it will work just fine
first:
inside todoItem.js
onChange={(e) => props.handleChange(e,props.item.id)}
second:
inside the parent file
handleChange(event, id) {
this.setState((prevState) => {
const newArray = prevState.todos.map((elem) => {
if(elem.id === id) {
elem.completed = event.target.checked
}
return elem
})
return {
todos: newArray
}
})
}
now everything will work as you expected
have a nice day
I'm no expert as I'm learning React myself but looking at the code handleChange(id) doesn't have an else state in its 'if' statement, have you tried adding?
Adding to #mouheb answer, you can simplify one more step. you don't need to map the all elements to update single item. you can change directly item (if it is mutable).
// todoItem.js
onChange={(e) => props.handleChange(e, props.item) }
// parent file
handleChange(event, prop) {
prop.completed = event.target.
this.setState({ todos: this.state.todos }) or this.setState({ todos: [...this.state.todos] })
}

React - interview exercise

I got the following React exercise with 2 components in an interview that I did not manage to make it compile...
The question was the following:
Update the Counter component to take onIncrement callbacks as props and ensure they update the counter's values independently. Each callback should take a single, integer value as a parameter which is the amount to increment the counter's existing value by.
Comments in the code but the my problem is how to implement the "onIncrement" function.
const { Component } = React;
const { render } = ReactDOM;
// state data for 3 counters
const data = [
{ id: 1, value: 1 },
{ id: 2, value: 2 },
{ id: 3, value: 3 }
];
// Counter Component
class Counter extends Component {
render() {
const { value } = this.props;
return (
<div className="counter">
<b>{value}</b>
<div className="counter-controls">
<button className="button is-danger is-small">-</button>
//I call the function passed
<button className="button is-success is-small" onClick={()=>{onIncrement(this.props.value)}}>+</button>
</div>
</div>
);
}
}
class App extends Component {
constructor(props, context) {
super(props, context);
}
onIncrement=(value)=>{
//I tried several things here but I did not manage to make it work. I guess that I need also the id of the object...
}
render() {
return (
<div>
{data.map(counter => (
<Counter
key={counter.id}
value={counter.value}
//I pass the callback function to the component
onIncrement={this.onIncrement}
/>
))}
</div>
);
}
}
render(
<App/>
, document.querySelector('#root'))
Basically, you'll want to use the id as a way to determine which value you need to update. How you have it set up, you won't be able to know which value needs to be updated (because you don't know which id was clicked) nor will the value be saved.
NOTE: The example below takes the id from event.target.id and the value from event.target.value which is then deconstructed in the handleChange callback. This is a more common and elegant solution than passing a value to a callback and then passing it and another value to another callback (more work, more code, but same functionality).
Best solution: https://codesandbox.io/s/rjmx8vw99p
components/UpdateQuantity.js
import React, { Component, Fragment } from "react";
export default class App extends Component {
state = {
items: [
{ id: "Apples", quantity: 0 },
{ id: "Strawberries", quantity: 0 },
{ id: "Grapes", quantity: 0 },
{ id: "Apricots", quantity: 0 }
]
};
handleChange = ({ target: { id, value } }) => {
this.setState(prevState => ({
items: prevState.items.map(item => {
const nextVal = item.quantity + ~~value; // ~~ === parseInt(val, 10) -- required because the "value" is turned into a string when placed on a DOM element
return id === item.id
? { id, quantity: nextVal > 0 ? nextVal : 0 }
: { ...item };
})
}));
};
render = () => (
<div className="container">
<h1>Updating Values Inside Array</h1>
{this.state.items.map(({ id, quantity }) => (
<div key={id} className="container">
<div>
{id} ({quantity})
</div>
<button
id={id}
value={1}
style={{ marginRight: 10 }}
className="uk-button uk-button-primary"
onClick={this.handleChange}
>
+
</button>
<button
id={id}
value={-1}
style={{ marginRight: 10 }}
className="uk-button uk-button-danger"
onClick={this.handleChange}
>
-
</button>
</div>
))}
</div>
);
}
Another solution: https://codesandbox.io/s/yq961275rv (not recommended as it requires an extra component and an extra callback -- BUT there's no binding required in the render method nor is there an anonymous function () => {} in the onClick callback)
components/UpdateQuantity.js
import React, { Component, Fragment } from "react";
import Button from "./button";
export default class App extends Component {
state = {
items: [
{ id: "Apples", quantity: 0 },
{ id: "Strawberries", quantity: 0 },
{ id: "Grapes", quantity: 0 },
{ id: "Apricots", quantity: 0 }
]
};
handleChange = (id, val) => {
this.setState(prevState => ({
items: prevState.items.map(item => {
const nextVal = item.quantity + val;
return id === item.id
? { id, quantity: nextVal > 0 ? nextVal : 0 }
: { ...item };
})
}));
};
render = () => (
<div className="container">
<h1>Updating Values Inside Array</h1>
{this.state.items.map(props => (
<div key={props.id} className="container">
<div>
{props.id} ({props.quantity})
</div>
<Button
{...props}
className="uk-button uk-button-primary"
handleChange={this.handleChange}
value={1}
>
+
</Button>
<Button
{...props}
disabled={props.quantity === 0}
className="uk-button uk-button-danger"
handleChange={this.handleChange}
value={-1}
>
-
</Button>
</div>
))}
</div>
);
}
components/button.js
import React, { PureComponent } from "react";
import PropTypes from "prop-types";
export default class Button extends PureComponent {
static propTypes = {
children: PropTypes.string.isRequired,
className: PropTypes.string,
disabled: PropTypes.bool,
id: PropTypes.string.isRequired,
handleChange: PropTypes.func.isRequired,
value: PropTypes.number.isRequired
};
handleClick = () => {
this.props.handleChange(this.props.id, this.props.value);
};
render = () => (
<button
disabled={this.props.disabled || false}
className={this.props.className}
onClick={this.handleClick}
style={{ marginRight: 10 }}
>
{this.props.children}
</button>
);
}
I know an answer has been accepted, but it doesn't actually satisfy the requirements fully, i.e.
Each callback should take a single, integer value as a parameter which is the amount to increment the counter's existing value by.
The accepted answer takes the event object as a parameter which is not the specified requirement. The only way to strictly satisfy the expected requirement is to bind unique
"...onIncrement callbacks as props..."
for each counter. This approach has drawbacks and performance implications as discussed in this article
Working example at https://codesandbox.io/s/j2vxj620z9

Categories

Resources