I've created a filter function in React. The user would type a search term into the input and the list would filter correctly. However, when the user erased the last letter the list would not update back to the previous results. To remedy this I had to setState twice, once to render the original array of items, and once to render filtered items:
componentDidMount() {
fetch("https://jsonplaceholder.typicode.com/users")
.then(response => response.json())
.then(users => {
this.setState({ filteredUsers: users });
this.setState({ users });
});
}
The filter function looks like this and is updated via onChange from the input:
filterList = event => {
let updateUsers = this.state.users;
updateUsers = updateUsers.filter(user => {
return (
user.name.toLowerCase().search(event.target.value.toLowerCase()) !== -1
);
});
this.setState({ filteredUsers: updateUsers });
};
I then render the component with filteredUsers:
render() {
const { filteredUsers } = this.state;
return (
<div className="App">
<input type="text" placeholder="Search" onChange={this.filterList} />
<div className="list">
<List users={filteredUsers} />
</div>
</div>
);
}
The question is whether I can streamline the process a bit without having to set State twice with the same data. Following DRY principles I find it unnecessary to do this, but fail to see how else I could make the filtering work.
Also, I plan on implementing several more filters to further limit search results, so the answer should have scalability in mind.
Check the components on codesandbox
You can render users if there aren't filteredUsers yet. So you have one setState on componentDidMount and you render the list like this <List users={filteredUsers || users} />, with the initial state being { users: [] }.
See working sandbox: https://codesandbox.io/s/xlp9lv53vw
Related
Suppose I have long list (let's assume there is no pagination yet) where each list item has input and ability to update own value (as a part of collection). Let's say code looks something like that:
const initItems = [
{ id: 0, label: "Hello world" },
...
{ id: 100, label: "Goodby" }
];
function List() {
const [items, setItems] = React.useState([...initItems]);
const handleChange = React.useCallback((e, id) => {
setItems(items.map(item => {
if (item.id === id) {
return {
...item,
label: e.target.value
}
}
return item;
}));
}, [items]);
return (
<ul>
{items.map(({ id, label }) => {
return (
<Item
id={id}
key={id}
label={label}
onChange={handleChange}
/>
)
})}
</ul>
)
}
// Where Item component is:
const Item = React.memo(({ onChange, label, id }) => {
console.log('Item render');
return (
<li>
<input type="text" value={label} onChange={e => onChange(e, id)} />
</li>
)
});
Looks pretty straightforward, right? While wrapping Item component with React.memo() what I wanted to achieve is to avoid re-render of each Item when some of the Item's gets updated. Well, I'm not sure it should works with this strategy, since each Item is a part of collection (items) and when I update any Item then items gets mapped and updated. What I did try - is to write custom areEqual method for Item component, where I do comparison of label value from props:
function areEqual(prev, next) {
return prev.label === next.label;
}
however with this approach the behaviour of updating items breaks down completely and updating next item reset previous updates and so on (I even could not observe any pattern to explain).
So the question: is it possible to avoid re-rendering of every item in such collection while having ability to update value of individual item?
Your problem here that you change callback on each render. So, you change callback, it changes onChange and this, in turn, runs rerender. To avoid it you can use updater function with setState.
const handleChange = React.useCallback((e, id) => {
// I made separate function so it would be easier to read
// You can just write `(items) =>` before your `items.map` and it will work
function updater(items) {
// we have freshest items here
return items.map((item) => {
if (item.id === id) {
return {
...item,
label: e.target.value,
};
}
return item;
});
}
// pass function
setItems(upadter);
// removed items from dependencies
}, []);
This way, your updater function will always get current value of state into parameters, and your props will update for actually updated item. Another solution would be to write custom updater that compares all values, but onChange. This is ok in short term, but this can become complex and cumbersome to maintain.
Here is live example: https://codesandbox.io/s/unruffled-johnson-ubz1l
I have a list of input to generate dynamically from an array of data I retrieve from an API.
I use .map() on the array to generate each of my input, and set value={this.state.items[i]} and the onChange property (with a modified handleChange to handle change on an array properly).
Now, I set in my constructor this.state = { items: [{}] }, but since I don't know how many items are going to be generate, value={this.state.items[i].value} crash since this.state.items[n] doesn't exist.
The solution is then to set each this.state.items[i] = {} (using Array.push for example) first, and then generate all the inputs.
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
inputs: apiData.map((v, i) => {
return <input key={i} value={this.state.items[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
})
https://jsfiddle.net/qzb17dut/38/
The issue with this approach is that this.state.items doesn't exist yet on value={this.state.items[i].value} and we get the error Cannot read property 'value' of undefined.
Thankfully, setState() comes with a handy second argument that allows to do something only once the state is set. So I tried this:
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
}, () => this.setState({
inputs: apiData.map((v, i) => {
return <input key={i} value={this.state.items[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
}))
https://jsfiddle.net/qzb17dut/39/
(Update: Please have a look at this example that better illustrate the use case: https://jsfiddle.net/jw81uo4y/1/)
Looks like everything should work now right? Well, for some reason, I am having this very weird bug were value= doesn't update anymore like when you forget to set onChange= on an input, but here onChange= is still called, value= is just not updated making the field remaining not editable.
You can see on the jsfiddle the problem for each method. The first one doesn't have the state set yet, which would allow the input to be edited, but crash because the state value was not yet set. And the second method fix the first issue but introduce this new weird bug.
Any idea about what I am doing wrong? Am I hitting the limit of react here? And do you have a better architecture for this use case? Thanks!
What about this approach instead, where you set the state of the API values only and then, generate the input based on the state from the render via Array.prototype.map like so
constructor (props) {
this.state = {items: []}
}
async componentDidMount(){
const apiData = await fetchApiData()
this.setState({items: apiData})
}
handleChange = (value, index) => {
const items = this.state.items;
items[index].value = value;
this.setState({ items });
};
updateState = () => {
const items = this.state.items;
items.push({value: ''}); // default entry on our item
this.setState({ items });
};
// here ur state items is exactly same structure as ur apiData
onSubmit =()=> {
console.log('this is apiData now', this.state.items)
}
render () {
<button onClick={this.updateState}>update state with inputs</button>
<button onClick={this.onSubmit}>Submit</button>
{this.state.items.map((item, index) => (
<input
key={index}
value={item.value}
onChange={e => this.handleChange(e.target.value, index)}
/>
))}
}
here is the codesandbox code for it
https://codesandbox.io/s/icy-forest-t942o?fontsize=14
with this, it will generate the input based on the items on the state, which in turns have the click handler which updates the state.
Well if I understand correctly, apiData is assigned to state.items and then also used to generate the inputs array. That means that for your purpose apiData and state.items are equivalent. Why don't you use the third map argument like:
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
inputs: apiData.map((v, i, arr) => {
return <input key={i} value={arr[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
});
or the apiData array directly?
I'm new to React/using API json data in a project so I'm having a little trouble. I've created a function where a user can type in a search query and a list of devices associated with their query will show up. These device names are fetched from an API. I'm trying to make it so that when the plus sign next to a device is clicked, it adds this device to a new array that is then displayed to the screen.
I'm not 100% familiar with the concept of state in React and I think that's where my issue is (in the addDevice function). It's partially working, where I click the device and it displays at the bottom, but when I click another device, instead of adding to the list, it just replaces the first device.
class App extends React.Component {
state = {
search: "",
devices: [],
bag: []
};
addDevice = (e, data) => {
console.log(data);
const newData = [this.state.devices.title];
this.setState({
bag: newData.concat(data)
});
};
onChange = e => {
const { value } = e.target;
this.setState({
search: value
});
this.search(value);
};
search = search => {
const url = `https://www.ifixit.com/api/2.0/suggest/${search}?doctypes=device`;
fetch(url)
.then(results => results.json())
.then(data => {
this.setState({ devices: data.results });
});
};
componentDidMount() {
this.search("");
}
render() {
return (
<div>
<form>
<input
type="text"
placeholder="Search for devices..."
onChange={this.onChange}
/>
{this.state.devices.map(device => (
<ul key={device.title}>
<p>
{device.title}{" "}
<i
className="fas fa-plus"
style={{ cursor: "pointer", color: "green" }}
onClick={e => this.addDevice(e, device.title)}
/>
</p>
</ul>
))}
</form>
<p>{this.state.bag}</p>
</div>
);
}
}
I want it to display all the devices I click one after another, but right now each device clicked just replaces the previous one clicked
I think you're close. It appears that you are getting the devices array and the bag array mixed up.
I'd suggest using Array.from to create a copy of your state array. Then push the new item into the array. Concat is used to merged two arrays.
addDevice = (e, data) => {
// create new copy of array from state
const newArray = Array.from(this.state.bag);
// push new item into array
newArray.push(data);
// update the state with the new array
this.setState({
bag: newArray
});
}
Then if you want to show the device titles as a comma separated string, you could just do:
<p>{this.state.bag.join(', ')}</p>
Hope this helps.
The issue is with your addDevice method and specifically with how you create newData. You set newData to [this.state.devices.title], which evaluates to [undefined] since this.state.devices is an array and therefore has no attribute called title. Therefore, the updated value of state.bag will be [undefined, data], and only render as data which is the title of the most recently clicked device.
I think what you mean to do here is append the title of the clicked device to the array state.bag. You can do this with an addDevice method like this:
addDevice = (e, data) => {
console.log(data);
const newBag = this.state.bag.concat(data);
this.setState({
bag: newBag
});
};
Though a better practice way of updating state.bag would make use of the functional form of setState, and the spread operator (...) is more common for this sort of stuff than using concat. Also renaming data to something more explanatory (like deviceTitle) would be helpful here. Example:
addDevice = (e, deviceTitle) => {
this.setState(prevState => ({
bag: [...prevState.bag, deviceTitle],
});
}
Edit:
If you want to add functionality to remove devices from state.bag, you can create a method called removeDevice and add a button next to each bag item when rendering.
For example:
removeDevice = (e, deviceTitle) => {
this.setState(prevState => ({
bag: prevState.bag.filter(d => d !== deviceTitle),
});
}
Then in your render method you would have something like this:
<ul>
{this.state.bag.map(deviceTitle => (
<li>
<span>{ deviceTitle }</span>
<button onClick={ e => this.removeDevice(e, deviceTitle) }>remove</button>
</li>
))}
</ul>
I'm trying to select a song from the playlist, but selection doesn't work as it should, because, when I click for the first time it returns "null" when I click on the second time it selects an item, but for some reason, when I keep clicking on different items I always get a previous item value, and I don't know why.
//Here is my action from redux:
import {FETCH_PLAYLIST_SUCCESS, FETCH_PLAYLIST_FAILURE, FETCH_PLAYLIST_REQUEST, SELECT_TRACK} from './types'
import {PREV, PLAY, PAUSE, NEXT, TOGGLE_TRACK} from './types';
import axios from "axios/index";
export const getPlayList = id => {
return function (dispatch) {
dispatch({type: FETCH_PLAYLIST_REQUEST});
axios.get('https://cors-anywhere.herokuapp.com/https://api.deezer.com/search/track?q=red+hot+chili+peppers').then(response => {
dispatch({
type: FETCH_PLAYLIST_SUCCESS,
payload: {
playlist: response.data.data,
currentTrack: response.data.data[0]
}
});
}).catch(err => {
dispatch({
type: FETCH_PLAYLIST_FAILURE,
payload: err,
});
})
}
};
//func which takes index of list item
export const selectTrack = (i) =>{
return function (dispatch) {
dispatch({
type: SELECT_TRACK,
payload: i
});
}
}
//reducer
case SELECT_TRACK:
return {
...state,
selectedTrack: state.playList[action.payload],
};
//click handler in container component
handleClick = (index) => {
this.props.selectTrack(index);
console.log(this.props.playlist.selectedTrack);
console.log(index);
};
//how I'm using it in playlist component
<Playlist handleClick={this.handleClick} tracks={this.props.playlist.playList}/>
//Playlist component
function PlayList(props) {
const {i,handleClick,currentSongIndex,tracks,isSelected,} = props;
return (
<div className="playlist-container">
<div className="playlist">
<ul>
{
tracks.map((track, index) => (
<Track
isSelected={isSelected}
index={index}
key={track.id}
id={track.id}
title_short={track.title}
duration={track.duration}
clickHandler={ () => handleClick(index)}
/>
))
}
</ul>
</div>
</div>
);
}
See how it works on screenshots
Upon item click, you are dispatching the action in the click handler of the container. The props in the handler at the time of console.log is still the old props. The new props will arrive only once redux updates state and component updates its props via the react lifecycle methods.
I am guessing you have not set any intial state in the reducer which might be the reason why the first value is coming as null.
Try adding the same statement : console.log(this.props.playlist.selectedTrack) in the render method. You should see the updated prop
#Easwar is correct (above) that your debugging logic is problematic.
With respect to the core problem, I don't see where some of your important props are passed in. You have:
const {i,handleClick,currentSongIndex,tracks,isSelected,} = props;
In your PlayList component, but you're invoking it like this:
<Playlist handleClick={this.handleClick} tracks={this.props.playlist.playList}/>
Where are currentSongIndex and isSelected passed in? Your trouble is likely there. It looks like they'll evaluate to null in <Playlist>'s render method.
I am currently trying to build a sortby function which needs to sort an array with venues in the redux store based on alfabetical order. A live demo can be found here: http://tabbs-web-app.herokuapp.com/discover/home
Paste bins
Home.jsx
VenueList.jsx
VenueListFilter.jsx
VenueListItem
Venue Selector / filter function
Filters Action Generactor (redux)
Filters Reducer (redux)
Description
Unfortunately I am not able to upload the project to codesandbox since it's exceeding the 120 limit modules for sandbox...
The following component is retrieving the array of venues from the API and saves them to the redux store. This component is connected with the redux store and has the following redux functions to filter the array:
const mapStateToProps = (state) => {
return {
venues: getFilteredVenues(state.venues, state.filters)
};
};
const mapDispatchToProps = (dispatch) => ({
startSetVenues: () => dispatch(startSetVenues())
});
Home.jsx
render() {
return (
<div>
<SectionTitle
title="Discover Nightlife with Tabbs."
subTitle="Instant jaccess to your favorite nightclubs, lounge, bars and parties
nationwide."
/>
<VenueListFilter data={filterOptions} />
<VenueList data={this.props.venues} />
</div>
);
}
The following component is a child component which needs the data from the connected component:
VenueList.jsx
render({ data } = this.props) {
return (
<div>
<Grid container spacing={8}>
{data.map((venue, key) => {
return <VenueListItem key={key} venue={venue} />;
})}
</Grid>
</div>
);
}
The last component is the individual venue row / object / component:
VenueListItem.jsx
<Grid item {...rest} className={classes.grid}>
<ImageCard
image="https://static.bab.la/pic/living/UK/going-out-dancing.jpg"
cardTitle={venue.venue_name}
cardSubtitle={venue.venue_description}
content="test, abc, def"
/>
</Grid>
what is the issue I am experiencing right now?
The redux store is correctly being ordered based on alfabetical order. The mapStateToProps function is being called in the Home.jsx with the correct ordered array. But.. the components render function is not being called again, so the VenueList does not receive the new props / updated values.
Additional information
Sort / selector function:
const getFilteredVenues = (venues, { sortBy }) => {
return venues.sort((a, b) => {
if (sortBy === "alfabetical") {
return a.venue_name.toLowerCase() > b.venue_name.toLowerCase();
} else {
return 0;
}
});
};
Redux version: ^3.7.2
React version: 16.2.0
sort does inplace array modification. Just make a copy.
const getFilteredVenues = (venues, { sortBy }) => {
return venues.slice(0).sort((a, b) => {
if (sortBy === "alfabetical") {
return a.venue_name.toLowerCase() > b.venue_name.toLowerCase();
} else {
return 0;
}
});
};