dynamic textInput re-renders the whole component - react-native

It is part of a course so library is not an option. Basically, given a json object, generate a form. I can see the elements but I can't type in them. From my understanding, on each typing, the component is being rendered so the useState is being re-initalized. The only way I can type is if i remove the
value={formFields[id]}
from the TextInput.
https://snack.expo.io/#wusile/tenacious-fudge
Here is my code:
/* eslint-disable react/jsx-closing-bracket-location */
import React, { useContext, useState, useEffect } from 'react';
import { View, ScrollView, Text } from 'react-native';
import { TextInput, Button } from 'react-native-paper';
/*
Build a form dynamically from jsonData
see examples/sampleform.json file
*/
const defineFormFields = (data) => {
/*
Define the state to use along with default values.
*/
const stateData = {};
data.forEach((e) => {
stateData[e.id] = e.defaultValue || '';
});
return stateData;
};
const MyComponent = ({ jsonData }) => {
const [formFields, updateFormFields] = useState(defineFormFields(jsonData));
const [currentSegmentElements, updateCurrentViewElements] = useState([]);
const updateFormData = (fieldName, value) => {
const updatedValue = {};
updatedValue[fieldName] = value;
updateFormFields({
...formFields,
...updatedValue
});
};
const elementTypes = {
text(label, id) {
return (
<TextInput
key={id}
accessibilityHint={label}
label={label}
defaultValue={formFields[id]}
value={formFields[id]}
placeholder={label}
onChangeText={(value) => updateFormData(id, value)}
/>
);
}
};
const buildSegment = () => {
/*
Which segment/portion of the json to show
*/
const uiElements = [];
jsonData.forEach((e) => {
const definition = elementTypes[e.type](
e.label,
e.id
);
uiElements.push(definition);
});
updateCurrentViewElements(uiElements);
};
useEffect(() => {
buildSegment();
}, []);
return (
<ScrollView>
<View>
<View>
{currentSegmentElements.map((m) => m)}
</View>
</View>
</ScrollView>
);
};
const FormBuilder = React.memo(MyComponent);
export default FormBuilder;
Now where I need a form, I do:
const jsonData = [
{
"id":"FN",
"label":"FIrst Name",
"type":"text"
},
{
"id":"SN",
"label":"Last Name",
"type":"text"
},
{
"id":"countryCode",
"label":"Country Code",
"defaultValue":"US",
"type":"text"
}
]
<FormBuilder jsonData={jsonData} />

replace your useEffect by this.
useEffect(() => {
buildSegment();
}, [formFields]);

Related

Replace form.change from react-final-form in react-admin v4

I have this component made for react-admin v3 that allows me to generate an id code. Now I'm upgrading to version 4 of react-admin and I don't know how to replace the part of the code where I do.
form.change("referredCode", code);
Here is the complete code of the component.
import React, { useEffect, useState } from "react";
import { TextInput, useDataProvider, LoadingIndicator } from "react-admin";
import { useForm } from "react-final-form";
import { randomIdGenerator } from "../../helpers/randomIdGenerator";
export default function UserReferredCode({ record }) {
const { referredCode } = record;
const [code, setCode] = useState("");
const [isLoading, setIsLoading] = useState(false);
const dataProvider = useDataProvider();
const form = useForm();
useEffect(() => {
if (!referredCode) {
// Generar id aleatorio
setIsLoading(true);
setCode(randomIdGenerator(6));
}
}, []);
useEffect(() => {
if (code) {
dataProvider
.getList("users", {
pagination: { page: 1, perPage: 1 },
filter: { referredCode: code },
})
.then(({ data }) => {
if (data.length > 0) {
setCode(randomIdGenerator(6));
} else {
setIsLoading(false);
}
})
.catch((e) => {
setIsLoading(false);
console.log(e);
});
}
form.change("referredCode", code);
}, [code]);
return (
<>
{isLoading ? (
<LoadingIndicator />
) : (
<TextInput
disabled
source="referredCode"
name="referredCode"
type="text"
placeholder="Code"
initialValue={referredCode || code}
/>
)}
</>
);
}
You should have a Form component wrapping all of this.
Check the Form documentation https://marmelab.com/react-admin/doc/4.0/Form.html
then you you can access it from useFormContext() (from react-hook-form).
the form has action setValue that accepts name and value
https://react-hook-form.com/api/useform/setvalue

Display all posts from database

