How can I keep selected options by react-select async? What I mean I have the following component which is inside of a form with multiple steps when I step back to the part where I use AsyncSelect I see placeholder and load options fire again(get all value from API) selected values. how to keep the selected value. this is my AsyncDropDown.js :
<AsyncSelect
styles={customStyles}
cacheOptions
loadOptions={loadOptions}
defaultOptions
onChange={handleChange}
isRtl={true}
isSearchable={false}
classNamePrefix='myDropDown'
placeholder={'(پیش فرض گروه فعال)'}
/>
and this loadOtions function :
const loadOptions = (selectedOption, callback) => {
let token = localStorage.getItem('Token')
let udid = localStorage.getItem('UUID')
let xml = `body of request`;
axios.post('myurl.com', xml, { headers: { 'Content-Type': 'text/xml;charset=UTF-8' } }).then(function (response) {
//console.log(response)
var options = {
attributeNamePrefix: "#_",
attrNodeName: "attr", //default is 'false'
textNodeName: "#text",
ignoreAttributes: true,
ignoreNameSpace: false,
allowBooleanAttributes: false,
parseNodeValue: true,
parseAttributeValue: false,
trimValues: true,
cdataTagName: "__cdata", //default is 'false'
cdataPositionChar: "\\c",
localeRange: "", //To support non english character in tag/attribute values.
parseTrueNumberOnly: false,
attrValueProcessor: a => he.decode(a, { isAttributeValue: true }),//default is a=>a
tagValueProcessor: a => he.decode(a) //default is a=>a
};
// Intermediate obj
var tObj = parser.getTraversalObj(response.data, options);
var jsonObj = parser.convertToJson(tObj, options);
if (jsonObj["soap:Envelope"]["soap:Body"].GetAllCategoriesResponse.GetAllCategoriesResult["diffgr:diffgram"].DocumentElement != null) {
var jsonDropDownDetails = jsonObj["soap:Envelope"]["soap:Body"].GetAllCategoriesResponse.GetAllCategoriesResult["diffgr:diffgram"].DocumentElement.CATEGORY
jsonDropDownDetails.map(item => {
const data = { value: item.CATEGORYNAME, label: item.CATEGORYNAME, index: item.CATEGORYID }
setDropDownOptions(dropDownOptions.push(data))
})
callback(dropDownOptions)
}
setIsLoading(false)
}).catch(function (error) {
console.log("erorr in DropDown : " + error)
})
};
and this is handelChange function :
const handleChange = selectedOption => {
setSelectedOption(selectedOption)
props.parentCallBack(selectedOption)
};
this is the Async Dropdown component use this component to the main view when in the main view go to the next view and get back to the main view missing selected Value and show placeHolder and call loadOptions function again and call API. how to solve this issue?
In order to retain values of your form fields when you click/next or previous:
Create a main form component say MasterForm and and render the form-steps say Step1 and Step2(which holds your input fields etc).
Maintain the value of your select in MasterForm and the values and onChangeHandlers the form steps.
I have created a working demo of react-select multi step form
MasterForm:
...
render() {
return (
<>
<Step1
value={this.state.step1SelectValue}
handleChange={e => {
this.setState({ step1SelectValue: e });
}}
currentStep={this.state.currentStep}
/>
<Step2
value={this.state.step2SelectValue}
handleChange={e => {
this.setState({ step2SelectValue: e });
}}
currentStep={this.state.currentStep}
/>
{this.previousButton()}
{this.nextButton()}
</>
);
}
...
Step1.js
...
<AsyncSelect
cacheOptions
loadOptions={this.loadOptions}
defaultOptions
onChange={this.props.handleChange}
isRtl={true}
isSearchable={false}
classNamePrefix="myDropDown"
placeholder={"(پیش فرض گروه فعال)"}
value={this.props.value}
...
Note:
You seem to not use search feature in your select. So I guess you can simply use normal Select and handle data fetching on your own in componentDidMount. See Step2 in my code demo
If you don't want to execute your loadOptions function for all the re-renders consider to use useMemo . Note that useMemo is available in functional components
Related
I have a keeper app where I am adding notes and storing them in database. When I make a post request to the server, I am trying to fetch the _id from database, which will eventually help me to later delete the note ( if needed).
Here is my jsx file
function CreateMessage(props) {
const [currentGuest, setCurrentGuest] = useState({
guestName: '',
guestMessage: '',
id:''
});
function handleMessages(event) {
const {name, value} = event.target;
setCurrentGuest(prevGuest => {
return {
...prevGuest,
[name] : value
};
});
}
function submitMessage(event) {
//props.onAdd(currentGuest);
const params = {
guestName: currentGuest.guestName,
guestMessage: currentGuest.guestMessage,
}
axios
.post("http://localhost:8000/notes", params)
.then(res => {
console.log("The response is"+res.data._id);
console.log(res.status);
setCurrentGuest(prevGuest => {
console.log(res.data._id)
return {
...prevGuest,
id: res.data._id
};
});
console.log(currentGuest);
})
event.preventDefault();
}
return (
<div>
<form>
<input
name="guestName"
placeholder="Guest Name"
value={currentGuest.guestName}
onChange={handleMessages}
/>
<textarea
name="guestMessage"
placeholder="Write a Message"
rows="3"
value={currentGuest.guestMessage}
onChange={handleMessages}
/>
<button onClick={submitMessage}>Add</button>
</form>
</div>
);
}
The id is properly being fetched and displayed in ```console.log("The response is"+res.data._id"). But on first submit, the is always empty and stale id gets attached to the currentGuest object on next submit
function submitMessage(event) {
//props.onAdd(currentGuest);
const params = {
guestName: currentGuest.guestName,
guestMessage: currentGuest.guestMessage,
}
axios
.post("http://localhost:8000/notes", params)
.then(res => {
console.log("The response is"+res.data._id);
console.log(res.status);
setCurrentGuest(prevGuest => {
console.log(res.data._id)
return {
...prevGuest,
id: res.data._id
};
});
console.log(currentGuest);
})
event.preventDefault();
}
In the above snippet, after getting the response you're correctly changing the state but the problem is with where you're checking the changed state(console.log(currentGuest)). You're basically logging before the state is changed.
You can use useEffect hook and log the state inside it. This runs every time the currentGuest Changes.
useEffect(() => {
console.log(currentGuest)
}, [currentGuest])
Update
You can use the modified currentGuest inside the useEffect hook:
useEffect(() => {
console.log(currentGuest)
if(currentGuest.id) {
props.onAdd(currentGuest);
// You can also reset the state here as follows
setCurrentGuest({
guestName: '',
guestMessage: '',
id:''
});
}
}, [currentGuest]) // You might need to add the necessary dependencies to this array.
I have a group of 3 checkboxes and the main checkbox for checking those 3 checkboxes.
When I select all 3 checkboxes I want for main checkbox to become checked.
When I check those 3 checkboxes nothing happens but when I then uncheck one of those trees the main checkbox becomes checked.
Can someone explain to me what actually is happening behind the scenes and help me somehow to solve this mystery of React state? Thanks!
Here is a code snnipet:
state = {
data: [
{ checked: false, id: 1 },
{ checked: false, id: 2 },
{ checked: false, id: 3 }
],
main: false,
}
onCheckboxChange = id => {
const data = [...this.state.data];
data.forEach(item => {
if (item.id === id) {
item.checked = !item.checked;
}
})
const everyCheckBoxIsTrue = checkbox.every(item => item === true);
this.setState({ data: data, main: everyCheckBoxIsTrue });
}
onMainCheckBoxChange = () => {
let data = [...this.state.data];
data.forEach(item => {
!this.state.main ? item.checked = true : item.checked = false
})
this.setState({
this.state.main: !this.state.main,
this.state.data: data,
});
}
render () {
const checkbox = this.state.data.map(item => (
<input
type="checkbox"
checked={item.checked}
onChange={() => this.onCheckboxChange(item.id)}
/>
))
}
return (
<input type="checkbox" name="main" checked={this.state.main} onChange={this.onMainCheckBoxChange} />
{checkbox}
)
I can't make a working code snippet based on the code you provided, one of the issues was:
const everyCheckBoxIsTrue = checkbox.every(item => item === true);
where checkbox is not defined.
However, I think you confused about using the old state vs the new state, it'd be simpler to differentiate if you name it clearly, e.g.:
eventHandler() {
const { data } = this.state; // old state
const newData = data.map(each => ...); // new object, soon-to-be new state
this.setState({ data }); // update state
}
Here's a working example for your reference:
class App extends React.Component {
state = {
data: [
{ checked: false, id: 1 },
{ checked: false, id: 2 },
{ checked: false, id: 3 }
],
main: false,
}
onCheckboxChange(id) {
const { data } = this.state;
const newData = data.map(each => {
if (each.id === id) {
// Toggle the previous checked value
return Object.assign({}, each, { checked: !each.checked });
}
return each;
});
this.setState({
data: newData,
// Check if every checked box is checked
main: newData.every(item => item.checked === true),
});
}
onMainCheckBoxChange() {
const { main, data } = this.state;
// Toggle the previous main value
const newValue = !main;
this.setState({
data: data.map(each => Object.assign({}, each, { checked: newValue })),
main: newValue,
});
}
render () {
const { data, main } = this.state;
return (
<div>
<label>Main</label>
<input
type="checkbox"
name="main"
// TODO this should be automatically checked instead of assigning to the state
checked={main}
onChange={() => this.onMainCheckBoxChange()}
/>
{
data.map(item => (
<div>
<label>{item.id}</label>
<input
type="checkbox"
checked={item.checked}
onChange={() => this.onCheckboxChange(item.id)}
/>
</div>
))
}
</div>
);
}
}
ReactDOM.render(
<App />
, document.querySelector('#app'));
<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>
<div id="app"></div>
Side note: You might want to consider not to use the main state
You shouldn't be storing state.main to determine whether every checkbox is checked.
You are already storing state that determines if all checkboxes are checked, because all checkboxes must be checked if every object in state.data has checked: true.
You can simply render the main checkbox like this:
<input
type="checkbox"
name="main"
checked={this.state.data.every(v => v.checked)}
onChange={this.onMainCheckBoxChange}
/>;
The line this.state.data.every(v => v.checked) will return true if all of the checkboxes are checked.
And when the main checkbox is toggled, the function can look like this:
onMainCheckBoxChange = () => {
this.setState(prev => {
// If all are checked, then we want to uncheck all checkboxes
if (this.state.data.every(v => v.checked)) {
return {
data: prev.data.map(v => ({ ...v, checked: false })),
};
}
// Else some checkboxes must be unchecked, so we check them all
return {
data: prev.data.map(v => ({ ...v, checked: true })),
};
});
};
It is good practice to only store state that you NEED to store. Any state that can be calculated from other state (for example, "are all checkboxes checked?") should be calculated inside the render function. See here where it says:
What Shouldn’t Go in State? ... Computed data: Don't worry about precomputing values based on state — it's easier to ensure that your UI is consistent if you do all computation within render(). For example, if you have an array of list items in state and you want to render the count as a string, simply render this.state.listItems.length + ' list items' in your render() method rather than storing it on state.
Hey guys im trying to create a autosuggestion in cooperation with redux-form. Im using the Creatable approach. I loading my options via an external API. The problem is, i need a extra field in every Option Object. {value: "test#gmx.de", label: "test#gmx.de", dn:"CN...." }. Is there a possibility to do so?
I typically add my own properties inside the callback for the API request, just before setting the options in the state. For example...
axios.get('/some/api/request')
.then(response => {
const options = response.data.map(item => {
// Add whatever custom properties you want here
return ({value: "test#gmx.de", label: "test#gmx.de", dn:"CN...." })
})
// set your options in the state to the new options constant from above
dispatch(change('formName', 'options', options))
Hope this helps!
//Handle change with either selectedOption
handleChange(selectedOption){
this.setState({ selectedOption })
if(this.props.onOptionSelect){
this.props.onOptionSelect(selectedOption.data)
}
}
loadOptions(input, callback) {
this.props.loadOptions(input).then(options => {
callback(null, {options: options})
})
}
render() {
const {selectedOption} = this.state
const selectClass = this.props.meta.touched && this.props.meta.error ? "has-error form-group" : "form-group"
return (
<div className={selectClass}>
<AsyncCreatable
value={selectedOption}
onChange={this.handleChange}
loadOptions={this.loadOptions}
isLoading={false}
placeholder={this.props.label}
promptTextCreator={(label) => this.props.promtLabel(label)}
onBlur={() => this.props.input.onBlur(selectedOption.value || "")}
/>
</div>
)
}
//Function to convert incomming users in usable options (React Select)
export const convertADUsersToOptions = users => {
return users.map(user => {
return {
value: normalizeDN(user.dn),
label: user.mail
}
})
}
I'm still learning React, and am following a code-along. I have a form that uses semantic ui react components, after struggling with this problem for a day or so, I just copy and pasted from their GitHub, and am still having the same problem:
onSearchChange = (e, data) => {
clearTimeout(this.timer);
this.setState({
query: data
});
this.timer = setTimeout(this.fetchOptions, 1000);
};
onChange = (e, data) => {
this.setState({ query: data.value });
this.props.onBookSelect(this.state.books[data.value]);
};
render() {
return (
<Form>
<Dropdown
search
fluid
placeholder="Search for a book by title"
value={this.state.query}
onSearchChange={this.onSearchChange}
options={this.state.options}
loading={this.state.loading}
onChange={this.onChange}
/>
</Form>
);
}
}
The console is throwing me a: Warning: Failed prop type: Invalid prop value supplied to Dropdown. It turned out that my value is being passed as an Object that contains my Dropdown component.
As I was writing this question, I realized that I'm in v16 and they were in 15. So, the answer might be as simple as updating the syntax for v16, but I'm not sure how to do that.
Edit:
I installed React 15.x, and that did fix the problem I was having, so I guess my real question now is why doesn't onSearchChange fire fetchOptions when I change data to data.value. fetchOptions is just an axios call that looks like:
fetchOptions = () => {
if (!this.state.query) return;
this.setState({ loading: true });
axios
.get(`/api/books/search?q=${this.state.query}`)
.then(res => res.data.books)
.then(books => {
const options = [];
const booksHash = {};
books.forEach(book => {
booksHash[book.goodreadsId] = book;
options.push({
key: book.goodreadsId,
value: book.goodreadsId,
text: book.title
});
});
this.setState({ loading: false, options, books: booksHash });
});
};
Doing the exact same tutorial (Rem Zolotykh's Build Real Web App with React series) and ran into the same problem with the newer version of React.
Found the issue and how to fix it:
Change the onSearchChange function to use data.searchQuery instead of data or data.value:
onSearchChange = (e, data) => {
clearTimeout(this.timer);
this.setState({
query: data.searchQuery // <-- Right here
});
this.timer = setTimeout(this.fetchOptions, 1000);
};
That makes it work. (Somewhat unexpectedly, leave it as data.value in the onChange function)
Should be an easy fix, all you need to do is update onSearchChange:
onSearchChange = (e, data) => {
clearTimeout(this.timer);
this.setState({
query: data.value
});
this.timer = setTimeout(this.fetchOptions, 1000);
};
You were saving the argument data to this.state.query. You actually need to store data.value to get the string value. Take a look at how onSearchChange works.
onSearchChange(event: SyntheticEvent, data: object)
event: React's original SyntheticEvent.
data: All props, includes current value of searchQuery.
This has been driving me and my team crazy. Here is the relevant code.
In the component's CDM we have:
componentDidMount() {
this.getContextID();
this.getConsumerID();
this.getEnvType();
//setTimeout(() => this.setState({ populatedMultiSchema: this.multiSchema }), 200);
//setTimeout(() => this.setState({ populatedMultiUISchema: this.multiUISchema }), 200);
this.setState({ populatedMultiSchema: this.multiSchema });
this.setState({ populatedMultiUISchema: this.multiUISchema });
}
so any one of the 3 methods listed will fetch the data for the dropdown. Here is an example of one (they are all basically the same).
getContextID() {
contextIDOptions = [];
console.log("CONVERT_TARGET::", this.props.fetchTarget)
return (
fetch(this.props.fetchTarget + "Configuration/ContextIDs", {
method: 'GET',
//mode: 'cors',
credentials: 'include',
}).then(response => {
if (response.status >= 400) {
this.setState({
value: 'no response - status > 400'
});
throw new Error('no response - throw');
}
return response.json()
}).then(function (json) {
for (var contextID = 0; contextID < json.List.length; contextID++) {
contextIDOptions.push(json.List[contextID]);
}
this.setState({ contextIDArray: contextIDOptions });
console.log("got contextIDs");
}.bind(this)).catch(() => {
this.setState({
value: 'no response - cb catch'
})
})
)
}
So we set the state there to 'contextIDArray'.
Then the JSON Schema form through it's multiUISchema Object has references to these widgets that help set the values for the form.
ContextIDWidget = (props) => {
return (
<div>
<input type="text" placeholder="Select one..." className="form-control" list="contextIDSelect" onChange={(event) => props.onChange(event.target.value)} />
<datalist id="contextIDSelect">
{this.state.contextIDArray.map((value, index) => { return <option key={index} value={value}>{value}</option> })}
</datalist>
</div>
)
}
This is the multiUISchema object (the part that matters for this discussion)
multiUISchema = {
file: {
'ui:widget': this.MultiFileWidget,
classNames: "uiSchema"
},
contextID: {
'ui:widget': this.ContextIDWidget,
classNames: "uiSchema"
},
}
And finally here it is in the return in the component.
return (
<div className="container" >
<Form
schema={this.state.populatedMultiSchema}
uiSchema={this.state.populatedMultiUISchema}
formData={this.state.formData}
onChange={({ formData }) => { this.setState({ formData }); this.setState({ totalFileSize: this.getMultiFileSize() }); this.checkConversionSupport() }}
onSubmit={this.handleSubmit}
>
So long story short, if Im using state object in the form, and Im doing setState on the objects Im using. Why do I always get a blank dropdown when I first load the component. Shouldn't the DOM (the dropdown in this case) get repainted with the updated data from the fetches when the state object is changed? I have console logs that show the fetched data in my inspection window so I know the data has been fetched. This is tab component. If I leave the tab or navigate to another page in my SPA and then go back to this page, then the dropdowns are all fully loaded. But I can never get it just load initially unless I set these timeouts in CDM instead of just setting state.
setTimeout(() => this.setState({ populatedMultiSchema: this.multiSchema }), 200);
setTimeout(() => this.setState({ populatedMultiUISchema: this.multiUISchema }), 200);
I know the post is long but felt I needed to include all the parts to get help with this. I can assure you we have been trying to resolve the issue for over a week. We welcome any comments. Thanks!
I am not fully familiar to your codebase. But it looks like something related about asynchronous requests. Here:
this.setState({ populatedMultiSchema: this.multiSchema });
this.setState({ populatedMultiUISchema: this.multiUISchema });
These two lines will be executed BEFORE these ones:
this.getContextID();
this.getConsumerID();
this.getEnvType();
But you expect them to get executed in reverse order. No. Your getContextID method making a request to a server. Javascript is asynchronous. But, by using await expression in an asynchronous function, you can pause the execution and wait for the Promise.
So, just update your componentDidMount method as below:
async componentDidMount() {
await this.getContextID();
await this.getConsumerID();
await this.getEnvType();
this.setState({ populatedMultiSchema: this.multiSchema });
this.setState({ populatedMultiUISchema: this.multiUISchema });
}
Here i created a Codepen on usage of async/await. There are some details in comments. You can play with it as you want.
Even if your problem is not caused mainly by this, this approach is better. You should either use async/await or Promise to work with network requests.