I have a React Admin Edit form with a field which by default doesn't exist in the record. I could manually type it in, save it and it would work. However I wish to retrieve the value of this field programmatically when a user clicks a button then adjust it.
I use a useMutation hook to access a custom API which performs an expensive operation and returns a result for the field. I only want to perform this operation when the user clicks a button.
So inside a Edit form I have this field called key I want to apply the data from this useMutation hook to it.
export const PostEdit = (props) => (
<Edit title={<PostTitle />} {...props}>
<SimpleForm>
<TextInput disabled source="id" />
<RetrieveKey />
<TextInput source="key" />
</SimpleForm>
</Edit>
);
The RetrieveKey button is like this
const RetrieveKey = ({ record }) => {
const [retrieve, { loading }] = useMutation(
{
type: "retrieveKey",
resource: "posts",
payload: { id: record.id }
},
{
onSuccess: ({ data }) => {
if (data) {
// Set the key field here.
} else {
console.log("No Key");
}
},
onFailure: (error) => console.log("Error");
}
);
return (
<Button
onClick={retrieve}
disabled={loading}
label={"Retrieve Key"}
></Button>
);
};
I have looked through the various Form Hooks but can't find anything documented which would let me accomplish this.
Note, I don't want to instantly call the UpdateMethod or this would be trivial and I could use the DataProvider useUpdate to call it. Instead I want to prefill in the form for the user from the responsed key.
A codesandbox is provided here: https://codesandbox.io/s/nifty-firefly-k13g7?file=/src/App.js
I needed to access the underlying React-Final-Form API in order to edit the form.
The React-Admin Docs briefly touch on it here: https://marmelab.com/react-admin/Inputs.html#linking-two-inputs
So in this case I needed to use the useForm hook which then allowed me to update the form value.
const RetrieveKey = ({ record }) => {
const form = useForm();
const [retrieve, { loading }] = useMutation(
{
type: "retrieveKey",
resource: "posts",
payload: { id: record.id }
},
{
onSuccess: ({ data }) => {
console.log(form);
if (data) {
form.change("key", data);
} else {
console.log("No Key found");
}
},
onFailure: (error) => console.log("Error");
}
);
return (
<Button
onClick={retrieve}
disabled={loading}
label={"Retrieve Key"}
></Button>
);
};
Related
After using the setItem method value of 'name' variable should change to value entered in the textInput but console.log(item) shows 'loading' After clicking submit button at first time. Which is initial value of the 'name' variable in useState.
Once I entered the submit button again the name variable value change to value entered in textInput.
Expected: When I click submit button at first time, item.name variable should change to textInput value
Using same code in class component work fine.
export default function App() {
const arr =[]
const [text, setText] = useState()
const[item,setItem] = useState([
{name: 'loading'}
])
const storedata= async ()=> {
// setItem({name: text})
arr.push({name: text})
try {
await AsyncStorage.setItem('mykey',JSON.stringify(arr));
console.log(await AsyncStorage.getItem('mykey'))
}
catch (error) {
console.log(error)
}
try{
setItem({item: JSON.parse(await AsyncStorage.getItem('mykey'))})
console.log('printing item',item)
}
catch(e){
console.log(e)
}
}
return (
<View style={styles.container}>
<TextInput onChangeText={(text) => setText(text)}
placeholder='type'
/>
<Button title='change'
onPress={storedata}
/>
</View>
);
}
Output:
Button click first time: printing item [{"name":"loading"}]
Button click second time: printing item {"item":[{"name":"hi"}]}
class App extends React.Component{
arr=[]
id=0
state= {
text: '',
item:[
{name: 'loading'}
]
}
storedata = async () => {
this.arr.push({ name: this.state.text})
await AsyncStorage.setItem('mylist',JSON.stringify(this.arr));
this.setState({
item: JSON.parse(await AsyncStorage.getItem('mylist'))
})
console.log(item)
}
The setState function is an asynchronous function. setState calls are batched to improve the performance of the application. So the results are not reflected immediately. That's why, you are getting the previous value for the item variable.
You can find more info here.
I have a form with ~15 fields where each section is a unique child component. I want to know how to pass data between the parent form and child components(using control because this is react native)
Right now, I see the proper value for testResult in onSubmit logs but data is undefined for some reason. This means my parent form is somehow not picking up the value in the child.
Parent Form:
const Stepper = () => {
const form = useForm({ defaultValues: {
testResult: "",
}
});
const { control, handleSubmit, formState: { errors }, } = form;
const testResult = useWatch({ control, name: "testResult" });
const onSubmit = (data) => {
console.log("watched testResult value: ", testResult);
console.log("form submission data: ", data);
};
return (
<WaterStep form={form} />
<Button onSubmit={handleSubmit(onSubmit())} />
)
}
Child component:
const WaterStep = ({ form }) => {
const { control, formState: { errors }, } = form;
return (
<Controller
name="testResult"
control={control}
rules={{
maxLength: 3,
required: true,
}}
render={({ field: onBlue, onChange, value }) => (
<TextInput
keyboardType="number-pad"
maxLength={3}
onBlur={onBlur}
onChangeText={onChange}
value={value}
/>
)}
/>
)}
Here I'm trying the first approach this answer suggests, but I've also tried the second with useFormContext() in child https://stackoverflow.com/a/70603480/8561357
Additionally, must we use control in React Native? The examples that use register appear simpler, but the official docs are limited for React Native and only show use of control
Update: From Abe's answer, you can see that I'm getting undefined because I'm calling onSubmit callback in my submit button. I mistakenly did this because I wasn't seeing any data getting logged when passing onSubmit properly like this handleSubmit(onSubmit). I still think my issue is that my child component's data isn't being tracked properly by the form in parent
The problem is most likely in this line:
<Button onSubmit={handleSubmit(onSubmit())} />
Since you're executing the onSubmit callback, you're not allowing react-hook-forms to pass in the data from the form. Try replacing it with the following
<Button onSubmit={handleSubmit(onSubmit)} />
For anyone still looking for guidance on using react-hook-form with child components, here's what I found out to work well:
Parent Component:
const Stepper = (props) => {
const { ...methods } = useForm({
defaultValues: {
testResult: "",
},
});
const onSubmit = (data) => {
console.log("form submission data: ", data);
};
const onError = (errors, e) => {
return console.log("form submission errors: ", errors);
};
return (
<FormProvider {...methods}>
<WaterStep
name="testResult"
rules={{
maxLength: 3,
required: true,
}}
/>
<Button onSubmit={handleSubmit(onSubmit)} />
)
}
Child:
import { useFormContext, useController } from "react-hook-form";
const WaterStep = (props) => {
const formContext = useFormContext();
const { formState } = formContext;
const { name, label, rules, defaultValue, ...inputProps } = props;
const { field } = useController({ name, rules, defaultValue });
if (!formContext || !name) {
const msg = !formContext
? "Test Input must be wrapped by the FormProvider"
: "Name must be defined";
console.error(msg);
return null;
}
return (
<View>
<Text>
Test Input
{formState.errors.testResult && <Text color="#F01313">*</Text>}
</Text>
<TextInput
style={{
...(formState.errors.phTestResult && {
borderColor: "#f009",
}),
}}
placeholder="Test Value"
keyboardType="number-pad"
maxLength={3}
onBlur={field.onBlur}
onChangeText={field.onChange}
value={field.value}
/>
</View>
);
};
Here's what we're doing:
Define useForm() in parent and de-structure all its methods
Wrap child in <FormProvider> component and pass useForm's methods to this provider
Make sure to define name and rules as props for your child component so it can pass these to useController()
In your child component, define useFormContext() and de-structure your props
Get access to the field methods like onChange, onBlur, value by creating a controller. Pass those de-structured props to useController()
You can go to an arbitrary level of nested child, just wrap parents in a <FormProvider> component and pass formContext as prop.
In Ancestor:
...
const { ...methods } = useForm({
defaultValues: {
testResult: "",
},
});
const onSubmit = (data) => {
console.log("form submission data: ", data);
};
...
<FormProvider {...methods}>
<ChildOne/>
</FormProvider>
In Parent:
const ChecklistSection = (props) => {
const formContext = useFormContext();
const { formState } = formContext;
return (
<FormProvider {...formContext}>
<WaterStep
name="testResult"
rules={{
maxLength: 3,
required: true,
}}
/>
</FormProvider>
)}
Thanks to https://echobind.com/post/react-hook-form-for-react-native (one of the only resources I found on using react-hook-form with nested components in react-native)
....
And a further evaluation of my blank submission data problem, if you missed it:
As Abe pointed out, the reason I didn't see data or errors being logged upon form submission was because onSubmit was not being called. This was because my custom submission button, which I didn't include in my original question for simplicity's sake, had a broken callback for a completion gesture. I thought I solved onSubmit not being called by passing it as a call onSubmit(), but I was going down the wrong track.
I'm trying to add a feature to my app which is add your contacts to your profile. I'm using a package for this but it works slow (in my case 470 contact record I got in my phone).
The package
import Contacts from 'react-native-contacts';
My code
componentDidMount() {
this.getContacts();
}
getContacts() {
PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.READ_CONTACTS, {
title: 'Contacts',
message: 'This app would like to view your contacts.',
buttonPositive: 'Please accept bare mortal',
})
.then(
Contacts.getAllWithoutPhotos().then(contacts => {
var contactss = [];
contacts.map(contact => {
/// I'm mapping it for showing them in simple list with checkbox
contactss.push({...contact, checked: false});
});
// then sorting for alphabetical order by displayName
contactss.sort(function (a, b) {
if (a.displayName.toLowerCase() < b.displayName.toLowerCase())
return -1;
if (a.displayName.toLowerCase() > b.displayName.toLowerCase())
return 1;
return 0;
});
this.setState(
{contacts: contactss, loaded: true},
() => {
console.log('contacts', this.state.contacts);
},
);
}),
)
.then(contacts => {});
}
That's all code. Is this normal or should I do what?
Thank you.
I'm just trying to handle with this data. Also I'm giving select option to user which account you want, like this.
//function to select contact
checkContc(contc) {
console.log('checkFriend', contc);
let contactsTemp = this.state.contactsTemp.map(el =>
el.recordID == contc.recordID
? {...el, checked: !el.checked}
: {...el},
);
this.setState({contactsTemp}, () => {
console.log('check frined stateD ', this.state);
});
}
// render in scrollview
<ScrollView>
{this.state.contactsTemp?.map((follower, index) => {
return (
<TouchableOpacity
onPress={() => {
this.checkFriend(follower);
}}>
<FriendsComponent
checked={follower.checked}
nameSurname={follower.displayName}
key={index}
/>
</TouchableOpacity>
);
})}
</ScrollView>
result
I made a research and it looks like it is that slow. I'm not sure if the only solution isn't the native one.
I have used indicative validator to validate the form. The Validation works perfectly fine. However, the navigation stops working when its written inside try when all validation operations are performed.
state ={
name:'',
email:'',
password:'',
password_confirmation:'',
error:{},
userAllData:'',
userData:''
}
Have used async and provided the rules for the email and password, the datatype and format.
registerUser = async (data) => {
const rules = {
email:'required|email',
password:'required|string|min:5'
}
Error Messages to be shown and axios API call.
const messages = {
required: (field) => `${field} is required.`,
'email.email': ' The email is required. ',
'password.min': 'Password is too short.',
}
try {
await validateAll(data, rules, messages)
const response = await axios.get('https://react-blog-
api.bahdcasts.com/api/auth/login', {
email:data.email,
password:data.password
})
this.setState({
userData:response.data.data.user,
userAllData:response.data.data
})
NavigationService.navigate('PublicHome')
}
catch(errors){
const formattedErrors = {}
if(errors.response && errors.response.status === 422){
formattedErrors['email'] = errors.response.data['email'][0]
this.setState({ error:formattedErrors, })
}
else{
errors.forEach(error => formattedErrors[error.field] = error.message)
this.setState({ error:formattedErrors })
}
}
}
Inside render: I have the textfields of email and password with a button inside which I am calling this.registerUser(this.state) on Press handler.
<Button style={Styles.btn}
onPress={() => { this.registerUser(this.state) }}>
<Text style={Styles.loginBtnText}>{'Login'.toUpperCase()}</Text>
</Button>
I'm having issue with my componentDidUpdate always updating the render()/state.
My state by default has applicantsData
this.state = {
applicantsData: []
};
ComponentDidMount and ComponentDidUpdate call my method that loads data for the state
componentDidMount() {
this.getApplicants();
}
componentDidUpdate(prevProps) {
// needs to be a unique value
if (prevProps.applicantsData !== this.props.applicantsData) {
this.getApplicants();
}
}
getApplicants = async () => {
AsyncStorage.getItem('CurrentPropertyID')
.then((value) => {
const propID = JSON.parse(value);
this.props.getApplicants(propID);
this.setState({ applicantsData: this.props.applicantsData });
});
}
Then, I have a search box and a FlatList to create my component. I'm using react-native-search-box' for the search box
<Search
ref="search_box"
onSearch={this.onSearch}
onChangeText={this.onChangeText}
/>
<FlatList
extraData={this.state}
data={this.state.applicantsData}
keyExtractor={(item) => {
return item.id.toString();
}}
renderItem={this.renderItem}
/>
My onChangeText method:
onChangeText = (searchText) => {
return new Promise((resolve) => {
let data = this.props.applicantsData;
if (searchText !== '') {
data = data.filter((item) =>
item.name.toUpperCase().includes(searchText.toUpperCase()) ||
item.Email.toUpperCase().includes(searchText.toUpperCase()) ||
item.Phone.toUpperCase().includes(searchText.toUpperCase())
).map(({ id, name, dtEntered, approve, Email, Phone, image }) =>
({ id, name, dtEntered, approve, Email, Phone, image }));
this.setState({ applicantsData: data });
} else {
this.setState({ applicantsData: this.props.applicantsData });
}
resolve();
});
}
Everything is working fine. The data is loaded and displayed in the screen. However, the componentDidUpdate is called all the name and keep updating the props, so when I use the search box to filter my state.applicantsData, it is quickly filtered, then all the data is loaded again because the this.getApplicants was called inside the componentDidUpdate().
Does it make sense? How can I fix this issue?
Thanks
This part is the problem.
componentDidUpdate(prevProps) {
// needs to be a unique value
if (prevProps.applicantsData !== this.props.applicantsData) {
this.getApplicants();
}
}
You have to use deep compare method to compare applicantsData.
You can use https://www.npmjs.com/package/deep-equal or isEqual from lodash