I have a Firestore collection, schemed as follows:
posts{
uid{
userPosts{
postID{
creation:
postText:
}
}
}
}
I want to display all of the posts, so I've made the corresponding queries and saved them in posts - an array of all the posts that I later iterate through.
The problem with the way I do it is that it keeps adding the same posts every render. So I've tried to set the array each time, but that way the code never passes through these posts && posts.length > 0 condition.
I'm really new to RN and JS in general, but what I was expecting is
Nothing to show here
at first, and then the list of posts.
The complete component:
import { Text, Pressable, FlatList, SafeAreaView } from "react-native";
import { globalStyles } from "../../styles/global";
import React, { useState, useEffect } from "react";
import { db } from "../../../firebase";
import Post from "../../API/Post";
import { collection, getDocs } from "firebase/firestore";
const FeedScreen = ({ navigation }) => {
const [posts, setPosts] = useState([]);
useEffect(() => {
const getPostData = async () => {
setPosts([]); // ---> Without this line the posts keeps adding each render
const q = collection(db, "posts");
const docSnap = await getDocs(q);
docSnap.docs.map(async (item) => {
const tmp = collection(db, "posts", item.id, "userPosts");
const tmpSnap = await getDocs(tmp);
tmpSnap.docs.map(async (element) => {
setPosts((prev) => {
prev.push(element.data());
return prev;
});
});
});
};
getPostData().catch(console.error);
return;
}, []);
return (
<SafeAreaView style={globalStyles.global}>
{posts && posts.length > 0 ? (
<FlatList
data={posts}
renderItem={({ item }) => (
<Post
post={item}
navigation={navigation}
style={globalStyles.list_of_posts}
/>
)}
keyExtractor={(item, index) => index.toString()}
/>
) : (
<Text>Nothing to show here</Text>
)}
<Pressable
title="edit"
onPress={() => {
navigation.navigate("CreatePost", { navigation });
}}
style={globalStyles.plus_btn}
>
<Text style={globalStyles.plus_btn_text}>+</Text>
</Pressable>
</SafeAreaView>
);
};
export default FeedScreen;
As said, I'm new to this so I'd love an explanation of what actually happens and how to do it properly.
I think the prev value of setPosts will always be [] since it does not immediately update if you call it. A standard way to do it is to call setPosts at the end of your function. Can you try this one?
useEffect(() => {
const getPostData = async () => {
const q = collection(db, "posts");
const docSnap = await getDocs(q);
const promises = docSnap.docs.map(async (item) => {
const tmp = collection(db, "posts", item.id, "userPosts");
const tmpSnap = await getDocs(tmp);
return tmpSnap.docs.map((element) => element.data());
});
const arrayOfPosts = await Promise.all(promises);
let newPosts = [];
arrayOfPosts.forEach((posts) => {
newPosts = [...newPosts, ...posts];
});
setPosts(newPosts);
};
getPostData().catch(console.error);
return;
}, []);

Screen State not Updating from AsyncStorage when going back

