I have this function that gets data from an API to be shown for selection in react-select. It gets so slow/drags. I tried looking at the solutions given here on stackoverflow but haven't been able to modify them to suit my use case.
const getItems = async () => {
await Axios.get(`http://127.0.0.1:5000/items/available/${myItem}`)
.then((response) => {
const items = response.data.map((item) => ({ label: item.name, value: item.id }));
setItemOptions(items);
}
}
This is how I tried to make the modifications
<Select filter={createFilter({ ignoreAccents: false })} filterOption={createFilter({ ignoreAccents: false })} isMulti options={itemOptions} />
DISCLAIMER: I'm learning react doing a project, so I'm new to it.
Related
Short Explanation
I just want to get the data from the textboxes and send them to the server. In jquery I would just list out all my textboxes and get the value and form the json and send it to the server
Please see example code
https://codesandbox.io/s/xenodochial-euclid-3bl7tc
const EMPTY_VALUE = "";
const App = () => {
const [valueFirstName, setValueFirstName] = React.useState(EMPTY_VALUE);
const [valueMiddleName, setValueMiddleName] = React.useState(EMPTY_VALUE);
const [valueLastName, setValueLastName] = React.useState(EMPTY_VALUE);
return (
<div>
First Name:
<Work365TextBox value={valueFirstName} setValue={setValueFirstName} />
<div>{valueFirstName}</div>
Middle Name:
<Work365TextBox value={valueMiddleName} setValue={setValueMiddleName} />
<div>{valueMiddleName}</div>
Last Name:
<Work365TextBox value={valueLastName} setValue={setValueLastName} />
<div>{valueLastName}</div>
</div>
);
};
Problem
The current code has a label for first name, middle name, and last name and components to contain the textboxes and then the state of the component is stored in the parent.Then the parent takes the state and displays it below the textbox. So the solution works great. But the code is messy
Question
If I have a form that asks for 20 values what is a cleaner way to handle this ? I have tried to do this with by defining a object as json and calling a method when the value in each textbox changes but then I need to have a method for each textbox I have on the screen and it doesn't seem very clean. How would you solve this problem in a clean way ? Preferably I want to be able to have 50 textbox components but just call one method to update state.
the object solution you mentioned is a great way.
one thing to add is that you can pass a name prop to your input and then in the onChange method you can access it via event.target.name
that way you can dynamically update your object
something like this
const onChangeHandler = (event) => {
const name = event.target.name;
const value = event.target.value;
setState((prev) => ({ ...prev, [name]: value }));
};
another solution is to define it with useReducer but that will need a extra code.
I would just use an array like that:
// one item for each textbox
const textboxes = [
{
// human-readable label
label: "First name",
// unique key for data access
key: "firstName"
},
{
label: "Middle name",
key: "middleName"
},
{
label: "Last name",
key: "lastName"
}
];
A state like this:
const [values, setValues] = React.useState(
// converts array to record-like object
Object.fromEntries(textboxes.map((box) => [box.key, ""]))
);
const handleChange = (key, value) => {
setValues((values) => ({ ...values, [key]: value }));
};
And render it like this:
<div>
{textboxes.map((box) => (
<>
{box.label}:
<Work365TextBox
value={values[box.key]}
setValue={(value) => handleChange(box.key, value)}
/>
<div>{values[box.key]}</div>
</>
))}
</div>
Full example
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'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