My goal is to pass values selected in a form, to a get request.
The request should look as follows, following submit of the values.
get(api/csv/pets/?columns=DOG&columns=CAT&columns=FISH)
onSubmit={async (values, { resetForm }) => {
alert(JSON.stringify(values, null, 2));
setCsvData(values);
console.log(csvData);
const getCsvFile = async (values) => {
try {
const { data } = await fetchContext.authAxios.get(
`/api/csv/pets/${id}/?columns=${csvData ? csvData + '&' : null}`, values);
toCsv(data);
} catch (err) {
console.log(err);
}
};
getCsvFile();
However, even though formik takes a payload of values, I still get undefined when placing it in csvData with setCsvData(values).
What can I do to get the values selected in the query, and in the format needed?
My data:
export const CheckList = [
{
id: 'column-dog',
label: 'Dog',
value: 'DOG',
name: 'column',
},
{
id: 'column-cat',
label: 'Cat',
value: 'CAT',
name: 'column',
},
{
id: 'column-turtle',
label: 'Turtle',
value: 'TURTLE',
name: 'column',
},
{
id: 'column-fish',
label: 'Fish',
value: 'FISH',
name: 'column',
},
]
My Form:
const [csvData, setCsvData] = useState([]);
const selectAllData = CheckList.map((checkbox) => checkbox.value);
return (
<Formik
initialValues={{
columns: [],
selectAll: false,
}}
onSubmit={async (values, { resetForm }) => {
alert(JSON.stringify(values, null, 2));
setCsvData(values);
console.log(csvData);
resetForm();
}}
>
{({ values, setFieldValue }) => (
<Form>
<div>
<Field
onChange={() => {
if (!values.selectAll) {
setFieldValue('columns', selectAllData);
} else {
setFieldValue('columns', []);
}
setFieldValue('selectAll', !values.selectAll);
}}
checked={values.selectAll}
type="checkbox"
name="selectAll"
/>{' '}
Select All
</div>
{CheckList.map((checkbox) => (
<div key={checkbox.value}>
<label>
<Field
type="checkbox"
name="columns"
value={checkbox.value}
/>{' '}
{checkbox.label}
</label>
</div>
))}
<button
className="btn"
type="submit"
download
>
DOWNLOAD CSV
</button>
</Form>
)}
</Formik>
)
I think you've over-complicated your code a little bit. Remove csvData from state, and instead pass values into request.
onSubmit={async (values, { resetForm }) => {
const getCsvFile = async () => {
try {
const { data } = await fetchContext.authAxios.get(
`/api/csv/pets/${id}/?columns=${values ? values + '&' : null}`, values);
toCsv(data);
} catch (err) {
console.log(err);
}
};
getCsvFile();
}
You were getting an error is because the request was firing before csvData was set. setState is asynchronous :)
You cannot use setState inside an async function, becuase setState is an async function itself and it doesn't return a promise. As a result, we can't use async/await for setState method.(If it's not possible to make it await, then state change will trigger right away with undefined values instead of setting the given values)
Please refer this for more info: https://iamsongcho.medium.com/is-setstate-async-b1947fbb25e5
Therefore, the best use case is to remove the async from onSubmit() function to make it synchronous. As a result, setState will trigger the state change in component asynchronously.
Then keep getCsvFile(values), after form reset as you've already done that. And keep getCsvFile() function outside the component, since it doesn't do any state relevant changes.
onSubmit={(values, { resetForm }) => {
alert(JSON.stringify(values, null, 2));
setCsvData(values);
console.log(csvData);
resetForm();
getCsvFile(); // this function will trigger asynchronously
}}
Related
I have this simple example of Formik where i have a simple input. When i run the page i see in the console that it renders twice. The formik package is exactly the same as the first message.
Why it renders twice if there is nothing changed?
const SignupForm = () => {
const [data, setData] = useState({
firstName: "",
lastName: "",
email: "",
});
return (
<Formik
initialValues={data}
enableReinitialize
validateOnBlur={false}
validateOnChange={false}
onSubmit={(values, { setSubmitting }) => {
}}
>
{(formik) => {
console.log(formik);
return (
<form onSubmit={formik.handleSubmit}>
<label htmlFor="firstName">First Name</label>
<input
id="firstName"
type="text"
{...formik.getFieldProps("firstName")}
/>
{formik.touched.firstName && formik.errors.firstName ? (
<div>{formik.errors.firstName}</div>
) : null}
</form>
);
}}
</Formik>
);
};
That is happening due to enableReinitialize property.
Formik itself has a few useEffects inside of it, and a formikReducer. So when you pass enableReinitialize - formikReducer is called 2 times:
payload: {}, type: "SET_ERRORS"
payload: {}, type: "SET_TOUCHED"
Which is happening due to folowing useEffects inside of the source codes:
React.useEffect(function () {
if (enableReinitialize && isMounted.current === true && !isEqual(initialErrors.current, props.initialErrors)) {
initialErrors.current = props.initialErrors || emptyErrors;
dispatch({
type: 'SET_ERRORS',
payload: props.initialErrors || emptyErrors
});
}
}, [enableReinitialize, props.initialErrors]);
React.useEffect(function () {
if (enableReinitialize && isMounted.current === true && !isEqual(initialTouched.current, props.initialTouched)) {
initialTouched.current = props.initialTouched || emptyTouched;
dispatch({
type: 'SET_TOUCHED',
payload: props.initialTouched || emptyTouched
});
}
}, [enableReinitialize, props.initialTouched]);
And those ifs are passed due to the initialTouched and initialErrors are initialized inside of the Formik with this:
var initialErrors = React.useRef(props.initialErrors || emptyErrors);
var initialTouched = React.useRef(props.initialTouched || emptyTouched);
So initial values are equal to empty ones which are {}. But inside of the if they have !isEqual(initialErrors.current, props.initialErrors)) for example, so comparison between {} and undefined is passed and we are going inside of the if body and updating the Formik internal state. That is what is causing an additional rerender.
So if you pass the following props to Formik component - console.log will be executed only once
initialErrors={{}}
initialTouched={{}}
Now about how to collect that information:
Configure local minimal workspace with React and Formik
Go to node_modules/formik/dist/formik.cjs.development.js and inject some logging code inside of the formikReducer, simple console.log
In the component that is using <Formik> import it from modified development js file. import { Formik } from "formik/dist/formik.cjs.development";
Formik version: 2.2.9
I'm new to React and I'm currently learning about useReducer.
I've created a simple login feature that verifies if the user inputted email includes '#' and the password length is greater than 5.
If these two conditions are met, I want my program to display an alert with success or fail message when pressing on the submit button.
What I'm curious about is that the application displays "Success" on submit when I add dispatch({type: 'isCredValid')} in useEffect(commented out in the code below), but the application displays "fail" when I add the dispatch({type: 'isCredValid'}) in the onSubmit handler without using useEffect. I was expecting the application to display "Success" when adding the dispatch({type: 'isCredValid')} in the onSubmit handler without the help of useEffect. Why is it not displaying "Success"? And why does my application display "Success" when the dispatch function is in the useEffect?
Reducer function :
const credReducer = (state, action) => {
switch(action.type) {
case 'email' :
return {...state, email: action.value, isEmailValid: action.value.includes('#')};
case 'password' :
return {...state, password: action.value, isPasswordValid: action.value.length > 5 ? true : false};
case 'isCredValid' :
return {...state, isCredValid: state.isEmailValid && state.isPasswordValid ? true : false};
default :
return state;
}
}
Component and input handlers
const Login = () => {
const [credentials, dispatch] = useReducer(credReducer, {
email: '',
password: '',
isEmailValid: false,
isPasswordValid: false,
isCredValid: false
})
// useEffect(() => {
// dispatch({type: 'isCredValid'})
// }, [credentials.isEmailValid, credentials.isPasswordValid])
const handleSubmit = (e) => {
e.preventDefault()
dispatch({ type: "isCredValid" })
if (credentials.isCredValid === true) {
alert ("Success!")
} else {
alert ('failed')
}
}
const handleEmail = (e) => {
dispatch({ type: "email", value: e.target.value })
}
const handlePassword = (e) => {
dispatch({ type: "password", value: e.target.value })
}
return (
<Card className={classes.card}>
<h1> Login </h1>
<form onSubmit={handleSubmit}>
<label>Email</label>
<input type="text" value={credentials.email} onChange={handleEmail}/>
<label>Password</label>
<input type="text" value={credentials.password} onChange={handlePassword}/>
<button type="submit"> Submit </button>
</form>
</Card>
)
}
if (credentials.isCredValid === true) {
alert ("Success!")
} else {
alert ('failed')
}
You are probably referring to above alert that you didn't immediately see "Success". That doesn't happen like that, just like with updating state, when you dispatch something, you will see the update on the next render.
This useEffect may work, but you're kind of abusing the dependency array here. You're not actually depending on credentials.isEmailValid or credentials.isPasswordValid. You should use these dependencies to decide which action to dispatch, and maybe that's your plan already.
The reason your handleSubmit doesn't seem to work, is what others point out. You won't be able to see the result until next render, so not inside the handleSubmit function.
// useEffect(() => {
// dispatch({type: 'isCredValid'})
// }, [credentials.isEmailValid, credentials.isPasswordValid])
const handleSubmit = (e) => {
e.preventDefault()
dispatch({ type: "isCredValid" })
if (credentials.isCredValid === true) {
alert ("Success!")
} else {
alert ('failed')
}
}
To see the results, add another useEffect and trigger the alert from there:
useEffect(() => {
if(credentials.isCredValid){
alert('Success!')
}
}, [credentials])
I have a list of objects. I want to make an api call once the location field of the object is changed. So for that, I have a useEffect that has id, index and location as its dependencies. I have set a null check for the location, if the location isn't empty, I want to make the api call. But the thing is, the api is being called even when the location is empty, and I end up getting a 400. How can I fix this and make the call once location isn't empty?
const [plants, setPlants] = useState([
{
name: 'Plant 1',
id: uuidv4(),
location: '',
coords: {},
country: '',
isOffshore: false,
}
]);
const [locationIDObject, setlocationIDObject] = useState({
id: plants[0].id,
index: 0
});
const handlePlantLocChange = (id, index, value) => {
setPlants(
plants.map(plant =>
plant.id === id
? {...plant, location: value}
: plant
)
)
setlocationIDObject({
id: id,
index: index
})
}
const getCoords = (id, index) => {
axios.get('http://localhost:3002/api/coords', {
params: {
locAddress: plants[index].location
}
}).then((response) => {
if(response.status === 200) {
handlePlantInfoChange(id, PlantInfo.COORD, response.data)
}
})
}
const handler = useCallback(debounce(getCoords, 5000), []);
useDeepCompareEffect(() => {
if(plants[locationIDObject.index].location !== '')
handler(locationIDObject.id, locationIDObject.index);
}, [ locationIDObject, plants[locationIDObject.index].location])
return (
<div className='plant-inps-wrapper'>
{
plants.map((item, idx) => {
return (
<div key={item.id} className="org-input-wrapper">
<input placeholder={`${item.isOffshore ? 'Offshore' : 'Plant ' + (idx+1) + ' location'}`} onChange={(e) => handlePlantLocChange(item.id, idx, e.target.value)} value={item.isOffshore ? 'Offshore' : item.location} className="org-input smaller-input"/>
</div>
)
})
}
</div>
)
I think your useCallback is not updating when value of your variables is changing and that is the issue:
Although the check is correct, but the call is made for older values of the variable. You should update the dependencies of your useCallback:
console.log(plants) inside getCoords might help you investigate.
Try this:
const handler = useCallback(debounce(getCoords, 5000), [plants]);
So it turns out the issue was with my debouncing function. I don't know what exactly the issue was, but everything worked as expected when I replaced the debouncing function with this:
useEffect(() => {
console.log("it changing")
const delayDebounceFn = setTimeout(() => {
getCoords(locationIDObject.id, locationIDObject.index)
}, 4000)
return () => clearTimeout(delayDebounceFn)
},[...plants.map(item => item.location), locationIDObject.id, locationIDObject.index])
WITHOUT react-paypal-button-v2 ~~~has an ovehead of 60KB
Similar question here but they suggest react-paypal-button-v2
I'm Trying to make a React PayPal button that changes the billing amount on props change.
I call the following component with props price and every time the price change i would like to re-render the button to update the actual price. WITHOUT react-paypal-button-v2
const PaypalForm = props => {
let paypalRef = useRef();
useEffect(() => {
window.paypal
.Buttons({
createOrder: (data, actions) => {
return actions.order.create({
purchase_units: [
{
description: "test",
amount: {
currency_code: "USD",
value: props.price
}
}
]
});
},
onApprove: async (data, actions) => {
const order = await actions.order.capture();
console.log(order);
},
onError: err => {
setError(err);
console.error(err);
}
})
.render(paypalRef.current);
}, [props.price]);
return (
<Row className="justify-content-center">
{error && <div>Uh oh, an error occurred! {error.message}</div>}
<div ref={paypalRef} />
</Row>
);
};
Everything is working except that a new button is created and added in the bottom of old one at each props change. I would like my new button to replace the old one. Without using react-paypal-button-v2
Something like:
useEffect(() => {
if(window.myButton) window.myButton.close();
window.myButton = window.paypal
.Buttons({
createOrder: (data, actions) => {
return actions.order.create({
purchase_units: [
{
description: "test",
amount: {
currency_code: "USD",
value: props.price
}
}
]
});
},
onApprove: async (data, actions) => {
const order = await actions.order.capture();
console.log(order);
},
onError: err => {
setError(err);
console.error(err);
}
});
window.myButton.render(paypalRef.current);
However, you do not actually need to re-render the button on price change!
You can do value: document.getElementById('...').value or similar (or whatever variable or function call you need)
In your example, if props.price returns the (new/current) desired value when the button is clicked, then that value will be used.
Basically, the createOrder function isn't called until you click a button.
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.