I'm building a React Native app.
My app has 5 Screens: Home (initialRouteName), DeckPage, QuestionPage, NewCardPage, NewDeckPage. (in this order)
I'm using Redux for state management. The state is updating from AsyncStorage.
The component that does the fetching is the class component "Home" by dispatching the "fetching" function in componentDidMount.
Component NewCardPage, NewDeckPAge are also updating the state with new content by dispatching the same fetching function as the Home when a button is pressed.
My problem appears when I want to delete a Deck component from inside DeckPage parent component. The function that does this job has this functionality: after removing the item from AsyncStorage, updates the STATE, and moves back to Screen HOME. The issue is that when I go back to HOME component the state doesn't update with the latest info from AsyncStorage.
This is not the case when I'm doing the same operation in the other 2 components NewCardPage, NewDeckPage.
I'll paste the code below:
import React, { Component } from "react";
import { connect } from "react-redux";
import { View, Text, StyleSheet, FlatList } from "react-native";
import Header from "../components/Header";
import AddDeckButton from "../components/AddDeckButton";
import DeckInList from "../components/DeckInList";
import { receiveItemsAction } from "../redux/actions";
class Home extends Component {
componentDidMount() {
this.props.getAsyncStorageContent();
}
renderItem = ({ item }) => {
return <DeckInList {...item} />;
};
render() {
const { items } = this.props;
// console.log(items);
const deckNumber = Object.keys(items).length;
return (
<View style={styles.container}>
<Header />
<View style={styles.decksInfoContainer}>
<View style={styles.deckNumber}>
<View style={{ marginRight: 50 }}>
<Text style={styles.deckNumberText}>{deckNumber} Decks</Text>
</View>
<AddDeckButton />
</View>
<View style={{ flex: 0.9 }}>
<FlatList
data={Object.values(items)}
renderItem={this.renderItem}
keyExtractor={(item) => item.title}
/>
</View>
</View>
</View>
);
}
}
const mapStateToProps = (state) => {
return {
items: state.items,
};
};
const mapDispatchToProps = (dispatch) => {
return {
getAsyncStorageContent: () => dispatch(receiveItemsAction()),
};
};
-----------DECKPAGE COMPONENT------------
import React from "react";
import { View, StyleSheet } from "react-native";
import Deck from "../components/Deck";
import { useSelector, useDispatch } from "react-redux";
import { removeItemAction, receiveItemsAction } from "../redux/actions";
import AsyncStorage from "#react-native-community/async-storage";
const DeckPage = ({ route, navigation }) => {
const { title, date } = route.params;
const questions = useSelector((state) => state.items[title].questions);
const state = useSelector((state) => state.items);
const dispatch = useDispatch();
// const navigation = useNavigation();
const handleRemoveIcon = async () => {
await AsyncStorage.removeItem(title, () => {
dispatch(receiveItemsAction());
navigation.goBack();
});
};
console.log(state);
return (
<View style={styles.deckPageContainer}>
<Deck
handleRemoveIcon={handleRemoveIcon}
title={title}
questions={questions}
date={date}
/>
</View>
);
};
-----------This is my ACTIONS file----------
import AsyncStorage from "#react-native-community/async-storage";
export const RECEIVE_ITEMS = "RECEIVE_ITEMS";
// export const REMOVE_ITEM = "REMOVE_ITEM";
export const receiveItemsAction = () => async (dispatch) => {
const objectValues = {};
try {
const keys = await AsyncStorage.getAllKeys();
if (keys.length !== 0) {
const jsonValue = await AsyncStorage.multiGet(keys);
if (jsonValue != null) {
for (let element of jsonValue) {
objectValues[element[0]] = JSON.parse(element[1]);
}
dispatch({
type: RECEIVE_ITEMS,
payload: objectValues,
});
} else {
return null;
}
}
} catch (e) {
console.log(e);
}
};
-----This is my REDUCERS file----
import { RECEIVE_ITEMS, REMOVE_ITEM } from "./actions";
const initialState = {
};
const items = (state = initialState, action) => {
switch (action.type) {
case RECEIVE_ITEMS:
return {
...state,
...action.payload,
};
// case REMOVE_ITEM:
// return {
// ...state,
// ...action.payload,
// };
default:
return state;
}
}
export default items;
-----This is my UTILS file----
import AsyncStorage from "#react-native-community/async-storage";
export const removeDeckFromAsyncStorage = async (title)=>{
try{
await AsyncStorage.removeItem(title);
}
catch(e){
console.log(`Error trying to remove deck from AsyncStorage ${e}`);
}
}

How to setting state with navigation params?

