How to not lose React state in this case when I do filter?
When I do filter I lose my previous state and program work not correct, for example, if I choose category sport, then try category fashion, I can't see anything in fashion, the case this all was dropped, when I choose sport.
I am new in React I would like to hear the best practices.
FilterCategory(e) {
// Filter
const filter = this.state.items.filter(
(item) => {
return item.category.indexOf(e.target.name) !== -1
}
)
// Update state
this.setState({
items:filter
})
}
Why not use query string to store filters.
Suppose your url is /products and filter selected is say gender male. then you can append
/products?gender=male.
Now in react using libraries like react-router you can access this query params and get the current filter selected and then perform whatever options you want to like call api etc.
If you further select other filters then just append the new filters again to query params like field1=value1&field2=value2&field3=value3...
And again as location props of react will change you will get the new params in the component.
Advantages of this technique.
1) No headache of maintaining state.
Storing filters in state can become complex and clumsy if not done in proper way.
2) No problem if page gets refreshed.
Suppose your user have selected filters and, page gets refreshed, all filters will be lost if saved in state. But if query string is done it will remain intact.
Due to this reasons i think query string is better option then state.
Just store filtered values as another state property.
state = {
items: [],
filteredItems: []
}
When you do filtering always refer to items and override filteredItems
filterItems(e) {
const filtered = this.state.items.filter(
(item) => {
return item.category.indexOf(e.target.name) !== -1
}
)
this.setState({filteredItems: filtered});
}
The problem is you're setting items to the filtered array returned by filter.
You could use another proprety in your state to store the target's item, so you're keeping your items as they are, something like this:
this.state({
items: [...yourItems],
chosenItem: []
})
filterCategory(e) {
let filter = this.state.items.filter((item) => {
item.category.indexOf(e.target.name) !== -1
})
// Update state keeping items intact
this.setState({
chosenItem: filter
})
}
You can just store and update the filter query in state and only return the filtered items in the render function instead of updating state.
class App extends React.Component {
state = {
items: [
{ category: ['fashion'], name: 'Gigi Hadid', id: 1 },
{ category: ['sports'], name: 'Lebron James', id: 2 },
{ category: ['fashion', 'sports'], name: 'Michael Jordan', id: 3 },
{ category: ['sports', 'tech'], name: 'Andre Iguodala', id: 4 },
{ category: ['tech'], name: 'Mark Zuckerberg', id: 5 },
],
filter: '',
}
handleChange = (e) => {
this.setState({ filter: e.target.value });
}
render() {
const { items, filter } = this.state;
let shownItems = items;
if (filter) {
shownItems = items.filter(({ category }) => category.includes(filter));
}
return (
<div>
<select value={filter} onChange={this.handleChange}>
<option value="">All</option>
<option value="fashion">Fashion</option>
<option value="sports">Sports</option>
<option value="tech">Tech</option>
</select>
<div>
<ul>
{shownItems.map(({ name, id }) => <li key={id}>{ name }</li>)}
</ul>
</div>
</div>
);
}
}
ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Related
I am using React Select (https://react-select.com/home) which contains a bunch of options and the onChange handler will identify the selected items and push it into a State array.
The issue im running into is how to remove unselected items from the State array:
import Select from 'react-select'
const [selectedTeams, setSelectedTeams] = useState([]);
function manageSelectedTeams(event) {
console.log('function triggered')
setSelectedTeams([])
event.forEach((team) => {
console.log(team.value)
setSelectedTeams(() => [...selectedTeams, team.value])
})
}
data = [
{
label: "Example"
value: "example"
},
{
label: "Example"
value: "example"
},
{
label: "Example"
value: "example"
},
]
return (
<Select isMulti options={data} onChange={(e) => manageSelectedTeams(e)} closeMenuOnSelect={false} placeholder={"Teams will auto populate here once Location is defined above"} />
)
How would I tackle removing items from selectedTeams when a particular option is unselected/unticked
TIA
react-select handles the insert and delete for you so do not need to create a separate logic for it. When we use onChange the array of selected objects could be one possible parameter. You can refer to the same in the documentation. https://react-select.com/props
Thus you need to update your code as follows:
function manageSelectedTeams(data) {
let finalValueList = data.map((item) => item.value);
console.log(finalValueList);
setSelectedTeams(finalValueList);
}
return (
<Select
isMulti
options={data}
onChange={manageSelectedTeams} //UPDATED
closeMenuOnSelect={false}
placeholder={
"Teams will auto populate here once Location is defined above"
}
/>
);
I have a drop-down list which is coming from the query and when I click on the option the related data should display. I have this drop-down as shown in image
.
How to display the option only once.
I have this below code:
class StoreLocator extends PureComponent {
constructor(props) {
super(props)
this.state = {
options : [],
}
}
getStoreLocatorDropdown(){
return(
<div>
<select>
<option value="" hidden>Select product/service type</option>
{
this.state.options.map((obj) => {
return <option value={obj.id} changeOption={this.handleChange}>{obj.name}</option>
})
}
</select>
</div>
)
}
handleChange(){
console.log("clicked")
}
async componentDidMount(){
let storeLocatorQuery = StoreLocatorInstance.getStoreLocator()
await fetchQuery(storeLocatorQuery).then((data) => {
this.setState({
options : data.storeLocatorLocations.items
})
this.getStoreLocatorDropdown()
},
(error) => console.log(error)
)
}
render() {
return (
<div>
<h1>Store Locator</h1>
<div>
{this.getStoreLocatorDropdown()}
</div>
</div>
)
}
}
export default StoreLocator
How to display option only once when it's values are repeated. And how to make it clickable and display its related data
To stop duplicate values from being displayed on your options list you can add an additional array(duplicateCheck) which would make sure that the values are not repeating
in your options list:
let duplicateCheck=[];
this.state.options.map((obj) => {
if(duplicateCheck.includes(obj.name))
{
return (null);
}
else
{
duplicateCheck.push(obj.name);
return <option value={obj.id} changeOption={this.handleChange}>{obj.name}</option>}
}
})
Seems like what you are trying to do is to only show the unique/distinct options in the drop down list.
One of the manual way you can resolve this is to filter your options datasource first.
In your case, it is the "this.state.options"
Inside your "componentDidMount" function, you can filter it before setting the value into your state:
var data = [
{ id: 1, name: 'Partsandservices' },
{ id: 2, name: 'Partsandservices' },
{ id: 3, name: 'Petromin' }
];
data.map(item => item.name)
.filter((value, index, self) => self.indexOf(value) === index)
// this should return you ["Partsandservices", "Petromin"]
However, this is not a recommended approach, as the root cause of this duplication should be resolved from the deepest level, which is from the "StoreLocatorInstance.getStoreLocator()".
Since the options returned are repeated name on "Partsandservices", does it contains different meaning?
Maybe Partsandservices in Location A and Partsandservices in Location B?
Or was it a mistake for returning two same names to your application?
You should check on that.
Updating state works if the state i am trying to update is outside the users array. But since i am having multiple users i need the state to be inside the objects and update anyway
I keep getting the error TypeError: Cannot read property 'name' of undefined
I've thought of setting state inside of a loop but i was told thats a bad idea.
So [e.target.name]: e.target.value was the only code i could find for dropdowns.
I tried passing id for each of the users but didnt know how to change state using that or what condition to put.
import React, { Component } from 'react'
export default class App extends Component {
state = {
users: [
{
id: uuid(),
firstName: 'John',
lastName: 'Doe',
favColor: 'None'
},
{
id: uuid(),
firstName: 'Jane',
lastName: 'Doe',
favColor: 'None'
}
]
}
handleChange = (e) => {
this.setState({
[e.target.name]: e.target.value
})
}
render() {
return (
<div>
{this.state.users.map((user) => {
return <div key={user.id}>
<h1>{user.firstName}</h1>
<h1>{user.lastName}</h1>
<form>
<select
name="favColor"
value={user.favColor}
onChange={() => this.handleChange(user.id)}
>
<option value="None" disabled>None</option>
<option value="Blue">Blue</option>
<option value="Red">Red</option>
<option value="Green">Green</option>
</select>
</form>
<h1>Fav Color: {user.favColor}</h1>
<hr />
</div>
})}
</div>
)
}
}
I expect the dropdowns to change state separately for each of the users
Your handleChange method is not accepting the correct arguments.
If you wish to update one user item in array you will need to create a new updated copy of the array and save back into state
handleChange = (e,id) => {
const updatedUser = {...this.state.users.find(x=>x.id ===id), favColor: e.target.value}
this.setState({
users: [...this.state.users.filter(x==>x.id!==id),updatedUser]
})
}
...
onChange={(e) => this.handleChange(e,user.id)}
To simplify mutations of state I can recommend taking a look at Immer
And as #JosephD rightly pointed out this won't mantain order so you will need to do something like this.state.users.map(u => u.id === id ? { ...u, favColor: e.target.value } : u)
Here is a codesandbox based on your code:
https://codesandbox.io/s/loving-cohen-do56l?fontsize=14
<select
name="favColor"
value={this.state.favColor}
onChange={(e) => this.handleChange(e)}> // first Change
handleChange = (e) => {
this.setState({
favColor: e.target.value
})
} // Second Change
This will work for you
You are updating the state the wrong way;
Your state:
users: [
{ id: 1, ... },
{ id: 1, ... }
]
Your update / intention:
users: [
{ id: 1, ... },
{ id: 1, ... }
]
favColor: color // intention, because you don’t pass event
To update the right way, you need to:
pass the event and currentId, to handleChange, of the selected dropdown. Otherwise you cannot know the current user. Also, in your example you don’t pass the event, so you cannot retrieve the information of the dropdown. Causing the name of undefined error.
check when id of user matches with id of dropdown and change the value.
This example should work for you.
https://codesandbox.io/s/small-sea-mw08n
I have a form with 4 inputs consuming data from the same state. What I want is, after a selection, the choose option will be removed from the another 3 selects.
I've tried many things, is difficult to list here, but in a nutshell: I create another list of selected options, filter the players array with this another list, map this and etc..
The problem with this approach is: i don't want to remove the selected item from the input where it was selected. So, i try to create copy of the same original list to each input. But, i think this way too much 'work around'.
Here is my component:
import React, { Component } from 'react';
export default class MatchPlayerSelect extends Component {
state = {
selectedPlayers: [],
players: [
{
name:"edmiel",
matches:0,
wins:0
},
{
name:"willian",
matches:0,
wins:0
},
{
name:"gustavo",
matches:0,
wins:0
},
{
name:"gabriel",
matches:0,
wins:0
}
]
}
render() {
const {players, selectedPlayers} = this.state;
return (
<div className="match-player">
<select onChange={this.setPlayer.bind(this)}>
<option defaultValue value="">-</option>
{
// i need to filter list here
// but this code only list the items on state
players.map((player, index) => {
return(
<option key={index} value={player}>
{player.name}
</option>
)
})
}
</select>
</div>
);
}
setPlayer(sender) {
this.setState({ selectedPlayer: [...this.state.selectedPlayers,
sender.target.value] });
}
}
I need to all select component render each state player (already doing this in the code above). But, when selected, i need to remove this selected option from the another inputs without remove from the focus select input. If exist a better solution to do this in React, please let me know how.
I need to do this using react jsx tools or vanilla js. No jquery!
That's all.
So for first, you should remove this.selectPlayer.bind(this) from the render method, this is not good for React, for more please follow this link.
In the current situation, you need to add one extra array and totally it should look like:
1. players - there you should keep all players(this one serves for filtering only).
2. filteredPlayers - there you should keep the filtered players(this one servers for render the options list).
3. selectedPlayersIds- sure, that the array where you collect all selected player's ids.
Every time when selecting a player, you'll take players array and removing all selected players + new selected player and assign to filteredPlayers array.
Code Snippet(selectPlayer function)
const selectedPlayersIds= [...this.state.selectedPlayersIds, newPlayer.id];
this.setState({
filteredPlayers: this.state.players.filter(player => this.selectedPlayerIds.indexOf(player.id) !== -1),
selectedPlayersIds
})
Use #Array.filter and #Array.includes, moreover, you need to apply a few fixes to your code to make it work:
At setPlayer change the key to selectedPlayers instead selectedPlayer
On mapping the players array to option the value property needs to be player.name and not player
players
.filter(player => !selectedPlayers.includes(player.name))
.map((player, index) => (
<option key={index} value={player.name}>
{player.name}
</option>
));
Full example:
class MatchPlayerSelect extends Component {
state = {
selectedPlayers: [],
players: [
{
name: 'edmiel',
matches: 0,
wins: 0
},
{
name: 'willian',
matches: 0,
wins: 0
},
{
name: 'gustavo',
matches: 0,
wins: 0
},
{
name: 'gabriel',
matches: 0,
wins: 0
}
]
};
render() {
const { players, selectedPlayers } = this.state;
return (
<div className="match-player">
<select onChange={this.setPlayer}>
<option defaultValue value="">
-
</option>
{players
.filter(player => !selectedPlayers.includes(player.name))
.map((player, index) => (
<option key={index} value={player.name}>
{player.name}
</option>
))}
</select>
<div>{JSON.stringify(selectedPlayers)}</div>
</div>
);
}
setPlayer = sender => {
const { selectedPlayers } = this.state;
this.setState({
selectedPlayers: [...selectedPlayers, sender.target.value]
});
};
}
ReactDOM.render(<MatchPlayerSelect />, document.getElementById('root'));
In my React state, I want to reorder an array of 3 objects by always putting the selected one in the middle while keeping the others in ascending order.
Right now, I'm using an order property in each object to keep track of the order, but this might not be the best approach.
For example :
this.state = {
selected: 'item1',
items: [
{
id: 'item1',
order: 2
},
{
id: 'item2'
order: 1
},
{
id: 'item3'
order: 3
}
]
}
Resulting array : [item2, item1, item3]
Now, let's imagine that a user selects item2. I will update the selected state property accordingly, but how can I update the items property to end up with a result like this:
this.state = {
selected: 'item2',
items: [
{
id: 'item1',
order: 1
},
{
id: 'item2'
order: 2
},
{
id: 'item3'
order: 3
}
]
}
Resulting array : [item1, item2, item3]
How would you do it? I have seen some lodash utility functions that could help but I would like to accomplish this in vanilla JavaScript.
You could do something crude like this:
// Create a local shallow copy of the state
var items = this.state.items.slice()
// Find the index of the selected item within the current items array.
var selectedItemName = this.state.selected;
function isSelectedItem(element, index, array) {
return element.id === selectedItemName;
};
var selectedIdx = items.findIndex(isSelectedItem);
// Extract that item
var selectedItem = items[selectedIdx];
// Delete the item from the items array
items.splice(selectedIdx, 1);
// Sort the items that are left over
items.sort(function(a, b) {
return a.id < b.id ? -1 : 1;
});
// Insert the selected item back into the array
items.splice(1, 0, selectedItem);
// Set the state to the new array
this.setState({items: items});
This assumes the size of the items array is always 3!
I'm gonna be lazy and just outline the steps you need to take.
Pop the selected item out of the starting array
Push the first item of the starting array into a new array
Push the selected item into the new array
Push the last item of the starting array into the new array
Set your state to use the new array
You can do something like:
NOTE: This works assuming there would three items in the array. However, if there are more we just need to specify the index position in the insert function.
this.state = {
selected: 'item1',
items: [
{
id: 'item1',
order: 1
},
{
id: 'item2',
order: 2
},
{
id: 'item3',
order: 3
}
]
};
// To avoid mutation.
const insert = (list, index, newListItem) => [
...list.slice(0, index), // part of array before index arg
newListItem,
...list.slice(index) // part of array after index arg
];
// Get selected item object.
const selectedValue = value => this.state.items.reduce((res, val) => {
if (val.id === selectedValue) {
res = val;
}
return res;
}, {});
const filtered = this.state.items.filter(i => i.id !== state.selected);
const result = insert(filtered, 1, selectedValue(this.state.selected));
We can get rid of the extra reduce if instead of storing id against selected you store either the index of the item or the whole object.
Of course we need to use this.setState({ items: result }). This solution would also ensure we are not mutating the original state array at any point.
I put together a fully working example what can be extended on so you can experiment with different ways to achieve your intended use-case.
In this case I created a button component and rendered three of them to provide a means of changing the selected state.
Important things to remember, always use the setState() function for updating React Class state. Also, always work on state arrays and objects with a cloned variable as you'll want to update the whole object/array at once. Don't modify attributes of pointer variables pointing to state objects or arrays.
It is very possible to add bugs to your code by referencing state objects/arrays and then changing their properties (accidentally or not) by modifying the pointer referencing the object. You will lose all guarantees on how the state will update, and comparing prevState or nextState with this.state may not work as intended.
/**
* #desc Sub-component that renders a button
* #returns {HTML} Button
*/
class ChangeStateButton extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = ({
//any needed state here
});
}
handleClick(e) {
//calls parent method with the clicked button element and click state
this.props.click(e.nativeEvent.toElement.id);
}
render() {
return (
<button
id = {this.props.id}
name = {this.props.name}
className = {this.props.className}
onClick = {this.handleClick} >
Reorder to {this.props.id}!
</button>
);
}
}
/**
* #desc Creates button components to control items order in state
* #returns {HTML} Bound buttons
*/
class ReorderArrayExample extends React.Component {
constructor(props) {
super(props);
this.reorderItems = this.reorderItems.bind(this);
this.state = ({
selected: 'item1',
//added to give option of where selected will insert
selectedIndexChoice: 1,
items: [
{
id: 'item1',
order: 2
},
{
id: 'item2',
order: 1
},
{
id: 'item3',
order: 3
}
]
});
}
reorderItems(selected) {
const {items, selectedIndexChoice} = this.state,
selectedObjectIndex = items.findIndex(el => el.id === selected);
let orderedItems = items.filter(el => el.id !== selected);
//You could make a faster reorder algo. This shows a working method.
orderedItems.sort((a,b) => { return a.order - b.order })
.splice(selectedIndexChoice, 0, items[selectedObjectIndex]);
//always update state with setState function.
this.setState({ selected, items: orderedItems });
//logging results to show that this is working
console.log('selected: ', selected);
console.log('Ordered Items: ', JSON.stringify(orderedItems));
}
render() {
//buttons added to show functionality
return (
<div>
<ChangeStateButton
id='item1'
name='state-button-1'
className='state-button'
click={this.reorderItems} />
<ChangeStateButton
id='item2'
name='state-button-2'
className='state-button'
click={this.reorderItems} />
<ChangeStateButton
id='item3'
name='state-button-2'
className='state-button'
click={this.reorderItems} />
</div>
);
}
}
/**
* #desc React Class renders full page. Would have more components in a real app.
* #returns {HTML} full app
*/
class App extends React.Component {
render() {
return (
<div className='pg'>
<ReorderArrayExample />
</div>
);
}
}
/**
* Render App to DOM
*/
/**
* #desc ReactDOM renders app to HTML root node
* #returns {DOM} full page
*/
ReactDOM.render(
<App/>, document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root">
<!-- This div's content will be managed by React. -->
</div>