Handling TextField change in Fluent UI React - reactjs

Using Fluent UI React, I'm displaying some data from an AppSync API in a TextField. I want to be able to show text from the API for a contact form. I then want to edit that text and click a button to post it back to the AppSync API.
If I use the TextField component on its own, I can then use a hook to set a variable to result of an AppSync API call and then have the TextField component read the value coming from the variable I set with the hook. I can then edit that text as I feel like and its fine.
The problem I have is that if I want to take edits to the TextField and set them using my hook I lose focus on the TextField. To do this I am using the onChange property of TextField. I can set the variable fine but I have to keep clicking back in to the input window.
Any thoughts on how I can keep the focus?
import React, { useEffect, useState } from 'react';
import { API, graphqlOperation } from 'aws-amplify';
import * as queries from '../../graphql/queries';
import { Fabric, TextField, Stack } from '#fluentui/react';
const PhoneEntryFromRouter = ({
match: {
params: { phoneBookId },
},
}) => PhoneEntry(phoneBookId);
function PhoneEntry(phoneBookId) {
const [item, setItem] = useState({});
useEffect(() => {
async function fetchData() {
try {
const response = await API.graphql(
graphqlOperation(queries.getPhoneBookEntry, { id: phoneBookId })
);
setItem(response.data.getPhoneBookEntry);
} catch (err) {
console.log(
'Unfortuantely there was an error in getting the data: ' +
JSON.stringify(err)
);
console.log(err);
}
}
fetchData();
}, [phoneBookId]);
const handleChange = (e, value) => {
setItem({ ...item, surname: value });
};
const ContactCard = () => {
return (
<Fabric>
<Stack>
<Stack>
<TextField
label='name'
required
value={item.surname}
onChange={handleChange}
/>
</Stack>
</Stack>
</Fabric>
);
};
if (!item) {
return <div>Sorry, but that log was not found</div>;
}
return (
<div>
<ContactCard />
</div>
);
}
export default PhoneEntryFromRouter;
EDIT
I have changed the handleChange function to make use of prevItem. For this event it does accept the event and a value. If you log that value out it is the current value and seems valid.
Despite the change I am still seeing the loss of focus meaning I can only make a one key stroke edit each time.
setItem((prevItem) => {
return { ...prevItem, surname: e.target.value };
});
};```

I think you want the event.target's value:
const handleChange = e => {
setItem(prevItem => { ...prevItem, surname: e.target.value });
};
You should also notice that in your version of handleChange(), value is undefined (only the event e is being passed as a parameter).
Edit: Now I see that you're setting the value item with data from a fetch response on component mount. Still, the value of item.surname is initially undefined, so I would consider adding a conditional in the value of the <TextField /> component:
value={item.surname || ''}

Related

Checkbox is not working properly with React state

I am building a form where the hotel owners will add a hotel and select a few amenities of the same hotel. The problem is If I use state in the onChange function the checkbox tick is not displayed. I don't know where I made a mistake?
import React from "react";
import { nanoid } from "nanoid";
const ListAmenities = ({
amenities,
className,
setHotelAmenities,
hotelAmenities,
}) => {
const handleChange = (e) => {
const inputValue = e.target.dataset.amenitieName;
if (hotelAmenities.includes(inputValue) === true) {
const updatedAmenities = hotelAmenities.filter(
(amenitie) => amenitie !== inputValue
);
setHotelAmenities(updatedAmenities);
} else {
//If I remove this second setState then everything works perfectly.
setHotelAmenities((prevAmenities) => {
return [...prevAmenities, inputValue];
});
}
};
return amenities.map((item) => {
return (
<div className={className} key={nanoid()}>
<input
onChange={handleChange}
className="mr-2"
type="checkbox"
name={item}
id={item}
data-amenitie-name={item}
/>
<label htmlFor={item}>{item}</label>
</div>
);
});
};
export default ListAmenities;
The problem is that you are using key={nanoid()}. Instead, using key={item] should solve your probem.
I believe your application that uses ListAmenities is something like this:
const App = () => {
const [hotelAmenities, setHotelAmenities] = useState([]);
return (
<ListAmenities
amenities={["A", "B", "C"]}
className="test"
setHotelAmenities={setHotelAmenities}
hotelAmenities={hotelAmenities}
/>
);
};
In your current implementation, when handleChange calls setHotelAmenities it changed hotelAmenities which is a prop of ListAmenities and causes the ListAmenities to rerender. Since you use key={nanoid()} react assumes that a new item has been added and the old one has been removed. So it re-renders the checkbox. Since there is no default value of checkbox, it is assumed that it is in unchecked state when it is re-rendered.

React Hook Form and persistent data on page reload

I'm using React Hook Form v7 and I'm trying to make my data form persistent on page reload. I read the official RHF documentation which suggests to use little state machine and I tried to implement it but without success. Is there a better way to do it? However...
The first problem I encountered using it, is that my data is a complex object so the updateAction it should be not that easy.
The second problem is that I don't know when and how to trigger the updateAction to save the data. Should I trigger it on input blur? On input change?
Here's my test code:
If persisting in the localStorage works you, here is how I achieved it.
Define a custom hook to for persisting the data
export const usePersistForm = ({
value,
localStorageKey,
}) => {
useEffect(() => {
localStorage.setItem(localStorageKey, JSON.stringify(value));
}, [value, localStorageKey]);
return;
};
Just use it in the form component
const FORM_DATA_KEY = "app_form_local_data";
export const AppForm = ({
initialValues,
handleFormSubmit,
}) => {
// useCallback may not be needed, you can use a function
// This was to improve performance since i was using modals
const getSavedData = useCallback(() => {
let data = localStorage.getItem(FORM_DATA_KEY);
if (data) {
// Parse it to a javaScript object
try {
data = JSON.parse(data);
} catch (err) {
console.log(err);
}
return data;
}
return initialValues;
}, [initialValues]);
const {
handleSubmit,
register,
getValues,
formState: { errors },
} = useForm({ defaultValues: getSavedData() });
const onSubmit: SubmitHandler = (data) => {
// Clear from localStorage on submit
// if this doesn’t work for you, you can use setTimeout
// Better still you can clear on successful submission
localStorage.removeItem(FORM_DATA_KEY);
handleFormSubmit(data);
};
// getValues periodically retrieves the form data
usePersistForm({ value: getValues(), localStorageKey: FORM_DATA_KEY });
return (
<form onSubmit={handleSubmit(onSubmit)}>
...
</form>
)
}
I already faced this issue and implemented it by creating a custom Hook called useLocalStorage. But since you are using the React hook form, it makes the code a bit complicated and not much clean!
I suggest you simply use the light package react-hook-form-persist.
The only work you need to do is to add useFormPersist hook after useForm hook. Done!
import { useForm } from "react-hook-form";
import useFormPersist from "react-hook-form-persist";
const yourComponent = () => {
const {
register,
control,
watch,
setValue,
handleSubmit,
reset
} = useForm({
defaultValues: initialValues
});
useFormPersist("form-name", { watch, setValue });
return (
<TextField
title='title'
type="text"
label='label'
{...register("input-field-name")}
/>
...
);
}
The state itself won't persist any data on page reload.
You need to add your state data to Local Storage.
Then load it back into the state on componentDidMount (useEffect with empty dependency array).
const Form = () => {
const [formData, setFormData] = useState({})
useEffect(() => {
if(localStorage) {
const formDataFromLocalStorage = localStorage.getItem('formData');
if(formDataFromLocalStorage) {
const formDataCopy = JSON.parse(formDataFromLocalStorage)
setFormData({...formDataCopy})
}
}
}, []);
useEffect(() => {
localStorage && localStorage.setItem("formData", JSON.stringify(formData))
}, [formData]);
const handleInputsChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
})
}
return (
<div>
<input
type="text"
name="firstName"
placeholder='first name'
onChange={e => handleInputsChange(e)}
value={formData?.firstName}
/>
<input
type="text"
name="lastName"
placeholder='last name'
onChange={e => handleInputsChange(e)}
value={formData?.lastName}
/>
</div>
)
}

Cannot get Typeahead value

I'm trying to create a tag input typeahead from this library:
import { Typeahead } from 'react-bootstrap-typeahead';
in my reactjs app:
<Typeahead
allowNew
id="custom-selections-example"
multiple
newSelectionPrefix="Add a new item: "
options={opt}
placeholder="Autocomplete"
name="tags"
onChange={onChange}
value={values.options}
/>
the statement console.log(values.options)-> does not return anything when I select one of the options...
Can someone show me a way to get the value?
UPDATE
I have previously tested with onChange function and gives the following error:
TypeError: Cannot read property 'name' of undefined
OnChange Method:
const { values, onChange, onSubmit } = useForm(createPostCallback, {
tags:''
})
const [createData, { error }] = useMutation(CREATE_QUERY, {
variables: values,...code continues
Here is the working code . https://codesandbox.io/s/wonderful-sanderson-eg0g8?file=/src/index.js:0-648. Here I modified the code based on your requirement, now you can call your parent onChange method from the handleChange method.
import React, { useEffect, useRef, useState } from 'react';
import { Typeahead } from 'react-bootstrap-typeahead';
import ReactDOM from 'react-dom';
import options from './data';
import 'react-bootstrap-typeahead/css/Typeahead.css';
import './styles.css';
const TypeaheadExample = () => {
const [selected, setSelected] = useState([]);
const refHidden = useRef(null);
const onChange = (name) => (values) => {
if (values.length > 0) {
const e = new Event('input', { bubbles: true });
setNativeValue(refHidden.current, values[0].capital);
refHidden.current.dispatchEvent(e);
}
};
function setNativeValue(element, value) {
const valueSetter = Object.getOwnPropertyDescriptor(element, 'value').set;
const prototype = Object.getPrototypeOf(element);
const prototypeValueSetter = Object.getOwnPropertyDescriptor(
prototype,
'value'
).set;
if (valueSetter && valueSetter !== prototypeValueSetter) {
prototypeValueSetter.call(element, value);
} else {
valueSetter.call(element, value);
}
}
function handleChange(e) {
console.log(e.target.name);
console.log(e.target.value);
}
//{(text, e) => { console.log(text, e); }}
return (
<>
<Typeahead
id="basic-example"
onChange={onChange('basic')}
options={options}
placeholder="Choose a state..."
selected={selected}
/>
<input
ref={refHidden}
style={{ display: 'none' }}
name="control_Name"
onChange={handleChange}
/>
</>
);
};
ReactDOM.render(<TypeaheadExample />, document.getElementById('root'));
Unless I misunderstand what you are doing or there is missing code from your example, you want to set the values selected from that component correct?
Looking through the library's examples, it would seem like you don't have the onChange prop, where ideally you will set that value. So try this:
<Typeahead
allowNew
id="custom-selections-example"
multiple
newSelectionPrefix="Add a new item: "
options={opt}
placeholder="Autocomplete"
name="tags"
onChange={(value) => console.log('SET VALUE HERE!', value)}
value={values.options}
/>
Edit: take note for form related components in general, chances are you want to do anything relating to setting and storing values within the events (so onChange and possible variations of that).
From the documentation:
<Typeahead
allowNew
id="id"
multiple
newSelectionPrefix="Add a new item: "
options={opts}
placeholder="Autocomplete tags"
onChange={(selected) => { values.tags = selected }} />
The typeahead behaves similarly to other form elements. It requires an array of data options to be filtered and displayed.
<Typeahead
onChange={(selected) => {
// Handle selections...
}}
options={[ /* Array of objects or strings */ ]}
/>

UseState hook not updating the step properly

I want to change state value with change in the 'select' element. So I am calling the setFilter method in the onChange handler. But state is not getting updated. It's holding the previous value.
How to fix this issue?
I want to change state value with change in the 'select' element. So I am calling the setFilter method in the onChange handler. But state is not getting updated. It's holding the previous value.
How to fix this issue?
import React, { useState, useEffect } from 'react'
import { useHistory } from 'react-router-dom'
import { useApolloClient} from 'react-apollo';
import { Formik, Form, ErrorMessage } from 'formik';
import * as Yup from "yup";
import AsyncPaginate from 'react-select-async-paginate'
import { GET_ITEM_CODES } from '../../library/Query';
export default function SampleForm({initialData}){
const history = useHistory();
const [productFilter, setProductFilter] = useState('');
const client = useApolloClient();
const defaultAdditional = {
cursor : null
}
const shouldLoadMore = (scrollHeight, clientHeight, scrollTop) => {
const bottomBorder = (scrollHeight - clientHeight) / 2
return bottomBorder < scrollTop
}
const loadItemcodeOptions = async (q = 0, prevOptions, {cursor}) => {
console.log('qu',q*1)
const options = [];
console.log('load')
const response = await client.query({
query:GET_ITEM_CODES,
variables : {filter: {
number_gte : q*1
},skip:0, first:4, after: cursor}
})
console.log('res',response)
response.data.itemCodes.itemCodes.map(item => {
return options.push({
value: item.number,
label: `${item.number} ${item.description}`
})
})
console.log('0',options)
return {
options,
hasMore: response.data.itemCodes.hasMore,
additional: {
cursor: response.data.itemCodes.cursor.toString()
}
}
}
const handleFilter = (e) => {
console.log('e',e)
setProductFilter(e.value)
console.log('pf',productFilter) // output is previous State(wrong)
}
useEffect(() => {
console.log('epf',productFilter) // output the current state(expected)
})
return(
<Formik
initialValues = {{
itemCode: !!initialData ? {value: initialData.itemCode, label: initialData.itemCode} : '',
}}
validationSchema = {Yup.object().shape({
itemCode: Yup.number().required('Required'),
})}
>
{({values, isSubmitting, setFieldValue, touched, errors }) => (
<Form>
<label htmlFor="itemCode">Item Code</label>
<AsyncPaginate
name="itemCode"
defaultOptions
debounceTimeout={300}
cacheOptions
additional={defaultAdditional}
value={values.itemCode}
loadOptions={loadItemcodeOptions}
onChange={option => {
handleFilter(option)
setFieldValue('itemCode', option)
}}
shouldLoadMore={shouldLoadMore}
/>
<ErrorMessage name="itemcode"/>
<pre>{JSON.stringify(values, null, 2)}</pre>
</Form>
)}
</Formik>
)
}
Actually setProductFilter is setting state asynchronously, so you'll get updated state in the effect, not right after calling setState. But your effect is going to run every time when your component gets re-rendered so you should add productFilter as a dependency of useEffect.
One other thing I want to mention is, I don't know about your use case but you should stick to the rule: Single source of truth. You have two states for productFilter, one is in Formik, i.e. itemCode, and other in your local state. I think you can remove your local state and use item code from formikProps.values.itemCode.

React native, cannot update during an existing state transition

I have a react native component. I got the error:
Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
Code:
import....
class Register extends Component {
static navigationOptions = {
header: null,
};
async handleSubmit(values, customerCreate) {
const { email, password, firstName, lastName, phone } = values;
const input = { email, password, firstName, lastName, phone };
const customerCreateRes = await customerCreate({ variables: { input } });
const isCustomerCreated = !!customerCreateRes.data.customerCreate.customer.id;
if (isCustomerCreated) {
const isStoredCrediential = await storeCredential(email, password);
if (isStoredCrediential === true) {
// Store in redux
// Go to another screen
console.log('test');
}
}
}
render() {
return (
<Mutation mutation={CREATE_CUSTOMER_ACCOUNT}>
{
(customerCreate, { error, data }) => {
return (
<MainLayout
title="Create Account"
backButton
currentTab="profile"
navigation={this.props.navigation}
>
{ showError }
{ showSuccess }
<RegistrationForm
onSubmit={async (values) => this.handleSubmit(values, customerCreate)}
initialValues={this.props.initialValues}
/>
</MainLayout>
);
}
}
</Mutation>
);
}
}
const mapStateToProps = (state) => {
return {
....
};
};
export default connect(mapStateToProps)(Register);
CREATE_CUSTOMER_ACCOUNT is graphql:
import gql from 'graphql-tag';
export const CREATE_CUSTOMER_ACCOUNT = gql`
mutation customerCreate($input: CustomerCreateInput!) {
customerCreate(input: $input) {
userErrors {
field
message
}
customer {
id
}
}
}
`;
More detail here
Who is using the handleSubmit?
There is a button in the form call the handleSubmit, when press.
is this syntax correct onPress={handleSubmit} ?
const PrimaryButton = ({ label, handleSubmit, disabled }) => {
let buttonStyle = styles.button;
if (!disabled) {
buttonStyle = { ...buttonStyle, ...styles.primaryButton };
}
return (
<Button block primary={!disabled} disabled={disabled} onPress={handleSubmit} style={buttonStyle}>
<Text style={styles.buttonText}>{label}</Text>
</Button>
);
};
export default PrimaryButton;
Update 1:
If I remove customerCreate (coming from graphql), the error disappears. It means the async await is actually correct, but I need the customerCreate
Did you check with following code ?
onSubmit={(values) => this.handleSubmit(values, customerCreate)}
If you are trying to add arguments to a handler in recompose, make sure that you're defining your arguments correctly in the handler.
Also can be you're accidentally calling the onSubmit method in your render method, you probably want to double check how your onSubmit in RegistrationForm component.
Also you might want to try one more thing, moving async handleSubmit(values, customerCreate) { to handleSubmit = async(values, customerCreate) =>;
If this doesn't work, please add up your RegistrationForm component as well.
Bottom line, unless your aren't setting state in render, this will not happen.
It turns out the async await syntax is correct. The full original code (not posted here) contains Toast component react-base. The other developer is able to tell me to remove it and the error is gone. Sometimes it is hard to debug.

Resources