I have a search input I'd like to clear after the value is submitted and the search is performed with the value. In similar questions, it was suggested to set the state of the input value to '', but I think that's what I tried and it didn't do anything.
I only have a parent and child component in my app. The parent component has a method for searching jokes (searchJokes), and it is passed down as a prop with a different name to the child component in the component instance with onFormSubmit={this.searchJokes}. In the child component, when the user enters something into the search input, its event.target.value is passed with onChange={e => props.onInputChange(e.target.value)} corresponding to the onSearchChange method in the parent prop, and the value is used to update the state of searchTerm.
I added searchTerm: '' to the end of the searchJokes method, which fetches a search according to the search term, as you can see in the parent component code below.
Parent component:
class App extends Component {
constructor() {
super();
this.state = {
searchTerm: '',
jokes: [],
isFetchingJokes: false,
isSearch: false
};
this.onSearchChange = this.onSearchChange.bind(this);
this.randomizeJokes = this.randomizeJokes.bind(this);
this.searchJokes = this.searchJokes.bind(this);
}
randomizeJokes() {
this.setState({
isFetchingJokes: true,
isSearch: false
});
fetch(
'https://icanhazdadjoke.com/',
{
method: 'GET',
headers: {
Accept: 'application/json'
}
})
.then(response => response.json())
.then(json => {
let joke = json.joke;
this.setState({
joke,
isFetchingJokes: false
});
});
}
searchJokes(limit = 15) {
// If nothing entered, user gets "Please fill out this field" message due to "required" attribute on input element
if (this.state.searchTerm !== '') {
this.setState({
isFetchingJokes: true,
isSearch: true
});
fetch(
`https://icanhazdadjoke.com/search?term=${
this.state.searchTerm
}&limit=${limit}`,
{
method: 'GET',
headers: {
Accept: 'application/json'
}
})
.then(response => response.json())
.then(json => {
let jokes = json.results;
this.setState({
jokes,
isFetchingJokes: false,
searchTerm: '' // <-- DOESN'T CLEAR INPUT
});
});
}
}
onSearchChange(value) {
this.setState({ searchTerm: value });
}
jokeRender() {
return (
<div>
{this.state.isSearch ?
<ul>{this.state.jokes.map(item => <li key={item.id}>{item.joke}</li>)}
</ul> : <p className="random-joke">{this.state.joke}</p>}
</div>
);
}
render() {
return (
<div>
<h1>Dad Jokes</h1>
<RetrievalForm
onFormSubmit={this.searchJokes}
onInputChange={this.onSearchChange}
isSearching={this.state.isFetchingJokes}
onRandomize={this.randomizeJokes}
/>
{this.state.isFetchingJokes ? <p className="searching-message">Searching for jokes...</p> : this.jokeRender()}
</div>
);
};
}
Child component:
const RetrievalForm = props => {
const onSubmit = e => {
// Prevents GET request/page refresh on submit
e.preventDefault();
props.onFormSubmit();
};
return (
<>
<form onSubmit={onSubmit}>
<input
type="text"
placeholder="Enter search term..."
onChange={e => props.onInputChange(e.target.value)}
required
/>
<div>
{/* Specifying type here since it's good practice; different browsers may use default types for buttons */}
<button type="submit" disabled={props.isSearching}>Search</button>
{/* type="button" stops input validation message from being displayed (on Firefox) when randomize button is clicked without anything entered */}
<button type="button" onClick={props.onRandomize} disabled={props.isSearching} className="randomize-button">
Randomize
</button>
</div>
</form>
</>
);
};
Any help would be greatly appreciated.
You need to pass your searchTerm down to the RetrievalForm and in that input set value={searchTerm} so that it's value will be bound to that state.
Basically, you need to store the input value in the component's state. When onSubmit is called, we should revert that value to an empty string.
Example with some React Hooks goodness:
import React, { Component, useState } from 'react';
const RetrievalForm = props => {
const [searchTerm, setSearchTerm] = useState('');
const onChange = e => {
const { value } = e.target;
props.onInputChange(value);
setSearchTerm(value)
}
const onSubmit = e => {
// Prevents GET request/page refresh on submit
e.preventDefault();
props.onFormSubmit();
setSearchTerm('');
};
return (
<>
<form onSubmit={onSubmit}>
<input
type="text"
value={searchTerm}
placeholder="Enter search term..."
onChange={onChange}
required
/>
<div>
{/* Specifying type here since it's good practice; different browsers may use default types for buttons */}
<button type="submit" disabled={props.isSearching}>
Search
</button>
{/* type="button" stops input validation message from being displayed (on Firefox) when randomize button is clicked without anything entered */}
<button type="button" onClick={props.onRandomize} disabled={props.isSearching} className="randomize-button">
Randomize
</button>
</div>
</form>
</>
);
};
Example here: https://stackblitz.com/edit/react-db5ire
Related
I am very new to react and working on some basics where I came up in the situation - I want to set state immediately after API call.
Scenario:
2 Forms:
1st form => accepts id and calls api to get data of single user
2nd form => updates data
PROBLEM: I want to set state when I get data after clicking submit button on 1st Form
import React, { Component, useEffect } from 'react'
import { connect } from 'react-redux';
import { getSingleUser } from '../redux/user/userActions';
export class UsersContainerUpdate extends Component {
constructor(props) {
super(props);
console.log(props.propFirstName);
this.state = {
id: '',
// first_name: props.propFirstName === '' ? '' : props.propFirstName,
first_name: props.propFirstName,
last_name: props.propLastName === '' ? '' : props.propLastName,
phone: props.propPhone === '' ? '' : props.propPhone,
email: props.propEmail === '' ? '' : props.propEmail,
address: props.propAddress === '' ? '' : props.propAddress,
city: props.propCity === '' ? '' : props.propCity,
state: props.propState === '' ? '' : props.propState,
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleUpdate = this.handleUpdate.bind(this);
}
handleChange = (field, event) => {
this.setState({ [field]: event.target.value });
}
handleSubmit(event) {
// alert('A name was submitted: ' + this.state.name);
event.preventDefault();
const {
id
} = this.state;
const postData = {
id: id
};
// console.log(this.state);
// console.log(postData);
this.props.getSingleUserData(id);
// if (this.props.getSingleUserData(id)) {
// this.setState({
// ...this.state,
// first_name: this.props.propFirstName
// });
// }
}
handleUpdate(event) {
// alert('A name was submitted: ' + this.state.name);
event.preventDefault();
const {
first_name,
last_name,
phone,
email,
address,
city,
state
} = this.state;
const postData = {
first_name: first_name,
last_name: last_name,
phone: phone,
email: email,
address: address,
city: city,
state: state
};
console.log(this.state);
console.log("POSTDATA:", postData);
// alert('hi');
// this.props.updateUserData(id,postData);
}
render() {
return (
<div>
<h1>Update User By ID</h1>
<form onSubmit={this.handleSubmit}>
<div>
<label>ID:</label>
<input
type="text"
value={this.state.id}
onChange={(event, newValue) => this.handleChange('id', event)}
/>
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
<div>
<h1>Update User</h1>
<form onSubmit={this.handleUpdate}>
<div>
<label>First Name:</label>
<input
type="text"
value={this.state.first_name || this.props.propFirstName}
onChange={(event, newValue) => this.handleChange('first_name', event)}
/>
</div>
<div>
<label>Last Name:</label>
<input
type="text"
value={this.state.last_name || this.props.propLastName}
onChange={(event, newValue) => this.handleChange('last_name', event)} />
</div>
<div>
<label>Phone:</label>
<input
type="text"
value={this.state.phone || this.props.propPhone}
onChange={(event, newValue) => this.handleChange('phone', event)} />
</div>
<div>
<label>Email:</label>
<input
type="text"
value={this.state.email || this.props.propEmail}
onChange={(event, newValue) => this.handleChange('email', event)} />
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
<div>
Notice Message : {this.props.propFirstName}
</div>
</div>
</div>
)
}
}
const mapStateToProps = state => {
console.log(state.user);
return {
propFirstName: state.user.first_name,
propLastName: state.user.last_name,
propPhone: state.user.phone,
propEmail: state.user.email,
propAddress: state.user.address,
propCity: state.user.city,
propState: state.user.state
}
}
const mapDispatchToProps = dispatch => {
return {
getSingleUserData: id => dispatch(getSingleUser(id)),
// updateUserData: (id,postData) => dispatch(updateUser(id,postData))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(UsersContainerUpdate)
The console outputs are
The console output of line:81 is the current state which is currently empty. I want to set it there.
Thanks in advance. Cheers!!
If your requirement is just to state after API call inside this.props.getSingleUserData(id),
Approach 1: (Unclean)
Add one more argument to getSingleUserData(id, setState) and pass it this.setState as an argument and inside getSingleUserData you can set the state using the function reference passed
Approach 2:
You can return a promise from getSingleUserData and do setState once it is resolves
Suggestion:
Divide your big component into individual components (like one for getting user ID and one for User data updation). The more we identify and split our project into meanigfull individual components we get more clean codes. Also when you choose to move towards functional components you can reduce lot of boiler plates with hooks.
Problem
state.user is used to set the initial value of your component's state. Changes to those props do not change your state after the component is created. They do change the values in your inputs because the initial value was an empty string '' so you default to showing the value from props. This is very misleading since those inputs don't reflect the current state.
I bet I could delete at least half of this code but that's besides the point. But take a moment to think about why props.propState === '' ? '' : props.propState is always exactly the same as just props.propState.
Solution
I have two key recommendations for how I would rewrite this:
Select user by id
Separate into multiple components
Store only the modifications in the state
Create a selector function selectUserById that selects a user from your Redux state by the id. I don't think it makes sense to store the current user properties as top-level properties of state.user like you have them right now. It seems like you also have a property state.user.users which is an array of all loaded users so I would use that.
const selectUserById = (state, id) => state.user.users.find(user => user.id === id);
Or better yet, store an object of users keyed by id.
const selectUserById = (state, id) => state.user.users[id];
With this approach we either have a complete user object or we have undefined. It's easy to check for undefined and not show the "Update User" form at all until we have real data. This makes more sense than using empty strings as the default.
We can access the complete user object from Redux. I would not duplicate that object in state. Instead I would use the state only for the properties that you have changed. You would start out with the state as an empty object and add properties to it as you modify their inputs. You can always combine the two together using object spreading.
const merged = {...existing, ...changes}
Can you implement these suggestions using class components and connect? Yes. But why add the extra hurdle? Some of the things in your code like this.handleChange.bind(this) are relics of the past when we had to do that because there wasn't a better way. But now we have better ways so you should use them.
Code
Interactive Demo on CodeSandbox
import "./App.css";
import React, { useEffect, useState } from "react";
import { useSelector, useDispatch } from "../store";
import { getSingleUser, updateUser } from "../store/slice";
const selectUserById = (state, id) => state.user.users[id];
const UserIdForm = ({ submitId }) => {
const [id, setId] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
submitId(id);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>ID:</label>
<input
type="text"
value={id}
onChange={(event) => setId(event.target.value)}
/>
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
);
};
const UpdateUserForm = ({ id }) => {
const [changes, setChanges] = useState > {};
const existing = useSelector((state) => selectUserById(state, id));
const dispatch = useDispatch();
// respond to changes in id by clearing the changes state and requesting the user
useEffect(() => {
dispatch(getSingleUser(id));
setChanges({});
}, [dispatch, setChanges, id]);
if (!existing) {
return <div>Loading User...</div>;
}
const merged = { ...existing, ...changes };
const handleChange = (property, event) => {
// in function components you have to copy the whole state
setChanges((prevChanges) => ({
...prevChanges,
[property]: event.target.value
}));
};
const handleUpdate = (event) => {
event.preventDefault();
const postData = { ...merged, id };
console.log("POSTDATA:", postData);
dispatch(updateUser(postData));
};
const renderInput = (property, label) => {
return (
<div>
<label>
{label}
<input
type="text"
value={merged[property]} // shows the current value or the updated value
onChange={(event) => handleChange(property, event)}
/>
</label>
</div>
);
};
return (
<form onSubmit={handleUpdate}>
{renderInput("first_name", "First Name:")}
{renderInput("last_name", "Last Name:")}
{renderInput("phone", "Phone:")}
{renderInput("email", "Email:")}
<div>
<input type="submit" value="Submit" />
</div>
</form>
);
};
const UsersContainerUpdate = () => {
// this is the id that was last submitted.
const [id, setId] = useState();
return (
<div>
<div>
<h1>Update User By ID</h1>
<UserIdForm submitId={setId} />
</div>
{!!id && ( // only load when there is an actual id
<div>
<h1>Update User</h1>
<UpdateUserForm id={id} />
</div>
)}
</div>
);
};
export default UsersContainerUpdate;
I have created dynamic fields from JSON data, and I am successfully rendering on UI
Initially all the fields are disabled.
Once I click on edit I am making particular row editable which is working fine
On click of cancel what I want to do is make the fields disabled again and it should take the previous (initial value)
Issue
When I click on cancel I am setting the initial data aging but it is not taking, I am using react-form-hook for form validation, there we have reset() function but that too is not working.
What I am doing is
Getting data from main component and setting it to some state variable like below
useEffect(() => {
if (li) {
setdisplayData(li);
setCancelData(li);
}
}, [li]);
Now using displayData to render the elements
On click of Edit I am doing this
const Edit = () => {
setdisabled(false);
};
and on click of cancel I am doing below
const cancel = () => {
setdisabled(true); //disbaled true
console.log(cancelData);
setdisplayData(cancelData); setting my main data back to previous one
reset(); // tried this reset of react hook form but it did not work
};
I am using defaultValue so that when I click on Edit the field should allow me to edit.
Here is my full working code
To fix this issue I changed up your code to use value instead of defaultValue. Additionally added an onChange event handler which updates the displayData state whenever <input> changes value. Moreover, you do not need the cancelData state at all since the li prop has the original values.
Now when the onClick for the cancel button is fired, it resets the value of displayData state to whatever li originally was. Here is the modified code:
import React, { useState, useEffect } from "react";
import { useForm } from "react-hook-form";
function component({ li, index }) {
const [disabled, setdisabled] = useState(true);
const [displayData, setdisplayData] = useState(null);
const { register, reset, errors, handleSubmit, getValues } = useForm();
useEffect(() => {
if (li) {
setdisplayData(li);
}
}, [li]);
const Edit = () => {
setdisabled(false);
};
const cancel = () => {
setdisabled(true);
console.log(li);
// Reset displayData value to li
setdisplayData(li);
reset();
};
return (
<div>
<div>
{disabled ? (
<button className="btn btn-primary" onClick={Edit}>
Edit
</button>
) : (
<button className="btn btn-warning" onClick={cancel}>
Cancel
</button>
)}
</div>
<br></br>
{displayData !== null && (
<>
<div className="form-group">
<label htmlFor="fname">first name</label>
<input
type="text"
name="fname"
disabled={disabled}
value={displayData.name}
// Update displayData.name everytime value changes
onChange={({ target: { value } }) =>
setdisplayData((prev) => ({ ...prev, name: value }))
}
/>
</div>
<div className="form-group">
<label htmlFor="lname">last name</label>
<input
type="text"
name="lname"
disabled={disabled}
value={displayData.lname}
// Update displayData.lname everytime value changes
onChange={({ target: { value } }) =>
setdisplayData((prev) => ({ ...prev, lname: value }))
}
/>
</div>
</>
)}
<hr></hr>
</div>
);
}
export default component;
Hope this helps. Drop a comment if it's still not clear :)
ok I'll admit this is a bit of a hot mess but please bear with me.
Trying to understand why my redux-forms:
A: don't set ititialValues until I have set the store elsewhere in the app.. for eg. if I have another event that calls, getFirstTrip, then its in the store, and can load my form correctly with intialValues.
B: wipe themselves out when I click the router link again. Every time I click the router Link it behaves the same as the initial click.. even though the store is there.. the fields and initialValues are empty.
Using:
react-router, react-redux, react-form
structure like this:
view.js
const onSubmit = (formValues) => {
props.updateTrip(formValues); //< traditional put action to api
};
useEffect(() => {
props.getFutureTrip(); // traditional get action to api
//the action dispatches the reducer to create the vacations.nextVacation.tripInfos
}, []);
return (
<div>
Register Page
<TripRegistrationForm onSubmit={onSubmit} />
<Highlight>
{output}</Highlight>
</div>
);
};
const mapStateToProps = state => {
return {
initialValues: state.vacations.nextVacation.tripInfos //< this doesn't seem necessary.
}
}
export default connect(mapStateToProps,{updateTrip, getFutureTrip}) (TripsRegistration);
Parent form TripRegistrationForm.js
class TripRegistrationForm extends Component {
constructor(props) {
console.log("props", props);
super(props);
}
render() {
const { onSubmit, onChange} = this.props;
return (
<div>
<childForm
onChange={onChange}
onSubmit={onSubmit}
/>
</div>
);
}
}
TripRegistrationForm.propTypes = {
onSubmit: PropTypes.func.isRequired,
};
TripRegistrationForm = reduxForm({ form: "tripRegistration" })(TripRegistrationForm);
TripRegistrationForm = connect((state, ownProps) => ({
initialValues: state.vacations.nextVacation.tripInfos
}))(TripRegistrationForm);
export default TripRegistrationForm;
the form contents (these do populate initial values.. but not consistently... ever)
childForm.js
const RegistrationThirdPage = (props) => {
const { handleSubmit, pristine, submitting, lockForm } = props;
return (
<form disabled={lockForm} className="ui form error" >
<FormSection >
<h2>
Trip & Travel Details
</h2>
<Form.Group>
<Field
name="arriving"
placeholder="Arriving"
component={renderDatePicker}
label="Select your arrival date"
/>
<Field
component={renderSelect}
label="Number of Days"
name="packageDays"
options={singleArrayToKVs(colors)}
/>
</Form.Group>
<Form.Group>
<div>
<Button type="button" disabled={lockForm} className="ui-button green" onClick={handleSubmit(values => props.onSubmit({...values, lockVacation:false}))}>
Save my changes
</Button>
</div><div>
<Button type="submit" className="ui-button primary" disabled={bookVacation(pristine,submitting,lockForm)}
onClick={handleSubmit(values => props.onSubmit({...values, lockVacation:true}))}>
Book my vacation
</Button>
</div>
</Form.Group>
</FormSection>
</form>
);
};
export default reduxForm({
form: "tripRegistration", //Form name is same
destroyOnUnmount: false,
forceUnregisterOnUnmount: true, // <------ unregister fields on unmount
// validate,
})(RegistrationThirdPage);
my actions look like this:
export const getFutureTrip = () => async (dispatch,getState) => {
const{token} = getState().auth
if(!token)
return null;
const response = await axios.get(`/api/trip/futureTrip`,{headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}});
console.log("response.data from getFutureTrip", response.data);
dispatch({ type: GET_FUTURE_TRIP, payload: response.data });
};
I have tried to simplify the code down to what is relevant.
class AnnotatedLayout extends React.Component {
state = {
user: '',
enabled: false,
};
render() {
const { user, enabled } = this.state;
const contentStatus = enabled ? 'Disable' : 'Enable';
const textStatus = enabled ? 'enabled' : 'disabled';
return (
...
<Form onSubmit={this.handleSubmit}>
<FormLayout>
<TextField
value={user}
onChange={this.handleChange('user')}
label="Shop Name"
type="user"
helpText={
<span>
Log in with your store username.
</span>
}
/>
<Stack distribution="trailing">
<Button primary submit>
Submit
</Button>
</Stack>
</FormLayout>
</Form>
...
);
}
handleSubmit = () => {
this.setState({
user: this.state.user
});
localStorage.setItem('user', JSON.stringify(this.state.user));
console.log('submission', this.state);
console.log(this.state.user);
};
handleChange = field => {
return value => this.setState({ [field]: value });
};
}
export default AnnotatedLayout;
Essentially, I have a form component to my webpage that, on submitting, is executing this.handleSubmit, and that function is at the bottom.
What my code SHOULD be doing is saving that submitted string to the localStorage with the key 'user', but evidently (you can see below console.log output) that's not happening.
Any idea what's going on?
My website is hosted locally, tunneled to a URL, and used as the base URL for a shopify embedded app, just to give all relevant context.
UPDATE
handleSubmit = () => {
this.setState({
user: this.state.user
},
() => localStorage.setItem('user', "SMH"),
console.log(localStorage.getItem('user'))
);
console.log('submission', this.state);
};
Check this out, after submitting my text form now this is what I get
is localStorage like local or something, to the point where it doesnt save anything outside of a function??
It seems like you handleChange returns a method, which you need to call again to set the user value.
Instead of
<TextField
value={user}
onChange={this.handleChange('user')}
...
Try
<TextField
value={user}
onChange={e => this.handleChange('user')(e)}
...
The value in handleChange should accept e event value, which is the user value to set.
this.setState(
{
user: this.state.user
},
() => localStorage.setItem('user', JSON.stringify(this.state.user))
);
I have a map that render few items and one of its line is below
<a onClick={()=> this.setState({"openDeleteModal":true)}>Delete</a>
Obviously I want to open a modal when user click the delete, but I have to pass a few things like the name of the item, id of the item to perform the deletion. How can I pass says the name to the modal?
I can bind the obj name to a like this
Delete
Am I on the right track?
When working on React applications, try not to think in terms of passing values to other components, but rather updating state that your components are exposed to.
In your example, assuming your modal component is a child of the same component your list of a tags belongs to, you could set the values you are interested in exposing to the modal on the state, as well as updating the property that signals whether the modal is open or not. For example:
class Container extends React.Component {
constructor(props) {
super(props)
this.state = {
openDeleteModal: false,
activeItemName: '', //state property to hold item name
activeItemId: null, //state property to hold item id
}
}
openModalWithItem(item) {
this.setState({
openDeleteModal: true,
activeItemName: item.name,
activeItemId: item.id
})
}
render() {
let buttonList = this.props.item.map( item => {
return (<button onClick={() => this.openModalWithItem(item)}>{item.name}</button>
});
return (
<div>
{/* Example Modal Component */}
<Modal isOpen={this.state.openDeleteModal}
itemId={this.state.activeItemId}
itemName={this.state.activeItemName}/>
{ buttonList }
</div>
)
}
}
Copying over my answer from How to pass props to a modal
Similar scenario
constructor(props) {
super(props)
this.state = {
isModalOpen: false,
modalProduct: undefined,
}
}
//****************************************************************************/
render() {
return (
<h4> Bag </h4>
{this.state.isModalOpen & (
<Modal
modalProduct={this.state.modalProduct}
closeModal={() => this.setState({ isModalOpen: false, modalProduct: undefined})
deleteProduct={ ... }
/>
)
{bag.products.map((product, index) => (
<div key={index}>
<div>{product.name}</div>
<div>£{product.price}</div>
<div>
<span> Quantity:{product.quantity} </span>
<button onClick={() => this.props.incrementQuantity(product, product.quantity += 1)}> + </button>
<button onClick={() => this.decrementQuantity(product)}> - </button> // <----
</div>
</div>
))}
)
}
//****************************************************************************/
decrementQuantity(product) {
if(product.quantity === 1) {
this.setState({ isModalOpen: true, modalProduct: product })
} else {
this.props.decrementQuantity(product)
}
}
Try this: this is the form which has the button, and is a child component of some other component that passes the handleButtonAction method as props, and the button takes the input data and invokes this parent component method
handleSubmit = (e) => {
e.preventDefault();
const data = e.target.elements.option.value.trim();
if (!data) {
this.setState(() => ({ error: 'Please type data' }));
} else {
this.props.handleButtonAction(data, date);
}
}
{this.state.error && <p>{this.state.error}</p>}
<form onSubmit={this.handleSubmit}>
<input type="text" name="option"/>
<div>
<button>Get data</button>
</div>
</form>
The parent component:
handleButtonAction = (data) => {
axios.get(`http://localhost:3000/someGetMethod/${data}`).then(response => {
const resData = response.data;
this.setState({
openModal: true,
status: response.status,
data: resData
});
}).catch((error) => {
if (error.message.toLowerCase() === 'network error') {
this.setStateWithError(-1, {});
}
else { // not found aka 404
this.setStateWithError(error.response.status, '', {currency, date: ddat});
}
});
}