I am working on a React-native project with its basic packets(navigation etc). I have two screens. First there is a button and when i click the button. It's navigate to another screen which has flatlist. Then i click value in flatlist it is gives me a value . I can send that value to first screen with this.props.navigation.navigate and i can show it in console but i dont know how to use it to change buttonText which in my first screen? Where should i use setstate function in first screen ? (sorry for english)
Home.js
import React, {Component} from 'react';
import {View, Text} from 'react-native';
import {InputWithButton} from '../components/TextInput';
//const TEMP_BASE_CURRENCY = 'USD';
//const TEMP_CONVERT_CURRENCY = 'GBP';
class Home extends Component {
constructor(props) {
super(props);
this.state = {
baseCurrency: 'TRY', //Başlangıç olarak sayfa açıldığında gelecek olan değerler
convertCurrency: 'USD',
amount: null,
result: '',
date: '',
};
//const selected = this.props.route.params;
}
calculate = () => {
const amount = this.state.amount;
let url =
'https://api.exchangeratesapi.io/latest?base=' + this.state.baseCurrency;
fetch(url, {
method: 'GET',
})
.then((res) => res.json())
.then((data) => {
const date = data.date;
const result = (
data.rates[this.state.convertCurrency] * amount
).toFixed(2);
this.setState({
result,
date,
});
})
.catch((error) => {
console.log(error);
});
};
handleChangeText = (text) => {
//Yazıda değişim algılandığında api işlemleri başlasın
this.setState(
{
amount: text,
},
this.calculate,
);
};
handlePressBaseCurrency = () => {
//flatlist sayfası açılsın
const {navigation} = this.props;
navigation.navigate('CurrencyList');
};
handlePressConvertCurrency = () => {
//flatlist sayfası açılsın
};
render() {
const {baseCurrency, convertCurrency, amount, result, date} = this.state;
return (
<View>
<InputWithButton
buttonText={baseCurrency}
onPress={this.handlePressBaseCurrency}
keyboardType="numeric"
onChangeText={(text) => this.handleChangeText(text)}
/>
<InputWithButton
editable={false}
buttonText={convertCurrency}
onPress={this.handlePressConvertCurrency}
value={result}
/>
</View>
);
}
}
export default Home;
CurrencyList.js
import React, {Component} from 'react';
import {View, FlatList, Text} from 'react-native';
import currencies from '../data/currencies';
import {ListItem, Separator} from '../components/List';
const temp_base_currency = 'CAD';
class CurrencyList extends Component {
constructor(props) {
super(props);
this.state = {
selectedItem: '',
};
}
handlePress = (item) => {
this.setState({
selectedItem: item, //__
});
// const {navigate} = this.props.navigation;
// navigate('Home', {clickedItem: this.state.selectedItem});
//Tıklandığında beklesin
setTimeout(
() => this.props.navigation.navigate('Home', {selected: item}),
1,
); //__
};
render() {
return (
<View>
<FlatList
renderItem={({item}) => (
<ListItem
onPress={() => this.handlePress(item)}
text={item}
selected={item === this.state.selectedItem} //__
/>
)}
data={currencies}
keyExtractor={(item) => item}
ItemSeparatorComponent={Separator}
/>
</View>
);
}
}
export default CurrencyList;
It would have been better if you shared your code but here is what I would do.
SECOND SCREEN
this.props.navigation.navigate('firstScreen', {
name: 'Your value'
})
FIRST SCREEN
const name = this.props.route.params.name;
<Button>{name}</Button
You can pass the selected item from the Flatlist to the Home screen like this:
Home.js:
this.props.navigation.navigate('CurrencyList',
{
onGoback: (item) => this.setState({})
})
CurrencyList.js:
handlePress: (item) => {
/** your code **/
this.props.navigation.state.params.onGoBack(item)
this.props.navigation.navigate('Home')
}

Can't I return a form custom error anymore with "Altering the Form Values before Submitting"?

React-admin version 2 used redux-form. Together with the documentation "Altering the Form Values before Submitting" i created a custom button that returns a form error from a custom function, or does a save.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { crudCreate, SaveButton, Toolbar } from 'react-admin';
import { SubmissionError } from 'redux-form';
const myValidate = (values) => {
const errors = {};
if (values.files < 1) errors.files = 'resources.Order.errors.files';
return errors;
};
// A custom action creator which modifies the values before calling the default crudCreate action creator
const saveWithNote = (values, basePath, redirectTo) =>
crudCreate('posts', { ...values, average_note: 10 }, basePath, redirectTo);
class SaveWithNoteButtonView extends Component {
handleClick = () => {
const { basePath, handleSubmit, redirect, saveWithNote } = this.props;
return handleSubmit(values => {
return new Promise((resolve, reject) => {
const errors = myValidate(values);
if (errors) {
reject(new SubmissionError(errors))
} else {
resolve( saveWithNote(values, basePath, redirect) );
};
}
});
});
};
render() {
const { handleSubmitWithRedirect, saveWithNote, ...props } = this.props;
return (
<SaveButton
handleSubmitWithRedirect={this.handleClick}
{...props}
/>
);
}
}
const SaveWithNoteButton = connect(
undefined,
{ saveWithNote }
)(SaveWithNoteButtonView);
Now in React-admin version 3 "Altering the Form Values before Submitting" Can't I do this anymore?
This code does not work:(
import React, { useCallback } from 'react';
import ProcessIcon from '#material-ui/icons/HeadsetMic';
import { useForm, useFormState } from 'react-final-form';
import { SaveButton } from 'react-admin';
const validateProcess = (values) => {
const errors = {};
if (values.files < 1) errors.files = ['resources.Order.errors.files'];
return errors
};
const SaveWithProcessButton = ({ handleSubmitWithRedirect, ...props }) => {
const form = useForm();
const formState = useFormState();
const handleClick = useCallback(() => {
return new Promise((resolve, reject) => {
const errors = validateProcess(formState.values);
if (errors) {
reject(errors)
} else {
form.change('status', "DRAFT");
resolve( handleSubmitWithRedirect('show') );
};
});
}, [form]);
return <SaveButton {...props} handleSubmitWithRedirect={handleClick} label="ra.action.process" icon={<ProcessIcon />} />;
};
export default SaveWithProcessButton;
I manage to do this. Try my code below, I hope it will works for also for you.
const handleClick = useCallback(() => {
if (!formState.valid) {
form.submit();
return;
} else { ... your code goes here }
}, [form]);