How to set Formik custom component value to Formik value - javascript

I'm using Formik for my form with google place auto-complete, I want to render places auto-complete as a custom component in the Formik field.
form.js
<Formik initialValues={location:""}>
<Field name="location" component={PlacesAutoComplete} placeholder="enter your location"/>
{...rest of form}
</Formik>
auto-complete component
import PlacesAutocomplete , {
geocodeByAddress,
geocodeByPlaceId
} from "react-google-places-autocomplete";
export const PlacesAutoComplete = ({
field: { name, ...field }, // { name, value, onChange, onBlur }
form: { touched, errors }, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc.
classes,
label,
...props
}: any) => {
const [fieldName, setFildName] = React.useState(field.name);
const [address, setAddress] = React.useState(props.value || "");
const error = errors[name];
// const touch = touched[name];
const handleSelect = () => {
// set this value to formik value
};
const handleChange = () => {
// set this value to formik value
};
const handleError = () => {
props.form.setFieldError(fieldName, error);
};
return (
<PlacesAutocomplete
value={address}
onChange={handleChange}
onSelect={handleSelect}
onError={handleError}
name={name}
placeholder={props.placeholder}
id={name}
{...props}
apiKey="Api key here"
>
{({
getInputProps,
suggestions,
getSuggestionItemProps,
loading
}: any) => (
<div>
<input
{...getInputProps({
placeholder: "Search Places ...",
className: "location-search-input form-control"
})}
/>
<div className="autocomplete-dropdown-container">
{loading && <div>Loading...</div>}
{suggestions.map((suggestion: any) => {
const className = suggestion.active
? "suggestion-item--active"
: "suggestion-item";
// inline style for demonstration purpose
const style = suggestion.active
? { backgroundColor: "#fafafa", cursor: "pointer" }
: { backgroundColor: "#ffffff", cursor: "pointer" };
return (
<div
{...getSuggestionItemProps(suggestion, {
className,
style
})}
>
<span>{suggestion.description}</span>
</div>
);
})}
</div>
</div>
)}
</PlacesAutocomplete>
);
};
How I set places auto-complete value to formik value, I'm pretty new to react and confused in handle change and on change functions. also, I found a solution in react class component here, But when converting those codes into functional components I'm stuck in Onchange and onSlecet functions

Better not write functional components as you'll get stuck with the test cases if you are writing.
OnChange is even you type anything, the value gets stored in onChange.
Abe onSelect is when you select anything

Basically on change you need to call formik's field onChange function. So in case you get an event on handleChange, just do this
const handleChange = (event) => {
// set this value to formik value
field.onChange(event.target.value)
};
or in case you get value in handleChange then do this
const handleChange = (value) => {
// set this value to formik value
field.onChange(value)
};
This will sync your formik state with autocomplete state.
Now comes the part for select. In this case also you can take the same route
const handleSelect = (value) => {
// set this value to formik value
field.onChange(value)
};
or you can use the setField function of form to update the value
const handleSelect = (value) => {
// set this value to formik value
form.setField('location',value)
};

Related

MUI + React Hook Form: Fill out TextField value but then can't modify the value

