The user must enter a name and hospital in the form, then their name and hospital will appear in a side menu. But I can't make it happen, useState returns an empty state so it's not working
export function Home() {
const [nome, setNome] = useState("")
const [hospital, setHospital] = useState("")
const [describe, setDescribe] = useState("")
const [patient, setNewPatient] = useState({})
function handleCreateNewPatient(event){
event.preventDefault()
setNewPatient({
id: nome.length,
nome: nome,
hospital: hospital,
describe: describe
})
Users.push(patient)
}
the code with UI material of the form
<form onSubmit={handleCreateNewPatient} >
<div>
<TextField
value={nome}
onChange={event=>setNome(event.target.value)}
/>
<TextField
value={hospital}
onChange={event=>setHospital(event.target.value)}
/>
</div>
<div>
<TextareaAutosize
value={describe}
onChange={event=>setDescribe(event.target.value)}
/>
</div>
<Button type="submit">Confirmar</Button>
</form>
The issue is here:
setNewPatient({
id: nome.length,
nome: nome,
hospital: hospital,
describe: describe
})
Users.push(patient)
setState() is asynchronous. React does not guarantee that the state changes are applied immediately. Think of setState() as a request rather than an immediate command to update the component.
To get the updated value you have to use useEffect like:
useEffect(() => {
if( patient.hasOwnProperty('id') ) // Checking if patient is set or not
{
Users.push(patient)
}
}, [patient]); // Here `patient` is the dependency so this effect will run every time when `patient` change.
Such type of logic will help you.
State updates are not synchronous in React. You can do something like this:
function handleCreateNewPatient(event){
event.preventDefault();
const patient = {
id: nome.length,
nome: nome,
hospital: hospital,
describe: describe
};
setNewPatient(patient)
Users.push(patient)
}
or write an effect as mentioned in the other answer.
Related
I try to use watch hook to watch a variable changes - city. However, I would like that only city variable produces the effect, not all the form (say, name should not log anything): see the sample on Stackblitz:
import * as React from 'react';
import { useForm } from 'react-hook-form';
export default function App() {
const { watch, register } = useForm({
mode: 'all',
reValidateMode: 'onBlur',
});
const cityValue = watch('city', 'my city');
const nameValue = watch('name', 'my name');
// Will console the changes
React.useEffect(() => {
const subscription = watch((value, { name, type }) =>
console.log(value, name, type)
);
return () => subscription.unsubscribe();
}, [cityValue]);
return (
<div>
<p>Start editing & look at the console how values changes :</p>
<input value={cityValue} {...register('city')} />
<label>{cityValue}</label> <br />
<input value={nameValue} {...register('name')} />
<label>{nameValue}</label> <br />
</div>
);
}
Why does the [cityValue] filter in the useEffect not work?
It seem that the useEffect with [cityValue] above would be same as useEffect with []...
The condition on useEffect only states under which circumstances to re-run the effect, it has no effect on the values inside the effect. The watch call inside your effect applies to all watches it seems, and registers a change-listener for all of them. It also does so every time the city value changes, which you probably don't want.
I think what you want is just:
React.useEffect(() => console.log(cityValue), [cityValue]);
I have a mock mutation like so:
interface Person {
firstName: string;
lastName: string;
}
async function sendPersonApi({ firstName, lastName }: Person) {
await new Promise((res) => setTimeout(res, 1000));
return {
firstName,
lastName,
status: "success"
};
}
I have two components: <Form /> and <Output />. I basically want the mutation to run on form submit of the <Form />, and then show the result of that mutation in the <Output />.
I have a CodeSandbox with this behavior mostly working: https://codesandbox.io/s/long-worker-k350q?file=/src/App.tsx
Form
const personAtom = atom<Person>({
firstName: "",
lastName: ""
});
function Form() {
const [formState, setFormState] = useAtom(personAtom);
const handleSubmit: FormEventHandler<HTMLFormElement> = useCallback(
(event) => {
event.preventDefault();
const formElement = event.currentTarget;
const formData = new FormData(formElement);
const firstName = formData.get("firstName") as string;
const lastName = formData.get("lastName") as string;
setFormState({ firstName, lastName }); // This does not update the `data`/`isLoading`/`isError` in the <Output /> component
},
[setFormState]
);
return (
<form id="name-form" onSubmit={handleSubmit}>
<input name="firstName" /> <br />
<input name="lastName" /> <br />
<button>Submit</button>
</form>
);
}
Output
function Output() {
const [person] = useAtom(personAtom);
const { mutate, data, isLoading, isError } = useMutation(sendPersonApi, {
mutationKey: "sendPerson"
});
useEffect(() => {
mutate(person);
}, [person]);
return (
<output name="name-output" form="name-form">
<p>data: {JSON.stringify(data)}</p>
<p>isLoading: {JSON.stringify(isLoading)}</p>
<p>isError: {JSON.stringify(isError)}</p>
</output>
);
}
Initially, I had wanted to implement this by doing the mutation itself inside the <Form /> submit handler, but it seems like I was not be able to access the data/isLoading/isError in a different component even if I used the same mutationKey in useMutation.
So I ended up changing the implementation so that the submit handler just updates some global state (using jotai atoms), and then in the Output I have an effect that listens for changes to this global state and then calls mutate().
The problem now though is that the data always gets reset to undefined in every subsequent submit before getting a new value. Is there a way to stop useMutation from setting data to undefined before updating it to a new value?
Also, is there a cleaner way of using the result of a mutation in another component? I'd prefer to have the mutate() done inside the Form and then somehow use the data/isLoading/isError in a different component without having to rely on useEffect.
I think I'm going to just create a separate atom to hold the global state for the current data. Then I'll use the onSuccess of the useMutation to update this global state from within <Form /> and then just use that global state inside <Output />.
I posted an answer below, but if someone can explain why this is necessary you'll get the bounty, I went through a redux tutorial and feel like I didn't learn about mapDispatchToProps, only mapStateToProps. If you can explain at a deeper level what exactly mapStateToProps and mapDispatchToProps are doing and how they are different I'll give you the bounty.
Minimum Reproducible Example
I have a mapStateToProps function that looks like
const mapStateToProps = state => {
return {
firstName: state.firstName,
middleName: state.middleName,
lastName: state.lastName,
}
}
const ReduxTabForm = connect(mapStateToProps)(MyTab)
In my MyTab component I have a button that is supposed to be inactive if these 2 field do not have anything entered in, but the state of whether or not the button is disabled does not change
function App() {
const {firstName, lastName} = store.getState().formData
const isDisabled = () => {
const {firstName, lastName} = store.getState().form
const requiredFields = [firstName, lastName]
alert(requiredFields)
for(let i = 0; i < requiredFields.length; i=i+1){
if (!requiredFields[i]){
return true
}
}
return false
}
return (
<div className="App">
<div className='bg-light rounded'>
<div className='px-sm-5 pt-0 px-4 flexCenterCol mx-5'>
<div>
<input
type='text'
className="form-control"
value={store.getState().formData['firstName']}
placeholder="First Name"
onChange={(e) => {
store.dispatch(setFormData({'firstName': e.target.value}))
}}
></input>
<input
type='text'
className="form-control"
value={store.getState().formData['lastName']}
placeholder="Last Name"
onChange={(e) => {
store.dispatch(setFormData({'lastName': e.target.value}))
}}
></input>
</div>
<button
type="submit"
disabled={isDisabled()}
>
Button
</button>
</div>
</div>
</div>
)
}
That alert statement executes on page refresh, but does not execute any time after that when I enter data in. I have checked that the redux state updating and it is. The button will not update though, and the isDisabled function will not run more than once
I looked at your reducer code and it looks like this:
...
const reducer = combineReducers({
formData: formReducer,
})
export default reducer
Which means your redux state structure is like this:
state = {
formData: {
firstName: <value>,
middleName: <value>,
lastName: <value>,
}
}
Solution
So, to make your component re-render when the redux state is changed, you need to subscribe to the correct state variables in your mapStateToProps function. Change it to this and will work:
const mapStateToProps = state => {
return {
firstName: state.formData.firstName, // Note that I added "formData"
middleName: state.formData.middleName,
lastName: state.formData.lastName
}
}
Couple of side notes:
It's a better to use the props instead of directly accessing the redux store.
For debugging, console.log is preferred over alert. React DevTools and Redux DevTools are even better.
I don`t know if this will solve your problems, but maybe it is one of the things below:
1 - As Mohammad Faisal said the correct form of calling props should be
const { firstName, lastName } = props;
2 - Instead of reduxTabForm, maybe you could use this instead:
export default connect(mapStateToProps)(MyTab);
3 - And finally, maybe it is an error in the "isDisabled":
for(let i = 0; i < requiredFields.length; i=i+1){
if (!requiredFields){
return false
}
}
If you look carefully, you can see that you are not checking if there is an error inside requeiredFields, your are looking if that doesnt exist if (!requiredFields), maybe changing the condition to if(!requiredFields[i]) so it check each variable and not if the array doesn`t exists.
Edit: the return condition is correct? Returning False when something doesn`t exists?
const ReduxTabForm = connect(mapStateToProps,null)(MyTab)
Try this code snippet, as you are not passing dispatch function to your component. It is better to pass null value.
mapdispatchtoProps is the same basic theory of mapStateToProps.You are storing the function inside the store(which usually in actions) and when rendering the component you attach those function in store to your props. After rendering the component you will be able to run the functions which are in store.
your state values are passed to your component as props. so if you want to access them inside component you should do something like this
const {firstName, lastName} = props
What you could do is something like:
<button
type="submit"
disabled={!fistname && !lastname}
/>
this way if any of your fields be falsy, button is disabled. (empty string is falsy)
The React redux documentation also explains mapDispatchToProps
If you call your action called setFormData from your redux/actions.js then you will be mapping the action setFormData (which is an object) to your component. The action setFromData replace mapDispatchToProps. This is what the documentation says about mapping actions to your components
If itโs an object full of action creators, each action creator will be
turned into a prop function that automatically dispatches its action
when called.
To fix your problem, change your connect function call to this.
const ReduxApp = connect(
setFormData,
mapStateToProps
)(App)
Your issues is with this code isDisabled()
const isDisabled = () => {
const {firstName, lastName} = store.getState().form
const requiredFields = [firstName, lastName]
alert(requiredFields)
for(let i = 0; i < requiredFields.length; i=i+1){
if (!requiredFields){
return false
}
}
return true
}
You are trying to test in loop !requiredFields, an array you created which always return false, even if it doesn't have values. It's a reference type. What you can do now is
const isDisabled = () => {
const {firstName, lastName} = store.getState().form
const requiredFields = [firstName, lastName]
alert(requiredFields)
for(let i = 0; i < requiredFields.length; i=i+1){
if (!requiredFields[i]){
return false
}
}
return true
}
Your loop will check firstNAme and LastName values if they are undefined or not and test should respond with this.
The problem is in the mapStateToProps method. Your code is setting firstNAme and lastName as formData within state object and you are trying to access it from state object directly.
This codesandbox has your project updated with the fix. I didn't fix any other thing in your code so everything is as it is only mapStateToProps should be:
const mapStateToProps = (state) => {
return {
firstName: state.formData["firstName"],
middleName: state.middleName,
lastName: state.formData["lastName"]
};
};
and this will fix your issue. mapStateToPropsis kind of a projection function which will project entire store to some object which will only have properties based on requirements of your component.
Redux re-render components only when store state changes. Check if you are only updating store's state property not whole state because redux compare state by comparing their references, so if you are doing something like this inside your reducer:
State.firstName = action.payload.firstName;
return State;
Then change this to this:
return {...State, firstName: action.payload.firstName}
Note: If you unable to grasp that then kindly provide your reducer code too so that I can see how you are updating your store state.
Looking at your MRE on GitHub, the first thing I want to say is that your button will only have one state and it is the one that the isDisabled() method returns when you refresh the page. This is because the App component it's not getting refresh every time you write on the input fields, so you will never be able to make it change.
you need to subscribe to the correct state variables in your mapStateToProps function. Like this:
const mapStateToProps = state => {
return {
firstName: state.formData.firstName,
middleName: state.formData.middleName,
lastName: state.formData.lastName
}
}
So now that you have this right, you have to introduce this props into your component, like this:
function App({firstName, lastName}) { ...
Another thing to add, is that your are not initializing the states when you create your reducer in reducer.js. If you don't do this, your initial states for firstName and lastName will be null. Here is how you should do it:
import {combineReducers} from 'redux'
import {SET_FORM_DATA} from './actions'
const initialState = {
firstName: "",
lastName: ""
}
const formReducer = (state = initialState, action) => {
if (action.type === SET_FORM_DATA){
return {
...state,
...action.payload
}
}else{
return state
}
}
const reducer = combineReducers({
formData: formReducer,
})
export default reducer
Finally you have to update App:
function App({firstName, lastName}) {
return (
<div className="App">
<div className='bg-light rounded'>
<div className='px-sm-5 pt-0 px-4 flexCenterCol mx-5'>
<div>
<input
type='text'
className="form-control"
placeholder="First Name"
onChange={(e) => {
store.dispatch(setFormData({'firstName': e.target.value}));
console.log(firstName === "h");
}}
></input>
<input
type='text'
className="form-control"
placeholder="Last Name"
onChange={(e) => {
store.dispatch(setFormData({'lastName': e.target.value}))
alert(JSON.stringify(store.getState()))
}}
></input>
</div>
<button
type="submit"
disabled={firstName == "" || lastName == ""}
>
Button
</button>
</div>
</div>
</div>
);
}
So in this way you will be able to update the states from your store and have the dynamic behavior that you were looking for.
In isDisabled function you read data from form: const {firstName, lastName} = store.getState().form but I guess they are saved to formData.
Changing const {firstName, lastName} = store.getState().form to const {firstName, lastName} = store.getState().formData should help.
I am currently building a multi-step form during a user onboarding process, which is why I need to centralize all form data in a parent React component state.
I need to update initialValues with user information but this is an async process.
I thought of creating a useEffect hook calling setState, but maybe there is a more elegant way of doing so...
Having initialValues as one of useEffect dependencies seems to create an infinite loop (Maximum update depth exceeded). This is why the working solution I found was to duplicate all initialValues within... ๐
So how could I update only specific values from initialValues after getting async user information?
Here is a simplified version of the implementation:
import React, { useState, useEffect } from 'react'
// Auth0 hook for authentication (via React Context).
import { useAuth0 } from '../../contexts/auth/auth'
import { Formik, Form, Field } from 'formik'
export default () => {
const { user } = useAuth0()
const initialValues = {
profile: {
name: '',
address: '',
// Other properties...
},
personalInfo: {
gender: '',
birthday: '',
// Other properties...
},
}
const [formData, setFormData] = useState(initialValues)
const [step, setStep] = useState(1)
const nextStep = () => setStep((prev) => prev + 1)
useEffect(() => {
const updateInitialValues = (user) => {
if (user) {
const { name = '', gender = '' } = user
const updatedInitialValues = {
profile: {
name: name,
// All other properties duplicated?
},
personalInfo: {
gender: gender,
// All other properties duplicated?
},
}
setFormData(updatedInitialValues)
}
}
updateInitialValues(user)
}, [user, setFormData])
switch (step) {
case 1:
return (
<Formik
enableReinitialize={true}
initialValues={formData}
onSubmit={(values) => {
setFormData(values)
nextStep()
}}
>
<Form>
<Field name="profile.name" type="text" />
<Field name="profile.address" type="text" />
{/* Other fields */}
<button type="submit">Submit</button>
</Form>
</Formik>
)
case 2:
return (
<Formik
enableReinitialize={true}
initialValues={formData}
onSubmit={(values) => {
setFormData(values)
nextStep()
}}
>
<Form>
<Field name="personalInfo.gender" type="text" />
<Field name="personalInfo.birthday" type="text" />
{/* Other fields */}
<button type="submit">Submit</button>
</Form>
</Formik>
)
// Other cases...
default:
return <div>...</div>
}
}
it's probably late for me to see this question and I just happen to work on a similar project recently.
For my use case, I'm using only one Formik, and using theory similar to Formik Multistep form Wizard: https://github.com/formium/formik/blob/master/examples/MultistepWizard.js for my multistep forms.
And on each step, I need to fetch API to prefill data, I also use useEffect but since I just call the API onetime when I load the specific step, I force it to behave the same as ComponentDidMount(), which is to leave the [] empty with the comment // eslint-disable-next-line so it won't give me warning.
And I use setFieldValue in the useEffect after data is successfully loaded. I feel mine is also not a good way to handle this situation, and I just found something that might be useful: https://github.com/talor-hammond/formik-react-hooks-multi-step-form, it has a Dynamic initialValues. (Though it's typescript)
I am going to refer to this and also try to use for each of my steps, and probably use Context or Wrap them in a parent and store data in the parent Formik.
And getting infinite loop for you might because setFormData should not be in the dependency, since when you setState, the component re-render, the useEffect calls again.
Not sure if this can help you or you already find out how to implement it, I'll look into this deeper.
In the following code, the hook shown below does not change the state of the variable newName.
import React, { useState } from 'react'
const App = () => {
const [ persons, setPersons] = useState([
{ name: 'Arto Hellas' }
])
const [ newName, setNewName ] = useState('')
const textChangeHandler = (event)=>{
event.preventDefault()
setNewName(event.target.value) // WORKS FINE
}
const submitHandler = (event)=>{
event.preventDefault()
let temp = {name:newName}
setNewName('') //////////////////////////////////////// PROBLEM - doesnot set state!!
console.log('tenp name is',temp.name)
console.log('new name is',newName)
setInterval(()=>console.log("Set Interval",newName), 1000)
}
return (
<div>
<h2>Phonebook</h2>
<form onSubmit={submitHandler}>
<div>
name: <input onChange={textChangeHandler} />
</div>
<div>
<button type="submit" >add</button>
</div>
</form>
<h2>Numbers</h2>
{persons.map((person) => <Person key = {person.name} name={person.name}/> )}
</div>
)
}
const Person = ({name})=> <p> {name} </p>
setNewName works fine and the name is updated when anything is typed in the input box. However, when I submit button, the setNewName does not seem to work. Even after updating, executing the setNewName the name is still the old name.
I have even tried a setInterval (thinking it may be due to asynchronous nature of JS) and printed the newName but, it still shows the old name.
What is the problem and how can it be fixed?
thanks
If you look at your submitHandler function, you will notice that the name its self has not changed... it remains empty as in its initial state. React only reload if the state value changes as shown below
const submitHandler = (event)=>{
event.preventDefault()
let temp = {name:newName}
//setNewName('') // your value is not changing here ... removing line
setNewName(temp.name) // changing the name here with the one in temp
console.log('tenp name is',temp.name)
console.log('new name is',newName)
setInterval(()=>console.log("Set Interval",newName), 1000)
}
As you know setting a state is asynchronous. Never use scheduling functions like setTimeout setInterval to log out the value of state.
Use useEffect instead -
useEffect(() => {
console.log("value is", newName);
}, [newName]);
You hook is working fine without a problem. Here's a codeSandbox showing everything's working fine.
In React setState is not synchronous ( with class components and hooks ).
So you can not expect that a new state will be available immediately.
You need to change the implementation of the submitHandler function.