I've got the following state:
const [places, setPlaces] = useState(false)
const [selectedPlaces, setSelectedPlaces] = useState([])
I asynchronously populate places by calling an API that returns an array of objects that looks something like:
[
{name: "Shop #1", id: 1},
{name: "Shop #2", id: 2}
]
My goal is to render these objects, and have their ID to be added/removed from the selectedPlaces state.
Render:
return (
<div>
<div>
You have selected {selectedPlaces.length} total places
</div>
(places === false)
? <div>Loading...</div>
: places.map(place => { // render places from the places state when loaded
let [name, id] = [place.name, place.id]
return <div onClick={() => {
setSelectedPlaces(selected => {
selected.push("dummy data to simplify")
return selected
})
}}>{name}</div>
})
</div>
)
I've removed the key and added dummy data to make the movements simpler.
The problem arises when clicking on a div, the "You have selected ... total places" doesn't refresh until I force a re-render using fast refresh or through other methods (using browser/NextJS). Is this correct behaviour? It's as-if the state isn't being changed, but a console.log on the setSelectedPlaces displays fresh data symbolizing it is being changed.
I've tried:
Creating a useEffect handler for the selectedPlaces state which would setAmtPlaces using the length of the selected places. The same issue arises.
Searched/read-through multiple posts/GitHub issues like this and this
Replacing the list state with true/false in previous times I've encountered this issue, but I cannot use that approach with this problem since it's a dynamic amount of data being loaded.
Add a {} wrapper for the ternary operator:
{
places === false
? (...)
: (....)
}
push mutates the state. Use spread or concat in setSelectedPlaces
setSelectedPlaces(selected =>
selected.concat("abc")
)
let [name, id] = [place.name, place.id] can be change to let { name, id } = place
Here's a snippet:
const { useState } = React;
function App() {
const [places, setPlaces] = useState([
{ name: "Shop #1", id: 1 },
{ name: "Shop #2", id: 2 }
])
const [selectedPlaces, setSelectedPlaces] = useState([])
const onPlaceClick = id => setSelectedPlaces(state => state.concat(id))
return (
<div>
<div> You have selected {selectedPlaces.length} total places </div>
{ (places === false)
? <div>Loading...</div>
: places.map(({ id, name }) =>
<div onClick={_ => onPlaceClick(id)}>{name}</div>
)
}
<span>{selectedPlaces.join()}</span>
</div>
)
}
ReactDOM.render(
<App />,
document.getElementById("react")
);
<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>
Related
I'm building a chat app, I have 3 components from parent to child in this hierarchical order: Chat, ChatLine, EditMessage.
I'm looping through messages state in Chat to display multiple ChatLine components as a list, and I pass some state to ChatLine and then to EditMessage.
I need the state :
const [editValue, setEditValue] = useState("");
const [editingId, setEditingId] = useState(null);
to remain in the parent component Chat so I can have access to it later there.
Anyway, now when I click on the Edit button, the EditMessage component shows a textarea, and I'm setting state onChange in it, but everytime I click the Edit button or type a letter in the textarea all the components rerender as I see in React DevTool Profiler, even the children that didn't get affected, I only need the Chat and affected ChatLine to rerender at most.
The whole code is available in CodeSandbox, and deployed in Netlify.
And here it is here also :
(Chat.js)
import { useEffect, useState } from "react";
import ChatLine from "./ChatLine";
const Chat = () => {
const [messages, setMessages] = useState([]);
const [editValue, setEditValue] = useState("");
const [editingId, setEditingId] = useState(null);
useEffect(() => {
setMessages([
{ id: 1, message: "Hello" },
{ id: 2, message: "Hi" },
{ id: 3, message: "Bye" },
{ id: 4, message: "Wait" },
{ id: 5, message: "No" },
{ id: 6, message: "Ok" },
]);
}, []);
return (
<div>
<p>MESSAGES :</p>
{messages.map((line) => (
<ChatLine
key={line.id}
line={line}
editValue={editValue}
setEditValue={setEditValue}
editingId={editingId}
setEditingId={setEditingId}
/>
))}
</div>
);
};
export default Chat;
(ChatLine.js)
import EditMessage from "./EditMessage";
import { memo } from "react";
const ChatLine = ({
line,
editValue,
setEditValue,
editingId,
setEditingId,
}) => {
return (
<div>
{editingId !== line.id ? (
<>
<span>{line.id}: </span>
<span>{line.message}</span>
<button
onClick={() => {
setEditingId(line.id);
setEditValue(line.message);
}}
>
EDIT
</button>
</>
) : (
<EditMessage
editValue={editValue}
setEditValue={setEditValue}
setEditingId={setEditingId}
editingId={editingId}
/>
)}
</div>
);
};
export default memo(ChatLine);
(EditMessage.js)
import { memo } from "react";
const EditMessage = ({ editValue, setEditValue, editingId, setEditingId }) => {
return (
<div>
<textarea
onKeyPress={(e) => {
if (e.key === "Enter") {
// prevent textarea default behaviour (line break on Enter)
e.preventDefault();
// updating message in DB
updateMessage(editValue, setEditValue, editingId, setEditingId);
}
}}
onChange={(e) => setEditValue(e.target.value)}
value={editValue}
autoFocus
/>
<button
onClick={() => {
setEditingId(null);
setEditValue("");
}}
>
CANCEL
</button>
</div>
);
};
export default memo(EditMessage);
const updateMessage = (editValue, setEditValue, editingId, setEditingId) => {
const message = editValue;
const id = editingId;
// resetting state as soon as we press Enter
setEditValue("");
setEditingId(null);
// ajax call to update message in DB using `message` & `id` variables
console.log("updating..");
};
The problem is that all of the child components see their props change any time any of them is in the process of being edited, because you're passing the current editing information to all of the children. Instead, only pass the current editing text (editValue) to the component being edited, not to all the others.
ChatLine doesn't use editValue when it's not the instance being edited. So I'd do one of two things:
Use a different component for display (ChatLine) vs. edit (ChatLineEdit). Almost the entire body of ChatLine is different depending on whether that line is being edited or not anyway. Then only pass editValue to ChatLineEdit.
Pass "" (or similar) as editValue to the one not being edited. In the map in Chat: editValue={line.id === editingId ? editValue : ""}.
Pass an "are equal" function into memo for ChatLine that doesn't care what the value of editValue is if line.id !== editingId. By default, memo does a shallow check of all props, but you can take control of that process by providing a function as the second argument. For instance:
export default memo(ChatLine, (prevProps, nextProps) => {
// "Equal" for rendering purposes?
return (
// Same chat line
prevProps.line === nextProps.line &&
// Same edit value setter (you can leave this out, setters from `useState` never change)
prevProps.setEditValue === prevProps.setEditValue && // ***
// Same editingId
prevProps.editingId === prevProps.editingId &&
// Same editingId setter (you can leave this out too)
prevProps.setEditingId === prevProps.setEditingId && // ***
(
// Same edit value...
prevProps.editValue === prevProps.editValue ||
// OR, we don't care because we're not being edited
nextProps.line.id !== nextProps.editingId
)
);
});
This is fragile, because it's easy to get the check wrong, but it's another option.
I would go for #1. Not even passing props to components that they don't need is (IMHO) the cleanest approach.
I want to update setTopic without overriding previous state. But I am getting topic is not iterable error.
What I tried?
I tried looking a different examples on stack overflow, But still couldn't figure out how to append updated state without losing previous state.
Also, which is a better way to save multiple topics : an array of objects, simply objects or simply array?
const AddTopic = (props) => {
const { subjectName } = props;
const [topic, setTopic] = useState([
{
topics: [
{
id: Math.random().toString(36).substr(2, 7),
topicName: "topic name",
subject: subjectName,
},
],
},
]);
const addTopicHandler = () => {
setTopic(
[...topic].map((item) => {
return {
...item,
id: Math.random().toString(36).substr(2, 7),
topicName: "another topic name",
subject: subjectName,
};
})
);
};
console.log(topic);
Instead of the child component using state lift the state to a parent component, and then just create dumb Topic components from the state.
Rename your state. Call it topics, and the update function setTopics. Initialise it as an array, not an array containing one object containing an array.
You can't immediately log an updated state. You need to use useEffect to watch for changes in state, and then log something.
const { useEffect, useState } = React;
// Topic component - just gets handed the
// subject (in this example) in the props
function Topic({ subject, category }) {
return <div>{subject}: {category}</div>;
}
function Example() {
// Initialise `topics` as an array
const [ topics, setTopics ] = useState([]);
// When `topics` is updated, log the updated state
useEffect(() => console.log(JSON.stringify(topics)), [topics]);
// Helper function that maps over the state and
// produces an array of topics
function getTopics() {
return topics.map(topic => {
const { subject, category } = topic;
return (
<Topic
subject={subject}
category={category}
/>
);
});
}
// Helper function to add a new topic object
// to the topics state
function addTopic() {
const obj = { subject: 'Math', category: 'Fish' };
setTopics([...topics, obj ]);
}
return (
<div>
<div>{getTopics()}</div>
<button onClick={addTopic}>Add topic</button>
</div>
);
};
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<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>
Try to replace [...topic].map with [...topic.topics].map.
edit
Sorry, I didn't see that the topic itself is an arr of objs, so it should be: [...topic[0].topics].map.
I followed the answer in this thread to try to update my deeply nested object in React.
React: Setting State for Deeply Nested Objects w/ Hooks
What seems to work like a charm there, will somehow break for me when doing the following:
I have a table populated with items from an array defined like so:
const [items, setItems] = useState([
{
selected: false,
title: 'Item 1',
status: 'new'
},
{
selected: false,
title: 'Item 2',
status: 'used'
},
]);
When selecting an item from that list this function gets called to update selected variable for the object with the index i like so:
const select = (e) => {
const i = e.target.getAttribute('data-index');
setItems((prevState) => {
prevState[i].selected = !prevState[i].selected;
return [...prevState];
});
};
This will work exactly once. If I trigger select a second time or any time after that return [...prevState] somehow keeps returning the state unchanged. (selected stays true forever). I can't solve this.
items is attached to a component List like so:
<List
items={items}
/>
and inside List (shortened code):
{items.map((item, i) => {
return (
<tr className="list-table-tr">
{hasSelector ? (
<td className="list-table-td-selector">
{item.selected ? (
<div
data-index={i}
className="global-selector-selected"
onClick={select}
></div>
) : (
<div
data-index={i}
className="global-selector-unselected"
onClick={select}
></div>
)}
</td>
) : null}
You're breaking one of the primary rules of React state: You're modifying a state object directly, rather than making a copy.
To correctly do the update, you'd do this:
const select = (e) => {
const i = e.target.getAttribute('data-index');
setItems((prevState) => {
// Copy the array (your code was doing that)
const update = [...prevState];
const item = update[i];
// Copy the object (your code wasn't doing that) and update its
// `selected` property
update[i] = {...item, selected: !item.selected};
return update;
});
};
Note how both the array and the object are copied, rather than just the array.
I have the following code and need to add two extra things to it but I'm stuck and not sure how to do it.
I need to add:
If there are no products in the category a NotFound component will show a message.
By typing 'all' in the input we should be able to see the entire list of products again from all the categories.
Ideally I'm looking for the simplest solution as I'm currently learning React. Thanks!
Main Component
import React from 'react';
import Item from './components/Item';
class App extends React.Component {
state = {
items: [
{
title: "The Spice Girls",
price: 10,
category: "Pop",
quantity: 1,
},
{
title: "Beethoven",
price: 5,
category: "Classical",
quantity: 1,
},
{
title: "Bob Marley",
price: 15,
category: "Reggae",
quantity: 1,
}
],
category: " ",
filtered: [],
}
handleChange = e => {
this.setState({category: e.target.value},()=>console.log(this.state.category));
}
handleClick = (event) => {
event.preventDefault()
var newList = this.state.items;
var filteredItems = newList.filter(item => item.category === this.state.category)
this.setState({filtered: filteredItems})
}
render () {
let show;
if(this.state.category !== " "){
show = this.state.filtered.map((item, i) => <Item key = {i} cd={item}/>)
}else{
show = this.state.items.map( (item,i) => <Item key = {i} cd={item}/>)
}
return (
<div>
<h1 className = "title">CD</h1>
<h2>Search music below:</h2>
<form>
Music style: <input onChange = {this.handleChange}></input>
<button onClick = {this.handleClick}>Search</button>
</form>
{show}
</div>
)
}
}
export default App;
Item Component
import React from 'react';
class Item extends React.Component {
render () {
return (
<div className = "items">
<div className = "item">
<h3>{this.props.cd.title}</h3>
<div className = "price">Price: {this.props.cd.price}€</div>
<div className = "quantity">Quantity: {this.props.cd.quantity}</div>
<div className = "category">Category: {this.props.cd.category}</div>
</div>
</div>
)
}
}
export default Item;
First of all some suggested changes before I answer your question
There are a few things which confused me much when analysing your code, so will share them with you, as if in the future you work on teams, it would be handy if other people can understand your code.
You have an on change event for the text box which is different to the event for the search button next to it. Users would expect it to be the same and so that's really confusing.
You have 2 lists of items essentially, a raw and unfiltered and you switch between which 2 to present on the screen. Sometimes you need to have a raw set and that's fine, but perhaps make sure that the only ones which are presented as such is just either the state.items or the state.filtered. I would probably expect the state.filtered
Make your search case insensitive, e.g. pop should match Pop
Answer to your question'ss
If there are no products in the category a NotFound component will show a message.
For this I would first modify your show logic to work on the same filtered list just change your event functions to manipulate the filtered list and untouch the items one.
add another condition perhaps for when there are no cases
if (this.state.filtered) {
show = this.state.filtered.map((item, i) => <Item key={i} cd={item} />);
} else {
show = <h1>NoneFound</h1>;
}
By typing 'all' in the input we should be able to see the entire list of products again from all the categories.
handleClick = event => {
event.preventDefault();
var { category } = this.state;
var newList = this.state.items;
var filteredItems;
if ([" ", "All"].some(t => t === category)) {
filteredItems = newList;
} else {
filteredItems = newList.filter(
item => item.category.toLowerCase() === this.state.category.toLowerCase()
);
}
this.setState({ filtered: filteredItems });
};
My souloution to this was to modify your onClick event to correctly manipulated the filtered list.
You can see my full soloution to this on my codesandbox here
I am dynamically rendering a large list of cards using React hooks, and each component has its drop-down list. If I select one option, which updates the component state, every drop-down update to the same value. I have tried implementing select an option of React, and separately React-dropdown, and none of my approaches work. I recognize the problem is that when any dropdown is updated, they all refer to the same value in state and if I comment out the state update the dropdowns work.
I just don’t capture the state update. I am unsure what is the best way to capture these selection(s) while the user picks between the dropdowns and then showing the rendered selection.
A lot of the approaches I have seen don’t address having multiple dynamically created dropdowns on one page, so if there are any thoughts on how to resolve the issue, that would be helpful. Below is part of the code:
<select onChange(event=> selectedDropdownItem(selection)
value={event.target.value}
>
dropdownList.map(name =>
<option key={name.id} value={name.id} label={name.name} />
</select>
const [header, setHeader] = useState({ name: “select name”, id: null })
selectedDropdownItem = selection => {
setHeader({
name: selection.name,
id: selection.id
})
}
As i am not much clear about your question for the multiple selection in single component. I just created one which will do the dynamic dropdown based on that, and update the name based on selection using useState. Whenever you do the selection , it will come to parent component and do the update and return the value to the child component.
Hope i explained as you expected.
Here is the working code link.
If you have any queries, give a comment :)
import React, {useState} from 'react';
const DropDownCard = ({name, list, selectedValue, onChange}) => {
const onSelectChange = event => {
const { value } = event.target;
const { name } = list.find(val => val.id == value);
onChange({id: value, name});
};
return (
<div>
<h1>Card {name}</h1>
<select onChange={onSelectChange} value={selectedValue}>
{
list.map(name => <option
key={name.id}
value={name.id}
label={name.name}
/>)
}
</select>
</div>
)
}
const cardList = [
{name:'', selectedValue: null},
{name:'', selectedValue: null},
{name:'', selectedValue: null},
];
const Home = () => {
const [hea, setHea] = useState(cardList);
const list = [
{name: "select name", id: ''},
{name: "Name 1", id: 1},
{name: "Name 2", id: 2}
];
const onCardNameSelect = (info, index) => {
setHea(prevState => {
const {id, name} = info;
prevState[index].selectedValue = id;
prevState[index].name = name;
return [...prevState];
});
};
return(
<>
<div>Hello</div>
{hea.map((val,i) => <DropDownCard key={"dropDown"+Math.random()}
name={val.name}
selectedValue={val.selectedValue}
list = {list}
onChange = {event => onCardNameSelect(event, i)}
/>)}
</>
)
};
export default Home;