I'm using the combination of MUI + React Hook Form, so I've created a CustomTextField.tsx component to make it worked.
// CustomTextField.tsx
import { TextField } from "#mui/material";
export default function CustomTextField(props: any) {
return (
<Controller
name={props.name}
control={props.control}
render={({
field: { onChange, value },
fieldState: { error },
formState
}) => <TextField onChange={onChange} {...props} />}
/>
);
}
Then at the app/parent level, I want to these steps:
Fetch data and display to the TextField.
Modify the text in TextField
Submit the new value in TextField
This is my approach:
//App.tsx
export default function App() {
const { control, handleSubmit } = useForm();
const [fetchedData, setFetchedData] = useState<string>("");
...
return (
...
<CustomTextField
control={control}
name="description"
label="Description"
type="text"
variant="outlined"
value={fetchedData ? fetchedData: ""} //<-- fetched data is set to value of TextField to display
/>
...
);
}
With this approach, I managed to display the fetchedData on TextField UI, but can not modify that data on text field. Also when I submit, the data is not correct to what display on the TextField
I have created a codesandbox link for this: https://codesandbox.io/s/blissful-sanne-x858dx?file=/src/App.tsx:190-1155
How can I display the fetchedData, but also can modify the data in text field and then submit through React Hook Form later?
What you are trying to is have a text input where the user can type a value, but you can also set the value externally by clicking the "Fetch Data" button.
Your setup includes conflicting sources of truth. The value of the field is set to the data state and the value which is stored in the react-hook-form state is ignored.
What you want to do is to modify the internal state of the react-hook-form when the button is pressed.
You can delete the local state:
const [data, setData] = useState<string>("");
Instead, you can use the setValue helper function from the form:
const { control, handleSubmit, setValue } = useForm<FormValues>();
const fetchingData = () => {
setValue("description", "fetched description Text");
};
In your CustomTextField, make sure that you set the value of the input to the value from the form state.
render={({
field: { onChange, value },
fieldState: { error },
formState
}) => <CusTextField onChange={onChange} value={value} {...props} />}
Complete Code
Inspired by #Linda solution, I've came up with this approach, that ensures the functionality working with the style of MUI TextField as well.
The setValue will be passed down to custom TextField to set the value. And to keep the {...props} functionality, I delete the setValue props before spreading it on MUI TextField.
//CustomTextField.tsx
import { TextField } from "#mui/material"
import { Controller } from "react-hook-form"
export default function CustomTextField(props: any) {
const propsObj = { ...props }
delete propsObj.setValue
return (
<Controller
name={props.name}
control={props.control}
defaultValue={""}
render={({
field: { onChange, value },
fieldState: { error },
formState,
}) => (
<TextField
value={value}
onChange={({ target: { value } }) => {
onChange(value)
if (props?.setValue) props.setValue(props.name, value)
}}
{...propsObj}
/>
)}
/>
)
}
//App.tsx
export default function App(){
const { control, handleSubmit, setValue } = useForm()
return (
<form onSubmit={handleSubmit(...)}>
<CustomTextField
control={control}
name="description"
label="Description"
type="text"
variant="outlined"
setValue={setValue}
/>
<form/>
)
}

React - How to prevent re-rendering of all the input fields when input changes

