Iam trying to create a dropdown component and would like to use that selected option through out my app.
The thought is when a user select a Dropdown value then, its state got save in Redux reducer then to use that value for other action. But being a beginner Iam stuck on implementation part.
Note: The dropdown wouldnt have a submit button, just the action of selecting the drop down option.
My code until this stage looks like this:
RoleDropdown.js
class RoleDropdown extends Component {
constructor(props) {
super(props);
this.state = {
value: ''
};
}
...
...
render() {
return (
<div>
<select
onChange={() => this.props.selectedRoleAction()}
name="roles" className="form-control">
<option>Select a Role</option>
<option value="ABC" >ABC</option>
<option value="DEF" >DEF</option>
<option value="GHI" >GHI</option>
</select>
<p>role is: {this.props.activeRole.value}</p> //No value output here
</div>
)
}
SelectedRoleAction.js
const selectedRoleAction = (role) => {
const [value, setValue] = useState("")
setValue({ value: role.target.value })
console.log("event from drop down is " + role) //I cant see any logged value as well
return {
type: "ROLE_SELECTED",
payload: role,
}
};
Where Am I doing wrong? Does the "setValue" can be used in action reducers?
An action creator does not hold any local state. It is just a function that maps from some arguments to a Redux action which is an object with properties type and payload.
This is an action creator function:
const selectedRoleAction = (role) => {
return {
type: "ROLE_SELECTED",
payload: role,
}
};
In which case your component would call:
onChange={(e) => this.props.selectedRoleAction(e.target.value)}
You are trying to map from the event to the value in the action creator rather than in the component. This is unusual and I would recommend the above approach, but it's doable.
const selectedRoleEventHandler = (event) => {
return {
type: "ROLE_SELECTED",
payload: event.target.value,
}
};
In which case your component would call:
onChange={(e) => this.props.selectedRoleEventHandler(e)}
or just:
onChange={this.props.selectedRoleEventHandler}
Right now you are calling the function with no arguments this.props.selectedRoleAction() which will not work.
That just creates the action. I'm assuming that you are using the connect higher-order component so that calling this.props.selectedRoleAction will dispatch it to Redux.
The actual state is set through your Redux reducer function.
If the value is stored and updated through Redux then it should not also be in the component state.
You are dealing with a controlled component so you'll want to set the value property on your select.
I am disabling "Select a Role" and also giving it a value of the empty string "". I am using that value as a fallback if this.props.activeRole is not set.
<select
name="roles"
className="form-control"
value={this.props.selectedRole || ""}
onChange={(e) => this.props.selectedRoleAction(e.target.value)}
>
<option value="" disabled>Select a Role</option>
<option value="ABC">ABC</option>
<option value="DEF">DEF</option>
<option value="GHI">GHI</option>
</select>
Related
In React, I have created a Component to allow the user to submit a book to add to a list of books. The site visually looks like this (the AddBook component is framed in red):
I will share my full code further below, but first I just need to explain the problem. In the AddBook component, the "select author" dropdown menu (that you see in the screenshot above) is using <select> <option> elements, which allows the user to select from a list of authors, before submitting the form to add a book. The list of authors are fetched from a GraphQL API call.
snippet code:
function displayAuthors() {
if (loading) return (<option disabled>'Loading authors...'</option>);
if (error) return (`Error: ${error.message}`);
return data.authors.map((author) => {
return (
<option key={author.id} value={author.id}>{author.name}</option>
);
});
}
return (
<form id="add-book" onSubmit={submitBook}>
<h1>Add a new book to Wiki Books</h1>
<div className="field">
<label>Book name: </label>
<input type="text" onChange={handleChange} name="name" required value={bookEntry.name}/>
</div>
<div className="field">
<label>Genre: </label>
<input type="text" onChange={handleChange} name="genre" required value={bookEntry.genre}/>
</div>
<div className="field">
<label>Author (select author): </label>
<select onChange={handleChange} name="authorId">
{displayAuthors()}
</select>
</div>
<button type="submit">+</button>
</form>
The roadblock I've hit is ensuring the dropdown menu <select> element is REQUIRED, so it ensures selecting an author is required before submitting the book info. Now, after searching through forums I've concluded that React doesn't seem to have a native solution implemented for <select> REQUIRED validation. Instead there are convoluted hack solutions as workarounds.
To mitigate the issue, I simply removed the first <option> Select author </option> (you can't see it in the code shared because I've already removed it). So that leaves the dropdown menu, by default, set on the first author in the list, on the initial render of the page. Thereby forcing the user to have a choice of author selected by default. Of course they can always change the option to choose a different author. But the point is an author choice is already enforced by default.
Now the next issue I faced with this approach is - on initial render of the page, even though an author is already selected in the dropdown list by default, the corresponding <option> element for that author, its value of author.id doesn't get detected by React on initial render of the Component (this authorId value from the option element is needed for the book submission, calling an API to submit the book info to the database).
You would have to change the menu option first for the onChange attribute event listener in <select> element to detect the value of the selected <option> (the authorId). Which means my solution for ensuring an author is always selected (even on initial page render) is now pointless as React doesn't pick up the authorID value of the initial selected author.
<option key={author.id} value={author.id}>{author.name}</option> // value={author.id} doesn't get detected in initial render of page, unless menu option is changed for `onChange` to detect value={authorId}
.
To solve this, my solution is to create a state that would be set to the first author from the list of authors fetched from the API call. So that should be Patrick Rothfuss - the default selected author in the dropdown menu. So the authorId of Patrick Rothfuss is set to this state. Then this state can be used in the book submission, if the user doesn't changed the dropdown menu at all when submitting the form.
const [firstAuthorId, setFirstAuthorId] = useState("");
function displayAuthors() {
if (loading) return (<option disabled>'Loading authors...'</option>);
if (error) return (`Error: ${error.message}`);
return data.authors.map((author, index) => {
if (index === 0) {
setFirstAuthorId(author.id);
}
return (
<option key={author.id} value={author.id}>{author.name}</option>
);
});
}
Now the issue I'm facing here is when I set the state to the first author Id, I get the error below:
In fact, from troubleshooting the issue, the cause of the issue seems to be with that particular state setter setFirstAuthorId. No matter what I set it to, and from where I set it in the Component, it throws the error shown in the screenshot.
setFirstAuthorId("anything") // will throw an error
So as a workaround (which is not ideal), I created a conditional (ternary expression) that would check, upon the book submission (where the bookEntry object state is submitted to the API), if there is no authorId, then set the authorId property of bookEntry state to a hardcoded authorId of the first author (Patrick Rothfuss). Ideally the state firstAuthorId should be set to this instead of a hardcoded ID.
function submitBook(event) {
const filledAuthorId = bookEntry.authorId? bookEntry.authorId : "5ed44e015ecb7c42a0bf824d";
addBook({ variables: {
name: bookEntry.name,
genre: bookEntry.genre,
authorId: filledAuthorId
},
refetchQueries: [{ query: GET_ALL_BOOKS_QUERY }]
})
Full code:
import React, { useState } from 'react';
import { useQuery, useMutation } from '#apollo/react-hooks';
import { GET_ALL_AUTHORS_QUERY, ADD_BOOK_MUTATION, GET_ALL_BOOKS_QUERY } from '../queries/queries';
function AddBook() {
const { loading, error, data } = useQuery(GET_ALL_AUTHORS_QUERY);
// to call more than one query using the useQuery, call useQuery hook but give alias names to loading, error, data to avoid queries overwriting each other's returned fields
// const { loading: loadingAddBook, error: errorAddBook, data: dataAddBook} = useQuery(A_SECOND_QUERY)
const [ addBook ] = useMutation(ADD_BOOK_MUTATION);
const [bookEntry, setBookEntry] = useState({
name: "",
genre: "",
authorId: ""
});
const [firstAuthorId, setFirstAuthorId] = useState("");
function handleChange(event) {
const { name, value } = event.target;
console.log(`handleChange event: ${event.target.value}`);
setBookEntry(preValue => {
return {
...preValue,
[name]: value
}
});
}
function submitBook(event) {
const filledAuthorId = bookEntry.authorId? bookEntry.authorId : "5ed44e015ecb7c42a0bf824d";
addBook({ variables: {
name: bookEntry.name,
genre: bookEntry.genre,
authorId: filledAuthorId
},
refetchQueries: [{ query: GET_ALL_BOOKS_QUERY }]
})
.then(data => {
console.log(`Mutation executed: ${JSON.stringify(data)}`);
setBookEntry(prevValue => ({
...prevValue,
name: "",
genre: ""
}));
})
.catch(err => console.log(err));
event.preventDefault();
}
function displayAuthors() {
if (loading) return (<option disabled>'Loading authors...'</option>);
if (error) return (`Error: ${error.message}`);
return data.authors.map((author, index) => {
if (index === 0) {
setFirstAuthorId(author.id);
}
return (
<option key={author.id} value={author.id}>{author.name}</option>
);
});
}
return (
<form id="add-book" onSubmit={submitBook}>
<h1>Add a new book to Wiki Books</h1>
<div className="field">
<label>Book name: </label>
<input type="text" onChange={handleChange} name="name" required value={bookEntry.name}/>
</div>
<div className="field">
<label>Genre: </label>
<input type="text" onChange={handleChange} name="genre" required value={bookEntry.genre}/>
</div>
<div className="field">
<label>Author (select author): </label>
<select onChange={handleChange} name="authorId">
{displayAuthors()}
</select>
</div>
<button type="submit">+</button>
</form>
);
}
export default AddBook;
Given that you only need to update the firstAuthorId state with the first index of the data.authors array that is returned from the query, you should do it when the component is mounted, rather than calling the displayAuthors method and updating the state everytime to component re-renders.
This can be achieved using the useEffect hook.
useEffect(() => {
setFirstAuthorId(data.authors[0].id)
}, []);
Alternatively, you can set data.authors as part of the dependency array, such that the firstAuthorId state will be updated every time data.authors has changed.
useEffect(() => {
setFirstAuthorId(data.authors[0].id)
}, [data.authors]);
Better still, there is actually no need to maintain a separate state(firstAuthorId) to track the first object of data.authors. You can simply reference to it on whereever you need in the code itself.
In my react app i have an endpoint that returns user details and another on that returns countries and I'm calling both of them in the componentDidMount, i have a country dropdown and it's default value/option should be the country returned from the user details endpoint and it's options should be the countries list returned from the countries endpoint, the problem is when i use the value attribute to set the default value for the select it's always null and it doesn't show a value, so i tried using the <option selected="selected">{this.state.country}</option> but the onChange function doesn't work when i use it, so what's the best approach to achieve that, here is the code:
Using the value attribute:
<select value={this.state.country} onChange={this.handleChange}>{this.renderCountries()}</select>
Using the selected option:
<select onChange={this.handleChange}>
<option selected="selected">{this.state.country}</option>
{this.renderCountries()}
</select>
OnChange function:
handleChange = event => {
this.setState({ selectedOption: event.target.value });
};
Rendering options:
renderCountries() {
return this.state.countries.map(country => (
<option key={country.id} value={country.id}>
{country.name}
</option>
));
}
Set this.state.country to the value returned from the endpoint.
<select value={this.state.country} onChange={this.handleChange}> .
{this.renderCountries()}
</select>
The handleChange needs to setState for country not selectedOption. This is because the value attribute on select is this.state.country
handleChange = event => {
this.setState({ country: event.target.value });
};
renderCountries = () => {
return this.state.countries.map(country => (
<option key={country.id} value={country.id}>
{country.name}
</option>
));
};
Hopefully this should fix your issue.
I am learning react and struggling to get a second dropdown to populate based on which option is clicked from first dropdown.
I have included my code below.
I think the issue is where I try to set this.state.selected = param.tableName. I don't think that will work, but I'm not sure what to use instead.
I need an onClick event to change the this.state.selected variable to the option that is selected I think, and then check when tableName === selected. I will also include my JSON below so the context is more clear.
import React from 'react';
import axios from 'axios';
class Search extends React.Component {
constructor(props) {
super(props)
this.state = {
params: [],
columnParams: [],
selected: ''
}
}
componentWillMount() {
axios.get('http://localhost:3010/api/schema')
.then( response => {
this.setState({params: response.data})
})
}
render() {
return (
<div className="Search">
<select>{this.state.params.map((param, i) =>
<option key={i} onClick={this.setState(this.state.selected ={param.tableName})}>
{param.tableName}
</option>)}
</select>
<select>
{
this.state.params
.filter(({tableName}) => tableName === selected)
.map(({columns}) => columns.map((col) =><option>{col}</option>))}
[
{
"tableName": "customer",
"columns": [
"customerid",
"title",
"prefix",
"firstname",
"lastname",
"suffix",
"phone",
"email"
]
},
{
"tableName": "product",
"columns": [
"productid",
"name",
"color",
"price",
"productadjective",
"productmaterial"
]
},
You're doing it wrong way. You cannot call onClick method on <option>. Instead you should use onChange method on <select> (see react docs) like this:
<select onChange={this.handleChange} >
<option>1</option>
<option>2</option>
<option>3</option>
</select>
Then on 'onChange' event you can set your state to the selected option. Let's understand this scenario with an example.
Suppose you have two dropdowns. One for showing companies and one for jobs corresponding to each company. Each company has its own set of jobs like this:
companies:[
{ name: 'company1', jobs: ['job1-1', 'job1-2', 'job1-3']},
{ name: 'company2', jobs: ['job2-1', 'job2-2', 'job2-3']},
{ name: 'company3', jobs: ['job3-1', 'job3-2', 'job3-3']}
]
Now you have to just set a state, lets say 'selectedCompany' and then filter the companies array by 'selectedCompany'. Then you can just get that particular company object and map your 'jobs' dropdown by iterating over jobs array inside the company object.
Here is the code snippet:
https://codepen.io/madhurgarg71/pen/pRpoMw
event handlers for e.g. onClick must be a function
this.setState expects an object, which will be merged with the state
so the part where you set selected in your state must be
<option key={i} onClick={() => {
this.setState({selected: param.tableName})
}>{param.tableName}}}</option>
you use an undefined variable in your filter (selected),
you have to use .filter(({tableName}) => tableName === this.state.selected) instead
Some things to look at:
The syntax in your call to setState in your <option> onClick event handler looks incorrect.
You also make try to reference this.state.selected in your .filter condition without the path to the variable (this.state)
Suggested solution(s):
// The .setState method expects an object to be passed to it rather
// than an expression that mutates the state directly
// Pass it an object containing the properties you want to change,
// ensure your use colon (':') rather than equals sign ('=') to set property:
<option key={i} onClick={
this.setState({
selected: param.tableName
})
</option>
// In the <select> tag,
// you need to refer to the selected property in it as this.state.selected:
// Don't forget the closing select tag, it's missing from your snippet
<select>
{
this.state.params
.filter(tableName => (tableName === this.state.selected))
.map(columns => columns.map(col => <option>{col}</option>))
}
</select>
This issue can easily be dealt with by chaining a filter and map together on the second input, which filters it's options depending on the input of the first drop down.
<select value={country} onChange={e => setCountry(e.target.value)}>
{countries.map(country => (
<option value={country}>{country}</option>
))}
</select>
<select value={school} onChange={e => setSchool(e.target.value)}>
{schools.filter(school => school.Country === country).map(school => (
<option value={school.Name}>{school.Name}</option>
))}
</select>
Above we have two selects. The first selects a value, and then the options for the second will render based on this.
I have a simple component that renders select tag and synchronizes selected value with the state:
class App extends React.Component {
onChange = (event) => {
this.props.updateValue("value", event.target.value);
};
render() {
const {value} = this.props;
return <div>
<label>
<span>Select smth</span>
<select name="select" value={value} onChange={this.onChange}>
<option value="1">First</option>
<option value="2">Second</option>
</select>
</label>
<p>Value: {value}</p>
</div>;
}
}
export default connect(state => {value: state.value}, {updateValue: actions.updateValue})(App)
My problem that at the first render, the value coming from the store is empty. However when I am checking value with document.querySelector('[name="select"]').value it shows me some value. So my UI state is out of sync with store state. When later user will submit the form, the value will not be sent.
How can I always keep select value in sync with Redux store state?
See the full code at CodePen:
http://codepen.io/anon/pen/ObxNPp?editors=0010
UPD. The actual code also uses Redux. Updated code sample to show that. Setting initial value will not work because store doesn't know anything about available options.
Your initial state for the <select> tag is not set properly. You need to set:
class App extends React.Component {
state = {
value: 1
};
onChange = (event) => {
this.setState({value: event.target.value});
};
render() {
const {value} = this.state;
return <div>
<label>
<span>Select smth</span>
<select name="select" value={value} onChange={this.onChange}>
<option value="1">First</option>
<option value="2">Second</option>
</select>
</label>
<p>Value: {value}</p>
</div>;
}
}
Since the value of value is undefined as per your initial state, the select menu is not given a selected value which results in the first option being rendered as selected by the browser which is the default behavior of the drop-down menu in the browser.
Check out this forked CodePen for clarity.
Problem is you didn't assign any value to your state variable "value". put a console console.log('value', value) after const {value} = this.state; you will see the value is undefined. when you are checking the value with document.querySelector('[name="select"]').value, it is giving you the value of select not the state variable, once you are changing the dropdown, state variable receives a value after this everything will work proper. State variable value will be undefined until you change the dropdown.
since the state value is undefined, select rendered the first element.
Assign a proper value to your state variable :
state = {
value: 1 or 2,
};
Set your state to the be the correct value at the beginning:
http://codepen.io/anon/pen/qqPZva
state = {
value: this.props.value
};
ReactDOM.render(
<App value="2" />,
document.getElementById('root')
);
Update based upon comments:
state = {
value: this.props.options[0].value
};
I making a react app and I have a parent component Search with child components Input and Result. Input has a drop down menu which passes a value, genreValue, to Search, through a callback function when a button is clicked. Search then makes an api call, which works fine.
My problem is it takes two clicks of the button for the new API data to render. Looking at other SO questions I suspect I need to pass genreValue as an argument to the cb function, or my onClick is only initialising, rather than invoking it on the first click.
It's a pretty simple app so I wouldn't think Flux etc would be needed. My console logs seem to show the value being changed in the Search and Input components.
So what am I doing wrong?
Search.js
let Search = React.createClass ({
getInitialState(){
return {
movies: ['Men In Black'],
genreValue: '12'
};
},
componentDidMount(){
this.getMovies()
},
getMovies(){
let genre = this.state.genreValue;
let url = `http://api.themoviedb.org/3/discover/movie?${key}&with_genres=${genre}`;
Request.get(url).then((response) => {
console.log('response.body.results', response.body.results)
this.setState({
movies: response.body.results.map(function(movie){
return movie.title
})
});
});
},
handleGenre(newGenre) {
this.setState({ genreValue: newGenre })
return this.getMovies();
},
render(){
console.log(this.state.movies)
console.log('genreValue state', this.state.genreValue)
return (
<div>
<Input genre={this.state.genreValue} onGenreChanged={this.handleGenre}/>
<ul>
{this.state.movies.map( function(movie){
return <Results key={movie.id} data={movie}/>;
})}
</ul>
</div>
);
}
});
export default Search;
Input.js
let Input = React.createClass ({
selectHandler(){
return this.props.onGenreChanged(this.refs.genre.value);
},
render() {
console.log('genreValue prop', this.props.genre);
console.log('refs', this.refs.genre)
return <div>
<select ref="genre">
<option value="28">Action</option>
<option value="12">Adventure</option>
<option value="16">Animation</option>
<option value="35">Comedy</option>
<option value="80">Crime</option>
<option value="99">Documentary</option>
<option value="18">Drama</option>
<option value="10751">Family</option>
<option value="14">Fantasy</option>
<option value="10769">Non-english</option>
<option value="36">History</option>
</select>
<button onClick={this.selectHandler} value="Go">Go</button>
</div>
}
});
export default Input;
In the handleGenre function, state may not have updated when this.getMovies is called. You could change it to the following:
handleGenre(newGenre) {
this.setState({ genreValue: newGenre }, function() {
return this.getMovies();
});
},
Or, probably better practice would be to call this.getMovies in a componentDidUpdate lifecycle function if genreValue has changed:
componentDidUpdate: function(prevProps, prevState) {
if (prevState.genreValue !== this.state.genreValue) {
this.getMovies();
}
}