I am implementing a form which is generated using a Json. The Json is retrieved from API and then looping over the items I render the input elements. Here is the sample Json :
{
name: {
elementType: 'input',
label: 'Name',
elementConfig: {
type: 'text',
placeholder: 'Enter name'
},
value: '',
validation: {
required: true
},
valid: false,
touched: false
}
}
Here is how I render the form :
render() {
const formElementsArray = [];
for (const key in this.props.deviceConfig.sensorForm) {
formElementsArray.push({
id: key,
config: this.props.deviceConfig.sensorForm[key]
});
const itemPerRow = 4;
const rows = [
...Array(Math.ceil(props.formElementsArray.length / itemPerRow))
];
const formElementRows = rows.map((row, idx) =>
props.formElementsArray.slice(
idx * itemPerRow,
idx * itemPerRow + itemPerRow
)
);
const content = formElementRows.map((row, idx) => (
<div className='row' key={idx}>
{row.map((formElement) => (
<div className='col-md-3' key={formElement.id}>
<Input
key={formElement.id}
elementType={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shouldValidate={formElement.config.validation}
touched={formElement.config.touched}
label={formElement.config.label}
handleChange={(event) => props.changed(event, formElement.id)}
/>
</div>
))}
</div>
...
}
I am storing the form state in redux and on every input change , I update the state. Now the problem is everytime I update the state, the entire form is re-rendered again... Is there any way to optimise it in such a way that only the form element which got updated is re-rendered ?
Edit :
I have used React.memo in Input.js as :
export default React.memo(input);
My stateful Component is Pure component.
The Parent is class component.
Edit 2 :
Here is how I create formElementArray :
const formElementsArray = [];
for (const key in this.props.deviceConfig.sensorForm) {
formElementsArray.push({
id: key,
config: this.props.deviceConfig.sensorForm[key]
});
You can make content as a separate component like this.
And remove formElementsArray prop from parent component.
export default function Content() {
const formElementRows = useForElementRows();
formElementRows.map((row, idx) => (
<Input
formId={formElement.id}
handleChange={props.changed}
/>
)
}
Inside Input.js
const handleInputChange = useCallback((event) => {
handleChange(event, formId);
}, [formId, handleChange]);
<input handleChange={handleInputChange} />
export default React.memo(Input)
So you can memoize handleChange effectively. And it will allow us to prevent other <Input /> 's unnecessary renders.
By doing this forElementRows change will not cause any rerender for other components.
You could try a container, as TianYu stated; you are passing a new reference as change handler and that causes not only the component to re create jsx but also causes virtual DOM compare to fail and React will re render all inputs.
You can create a container for Input that is a pure component:
const InputContainer = React.memo(function InputContainer({
id,
elementType,
elementConfig,
value,
invalid,
shouldValidate,
touched,
label,
changed,
}) {
//create handler only on mount or when changed or id changes
const handleChange = React.useCallback(
(event) => changed(event, id),
[changed, id]
);
return (
<Input
elementType={elementType}
elementConfig={elementConfig}
value={value}
invalid={invalid}
shouldValidate={shouldValidate}
touched={touched}
label={label}
handleChange={handleChange}
/>
);
});
Render your InputContainer components:
{row.map((formElement) => (
<div className="col-md-3" key={formElement.id}>
<InputContainer
key={formElement.id}
elementType={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shouldValidate={formElement.config.validation}
touched={formElement.config.touched}
label={formElement.config.label}
//re rendering depends on the parent if it re creates
// changed or not
changed={props.changed}
/>
</div>
))}
You have to follow some steps to stop re-rendering. To do that we have to use useMemo() hook.
First Inside Input.jsx memoize this component like the following.
export default React.memo(Input);
Then inside Content.jsx, memoize the value of elementConfig, shouldValidate, handleChange props. Because values of these props are object type (non-primitive/reference type). That's why every time you are passing these props, they are not equal to the value previously passed to that prop even their value is the same (memory location different).
const elementConfig = useMemo(() => formElement.config.elementConfig, [formElement]);
const shouldValidate = useMemo(() => formElement.config.validation, [formElement]);
const handleChange = useCallback((event) => props.changed(event, formElement.id), [formElement]);
return <..>
<Input
elementConfig={elementConfig }
shouldValidate={elementConfig}
handleChange={handleChange}
/>
<../>
As per my knowledge, this should work. Let me know whether it helps or not. Thanks, brother.

How properly connect react-final-form and material-ui-chip-input?

I'm using material-ui-chip-input wrapped with Field react-final-form.
I wanted to limit "CHIPS" to 5 only.
Chips are compact elements that represent an input, attribute, or
action.
material ui docs
As you can see I'm using 2 states here
react useStates
react-final-form internal states
This is redundant because react-final-form handled states internally but I can't make to work I'm just showing what I have done so far.
Basically there are
two problems with my implementation.
It doesn't limit my chips.
My react-final-form field values - not updating when clicking DeleteChip
import ChipInput from 'material-ui-chip-input'
import { Field } from 'react-final-form'
const [state, setState] = useState([])
const AddChip = useCallback(
(chip) => {
if (state.length + 1 <= 5) {
setState((prev) => ([...prev.tags, chip]))
}
},
[setState, state.length]
)
const DeleteChip = useCallback(
(chip) => {
setState((prev) => (...prev.state.filter((p) => p !== chip)]
}))
},
[setState]
)
return (
<Field name="states" validate={isRequired} >
{({ input: { value, onChange, ...rest }, meta }) => {
<ChipInput
defaultValue={Array.isArray(value) ? value : []} // check value first because material-ui-chip-input require an array, by default react-final-form value is empty string
onChange={(event) => { // uncontrolled
AddChip(event)
onChange(event)
// I tried below code but same result not working
// if (state.length + 1 <= 5) {
// onChange(event)
// }
}}
onDelete={DeleteChip}
/>
}}
</Field>
)
material-ui-chip-input
react-final-form
see codesandbox demo
This is my take:
https://codesandbox.io/s/proud-water-xp2y1?file=/src/App.js
Key points:
Don't duplicate the state, let react final form handle the state for you
Pass an empty array as the initial state to the FORM, don't pass defaultValues to the Field.
according to the material-ui-chip-input package you need to use onAdd if used in controlled mode, which we do, since we let react final form handle the state.
Add the value prop to the Chipinput.
For cosmetic reasons: don't actually use the render-prop inside <Form /> - use a functional child component instead.
Code:
import ChipInput from "material-ui-chip-input";
import { Form, Field } from "react-final-form";
export default function App() {
return (
<Form
initialValues={{
states: []
}}
onSubmit={() => console.log("submitted")}
>
{({ values, onSubmit }) => (
<form onSubmit={onSubmit}>
<Field name="states">
{({ input: { value, onChange } }) => (
<ChipInput
value={value}
alwaysShowPlaceholder={true}
placeholder="type states here"
onAdd={(newVal) => {
if (value.length >= 5) return;
const newArr = [...value, newVal];
onChange(newArr);
}}
onDelete={(deletedVal) => {
const newArr = value.filter((state) => state !== deletedVal);
onChange(newArr);
}}
/>
)}
</Field>
<p>react useStates</p>
<p>react-final-form values</p>
<pre
style={{
backgroundColor: "rgba(0,0,0,0.1)",
padding: "20px"
}}
>
{JSON.stringify(values, 0, 2)}
</pre>
</form>
)}
</Form>
);
}

How to update and display first non-empty value on setState with react?

I'm trying to display the value of my inputs from a from, in a list. Everytime I hit submit, I expect that it should display the inputs in order.
The problem I'm having is that when I try to submit my form and display inputs in a list, it display an empty value first. On the next submit and thereafter, it displays the previous value, not the new one on the input field.
There's also an error message but i'm not understanding how to relate it to the problem. It's a warning message regarding controlled/uncontrolled components.
I've tried to add if statements to check for empty values in each functions but the problem persists.I've tried to manage the error massage by being consistent with all input to be controlled elements using setState, but nothing works.
I looked through todo list examples on github. I guess i'm trying to keep it in one functional component versus multiple ones, and I'm not using class components. I tried to follow the wesbos tutorial on Javascript 30 day challenge, day 15: Local Storage and Event Delegation. I'm trying to use React instead of plain JS.
Here's what my component looks like.
import React, { useEffect, useState } from "react";
import "../styles/LocalStorage.css";
export const LocalStorage = () => {
const [collection, setCollection] = useState([]);
const [value, setValue] = useState();
const [item, setItem] = useState({ plate: "", done: false });
const [display, setDisplay] = useState(false);
//set the value of the input
const handleChange = (e) => {
if (e.target.value === "") return;
setValue(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
if (value === "" || undefined) return;
setItem((prevState) => {
return { ...prevState, plate: value };
});
addItem(item);
setDisplay(true);
setValue("");
};
const addItem = (input) => {
if (input.plate === "") return;
setCollection([...collection, input]);
};
return (
<div>
<div className="wrapper">
<h2>LOCAL TAPAS</h2>
<ul className="plates">
{display ? (
collection.map((item, i) => {
return (
<li key={i}>
<input
type="checkbox"
data-index={i}
id={`item${i}`}
checked={item.done}
onChange={() =>
item.done
? setItem((state) => ({ ...state, done: false }))
: setItem((state) => ({ ...state, done: true }))
}
/>
<label htmlFor={`item${i}`}>{item.plate}</label>
</li>
);
})
) : (
<li>Loading Tapas...</li>
)}
</ul>
<form className="add-items" onSubmit={handleSubmit}>
<input
type="text"
name="item"
placeholder="Item Name"
required
value={value}
onChange={handleChange}
/>
<button type="submit">+ Add Item</button>
</form>
</div>
</div>
);
};
Since the setState function is asynchronous, you cannot use the state value item right after you fire the setItem(...). To ensure you get the latest value for your addItem function:
setItem((prevState) => {
const newItem = { ...prevState, plate: value };
addItem(newItem); // Now, it's getting the updated value!
return newItem;
});
And regarding the controlled and uncontrolled components, you can read the docs about it here. To fix your problem, you can initialize the value state with an empty string:
const [value, setValue] = useState('');

why value is not set on dropdown in react js (allready set value props)?

I am taking help from this link
how to set value in dropdown in react js?
set the value of dropdown.but it is not setting value in dropdown.
I am getting dropdown data after few seconds 3000 and then I need to set value on dropdown
expected output Aland Islands should be selected. { key: "ax", value: "ax", text: "Aland Islands" },
here is my code
https://codesandbox.io/s/sharp-rhodes-140fm
const SingleSelectAutoComplete = props => {
const { onSearchChange, input, label, data, value } = props;
return (
<div>
<label>{label}</label>
<Dropdown
{...props.input}
clearable
fluid
search
closeOnChange
onChange={(e, data) => {
return input.onChange(data.value);
}}
onSearchChange={onSearchChange}
selection
options={data}
value={value}
placeholder="Select Country"
/>
</div>
);
};
const val = "ax";
const [state, setState] = useState([]);
const [value, setValue] = useState([]);
setTimeout(() => {
setState(data);
setValue("ax");
}, 2000);
Since you use final form the value is not passed, if you pass a prop that is named "value" it will disappear, call it anything else and it shows up. In this case i called it helloWorld
Maybe you should study how final-form works as I only post this as "it does something", I don't know why it does it or how you're supposed to use it.
const SingleSelectAutoComplete = props => {
const { onSearchChange, helloWorld, label, data,onChange } = props;
return (
<div>
<label>{label}</label>
<Dropdown
{...props.input}
clearable
fluid
search
closeOnChange
//use the onChange function passed from App
//it will set the App state
onChange={(e, data) => {
onChange(data.value);
}}
onSearchChange={onSearchChange}
selection
options={data}
//pass value as helloWorld or final form will make
//prop disappear if you call the prop value
helloWorld={value}
placeholder="Select Country"
/>
</div>
);
};
in App:
function App() {
const [state, setState] = useState([]);
//removed val and setting val in the timeout
//you just pass the value to the useState function
const [value, setValue] = useState("ax");
setTimeout(() => {
setState(data);
}, 2000);
//log to show that setValue is used when you change input
console.log('value:',value)
return (
<div style={{ width: 600, margin: "0 auto" }}>
<RFFORM
onSubmit={onSubmit}
render={({ handleSubmit, form, submitting, pristine, values }) => (
<SForm onSubmit={handleSubmit}>
<RFField
component={SingleSelectAutoComplete}
label=""
name="ag"
placeholder=""
required={true}
//passing value prop doesn't seem to do anything
//calling it helloWorld and it'll show up
helloWorld={value}
data={state}
//pass setValue as onChange or user will not be able to change it
onChange={setValue}
/>
The whole point of React Final Form is that it manages your form values for you, so you should not be passing a value that you are managing with useState.
If you need to initialize (or reinitialize) your form with some value from outside, you pass it to initialValues. Here's a working example. The value that your select component needs is inside the ...input.

Categories