onChangeText returns the previous state value - react-native

I am trying to make a dynamic form. The user must enter the required count of fields in the first text field. After that, I add the entered count to the state and render all the fields. But in "onChangeText" the previous state value is returned. At the same time, if I click on the button, the current state value is returned?
import React, {useState} from "react";
import {Button, TextInput, View, StyleSheet} from "react-native";
export const TestScreen = () => {
const initialState: any = {
num: null,
inputs: null
};
const [state, setState] = useState(initialState);
const changeNum = (num = null) => {
const inputs = [];
if (num) {
for (let i = 0; i < num; i++) {
inputs.push(
<TextInput
key={i.toString()}
style={styles.input}
onChangeText={changeNewInput}
/>
)
}
}
setState({num, inputs})
};
const changeNewInput = (e) => {
console.log(state.num)
};
return (
<View>
<TextInput onChangeText={changeNum} style={{...styles.input, borderColor: 'red'}}/>
{state.inputs}
<Button title={'see count'} onPress={() => console.log(state.num)}/>
</View>
)
};
const styles = StyleSheet.create({
input: {
borderWidth: 1,
borderColor: 'black',
marginBottom: 5
}
});

the state update using the updater provided by useState hook is asynchronous. That's why you see previous value of your state when you call changeNewInput method after setState. If you want to see your state is updated you can use, useEffect.
useEffect(() => {
console.log(state.num)
}, [state]);

I'm using my code example
Create a new useEffect and set a new State inside
OLD STATE const [comment, setComment] = useState('')
NEW STATE const [newData, setNewData] = useState(null)
onChangeText={text => setComment(text)}
useEffect(() => {
setNewData(comment)
},[comment])

Related

How can I interact with React Native DateTimePicker from a jest testing suite

I have made a stripped-down version of my project that has a simple button, which opens a Date Picker when pressed. If the user selects a date the text label is updated. The onChange function of the datePicker calls another function. How can I interact with the datePicker from jest i.e. change the value and test that the label is updated?
This is my app:
import React from 'react';
import {useState} from "react"
import { Platform, Button, Text, View} from 'react-native';
import DateTimePicker from '#react-native-community/datetimepicker';
const App = () => {
const [date, setDate] = useState(new Date());
const [mode, setMode] = useState('date');
const [show, setShow] = useState(false);
const onChange = (event, selectedDate) => {
const currentDate = selectedDate;
setShow(false);
setDate(currentDate);
testFunction()
};
const showMode = (currentMode) => {
if (Platform.OS === 'android') {
setShow(true);
}
setMode(currentMode);
};
const showDatepicker = () => {
showMode(mode);
};
function testFunction(){
console.log("testFunction called")
}
return (
<View style={{
flex: 1,
justifyContent: "center",
alignItems: "center"
}}>
<Button style={{margin:50}} onPress={showDatepicker} title="Show date picker!" />
<Text>selected: {date.toLocaleString()}</Text>
{show && (
<DateTimePicker
testID="dateTimePicker"
value={date}
mode={mode}
is24Hour={true}
onChange={onChange}
/>
)}
</View>
);
};
export default App;
And this is my test
import React from 'react';
import renderer from 'react-test-renderer';
import {fireEvent, render, screen, waitFor, userEvent} from '#testing-library/react-native';
import App from "../app"
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper')
describe('Date Picker Tests', () => {
it.only("changing the date of datepicker", async() => {
let journal = render(<App/>)
let text = screen.getByText("Show date picker!")
fireEvent.press(text)
let p = screen.getByTestId("dateTimePicker")
console.log(p)
})
})
I get the error Unable to find an element with testID: dateTimePicker
So I want to know, how I can change the date on the DatePicker from Jest and test that the label is updated with the selected date.
import React from 'react';
import renderer from 'react-test-renderer';
import {fireEvent, render, screen, waitFor, userEvent} from '#testing-library/react-native';
import App from "../app"
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper')
// Utility function to prepare the date to be accepted by DatePicker's onChange method
const createDateTimeSetEvtParams = (
date: Date,
): [DateTimePickerEvent, Date] => {
return [
{
type: 'set',
nativeEvent: {
timestamp: date.getTime(),
},
},
date,
];
};
describe('Date Picker Tests', () => {
it.only("changing the date of datepicker", async() => {
// mock the function we expect to be called when date is changed
let testFunction = jest.fn()
// render the parent
render(<App/>)
// Press the button to make the DatePicker visible
fireEvent.press(screen.getByText("Show date picker!"))
// Generate new date
let date = new Date()
// Fire the onChange Event
fireEvent(
UNSAFE_getByType(DateTimePicker),
'onChange',
...createDateTimeSetEvtParams(date),
))
expect(testFunction).toHaveBeenCalled()
})
})

Testing react-native app with jest. Problem in accessing context and Provider

I know the title is very vague but I hope someone may have an idea.
I want to perform a simple snapshot test on one of my screens with jest but I keep getting errors like this:
Warning: React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
14 | test('renders correctly', async () => {
15 | const tree = renderer.create(
> 16 | <AuthContext.AuthProvider>
| ^
17 | <AuthContext.Consumer>
18 | <ValidateScreenPhrase ref={(navigator)=>{ setNavigator(navigator) }}/>
19 | </AuthContext.Consumer>
The problem is probably that I use a Context build that looks as follows:
import React, { useReducer } from 'react'
export default (reducer, actions, defaultValue) => {
const Context = React.createContext();
const Provider = ({children}) => {
const [state, dispatch] = useReducer(reducer, defaultValue)
const boundActions = {}
for (let key in actions){
boundActions[key] = actions[key](dispatch);
}
return (
<Context.Provider value={{ state, ...boundActions }}>{children}</Context.Provider>
)
}
return { Context, Provider }
}
from here I then build different Contexts that contain functions and states as e.g.:
import createDataContext from "./createDataContext"
import { navigate } from '../navigationRef'
const authReducer = (state, action) => {
switch (action.type){
case 'clear_error_message':
return { ...state, errorMessage: '' }
default:
return state
}
}
const validateInput = (dispatch) => {
return (userInput, expected) => {
if (userInput === expected) {
navigate('done')
}
else{dispatch({ type: 'error_message', payload: 'your seed phrase was not typed correctly'})}
}
}
export const { Provider, Context } = createDataContext(
authReducer,
{ clearErrorMessage },
{ errorMessage: '' }
)
Now the screen that I want to test is this:
import React, { useState, useContext, useEffect } from 'react'
import { StyleSheet, View, TextInput, SafeAreaView } from 'react-native'
import { Text, Button } from 'react-native-elements'
import { NavigationEvents } from 'react-navigation'
import { Context as AuthContext } from '../context/AuthContext'
import BackButton from '../components/BackButton'
const ValidateSeedPhraseScreen = ({navigation}) => {
const { validateInput, clearErrorMessage, state } = useContext(AuthContext)
const [seedPhrase, setSeedPhrase] = useState('')
const testPhrase = 'blouse'
const checkSeedPhrase = () => {
validateInput(seedPhrase, testPhrase)}
return (
<SafeAreaView style={styles.container}>
<NavigationEvents
onWillFocus={clearErrorMessage}
/>
<NavigationEvents />
<BackButton routeName='walletInformation'/>
<View style={styles.seedPhraseContainer}>
<Text h3>Validate Your Seed Phrase</Text>
<TextInput
style={styles.input}
editable
multiline
onChangeText={(text) => setSeedPhrase(text)}
value={seedPhrase}
placeholder="Your Validation Seed Phrase"
autoCorrect={false}
autoCapitalize='none'
maxLength={200}
/>
<Button
title="Validate"
onPress={() => checkSeedPhrase(seedPhrase, testPhrase)}
style={styles.validateButton}
/>
{state.errorMessage ? (<Text style={styles.errorMessage}>{state.errorMessage}</Text> ) : null}
</View>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginLeft: 25,
marginRight: 25
},
seedPhraseContainer:{
marginTop: '40%'
},
input: {
height: 200,
margin: 12,
borderWidth: 1,
padding: 10,
fontSize: 20,
borderRadius: 10
},
validateButton:{
paddingBottom: 15
}
})
export default ValidateSeedPhraseScreen
Here I import the AuthContext and make use of the function validateInput and state from the Context. Here I also don't know how to bring these into the testing file
and my test so far looks like this:
import React, {useContext} from "react";
import renderer from 'react-test-renderer';
import { setNavigator } from '../../src/navigationRef';
import ValidateScreenPhrase from '../../src/screens/ValidateSeedPhraseScreen'
import { Provider as AuthProvider, Context as AuthContext } from '../../src/context/AuthContext';
jest.mock('react-navigation', () => ({
withNavigation: ValidateScreenPhrase => props => (
<ValidateScreenPhrase navigation={{ navigate: jest.fn() }} {...props} />
), NavigationEvents: 'mockNavigationEvents'
}));
test('renders correctly', async () => {
const tree = renderer.create(
<AuthProvider>
<AuthContext.Consumer>
<ValidateScreenPhrase ref={(navigator)=>{ setNavigator(navigator) }}/>
</AuthContext.Consumer>
</AuthProvider>, {}).toJSON();
expect(tree).toMatchSnapshot();
});
I already tried out all lot of changes with the context and provider structure. I then always get errors like: "Authcontext is undefined" or "render is not a function".
Does anyone have an idea about how to approach this?

How to get the city location in react-native?

import React, { useEffect, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import * as Location from 'expo-location'
import Permissions from "expo-permissions";
export default function App() {
const [address, setAddress] = useState(null);
const [errorMsg, setErrorMsg] = useState(null);
useEffect(() => {
console.log(errorMsg)
console.log(address)
}, []);
const _getLocationAsync = async () => {
let { status } = await Permissions.askAsync(Permissions.LOCATION);
if (status !== "granted") {
setErrorMsg = "Permission to access location was denied";
}
const location = await Location.reverseGeocodeAsync({});
const address = await Location.reverseGeocodeAsync(location.coords);
address;
_getLocationAsync();
};
let text = "Waiting...";
if (errorMsg) {
text = setErrorMsg;
} else if (address) {
text = setAddress[0].city;
}
return (
<View style={styles.container}>
<Text style={styles.text}>{text}</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#bcbcbc',
alignItems: 'center',
justifyContent: 'center',
},
text: {
fontSize: 20
}
});
I have this code to get the city location in react-native, but I don't know what else I should do to get the location. I'm trying this for a while but I'm changing so many things and don't know exactly what I did...
I'd appreciate if someone could help me a little here
I used Geolocation from '#react-native-comunity/geolocation' which returns latitude and longitude.
Geolocation.getCurrentPosition(async (info) => {
const location = await getLocation(
info.coords.latitude,
info.coords.longitude,
);
...
And used google maps api for geocoding
export const getLocation = async (lat: number, long: number) => {
const apiKey = 'my api key'
return Api.get(
`https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${long}&key=${apiKey}`,
);
};
Hope this works for you

MobX observable changes not updating components

I have a store in MobX that handles containing the cart, and adding items to it. However, when that data is updated the components don't re-render to suit the changes.
I tried using useEffect to set the total price, however when the cartData changes, the state is not updated. It does work on mount, though.
In another component, I call addItemToCart, and the console.warn function returns the proper data - but that data doesn't seem to be synced to the component. clearCart also seems to not work.
My stores are contained in one RootStore.
import React, {useState, useEffect} from 'react';
import {List, Text} from '#ui-kitten/components';
import {CartItem} from '_molecules/CartItem';
import {inject, observer} from 'mobx-react';
const CartList = (props) => {
const {cartData} = props.store.cartStore;
const [totalPrice, setTotalPrice] = useState(0);
const renderCartItem = ({item, index}) => (
<CartItem itemData={item} key={index} />
);
useEffect(() => {
setTotalPrice(cartData.reduce((a, v) => a + v.price * v.quantity, 0));
console.warn('cart changed!');
}, [cartData]);
return (
<>
<List
data={cartData}
renderItem={renderCartItem}
style={{backgroundColor: null, width: '100%'}}
/>
<Text style={{textAlign: 'right'}} category={'s1'}>
{`Total $${totalPrice}`}
</Text>
</>
);
};
export default inject('store')(observer(CartList));
import {decorate, observable, action} from 'mobx';
class CartStore {
cartData = [];
addItemToCart = (itemData) => {
this.cartData.push({
title: itemData.title,
price: itemData.price,
quantity: 1,
id: itemData.id,
});
console.warn(this.cartData);
};
clearCart = () => {
this.cartData = [];
};
}
decorate(CartStore, {
cartData: observable,
addItemToCart: action,
clearCart: action,
});
export default new CartStore();
I have solved the issue by replacing my addItemToCart action in my CartStore with this:
addItemToCart = (itemData) => {
this.cartData = [
...this.cartData,
{
title: itemData.title,
price: itemData.price,
quantity: 1,
id: itemData.id,
},
];
console.warn(this.cartData);
};

why are colors being updated multiple times

Why are my random colors being updated multiple times? Is the right way to control this behavior through a lifecycle event?
import React from 'react';
import { StyleSheet, Text, ScrollView, FlatList, SectionList, View, Button, SegmentedControlIOS } from 'react-native';
import contacts, {compareNames} from './contacts';
import {Constants} from 'expo';
import PropTypes from 'prop-types'
function getRandomColor() {
var letters = '0123456789ABCDEF';
var color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
const Row=(props)=>(
<View style={styles.row}>
<Text style={{color:props.color}} >{props.name}</Text>
<Text >{props.phone}</Text>
</View>
)
const renderItem=(obj)=> {
return(<Row {...(obj.item)} color={getRandomColor()} />)
}
const ContactsList = props => {
const renderSectionHeader=(obj) =><Text>{obj.section.title}</Text>
const contactsByLetter = props.contacts.reduce((obj, contact) =>{
const firstLetter = contact.name[0].toUpperCase()
return{
...obj,
[firstLetter]: [...(obj[firstLetter] || []),contact],
}
},{})
const sections = Object.keys(contactsByLetter).sort().map(letter=>({
title: letter,
data: contactsByLetter[letter],
}))
return(
<SectionList
keyExtractor = { (item, key) => key.toString() }
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
sections={sections}
/>
)}
ContactsList.propTypes ={
renderItem: PropTypes.func,
renderSectionHeader: PropTypes.func,
contacts: PropTypes.array,
sections: PropTypes.func
}
export default class App extends React.Component {
state={show: false, selectedIndex: 0, contacts: contacts}
toggleContacts=()=>{
this.setState({show:!this.state.show})
}
sort=()=>{
this.setState({contacts: [...this.state.contacts].sort(compareNames)})
}
render() {
return (
<View style={styles.container}>
<Button title="toggle names" onPress={this.toggleContacts} />
<Button title="sort" onPress={this.sort} />
<SegmentedControlIOS
values={['ScrollView', 'FlatList','SectionList']}
selectedIndex={this.state.selectedIndex}
onChange={(event) => {
this.setState({selectedIndex: event.nativeEvent.selectedSegmentIndex});
}} />
{this.state.show && this.state.selectedIndex === 0 &&
<ScrollView >
{this.state.contacts.map(contact=>(
<Row {...contact}/> ))}
</ScrollView>}
{this.state.show && this.state.selectedIndex === 1 &&
<FlatList
data={this.state.contacts}
keyExtractor = { (item, index) => index.toString() }
renderItem={renderItem}>
</FlatList>}
{this.state.show && this.state.selectedIndex === 2 &&
<ContactsList
contacts={this.state.contacts}>
</ContactsList>}
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
// alignItems: 'flex-start',
paddingTop: Constants.statusBarHeight + 25,
},
row: {
padding:20,
},
});
const NUM_CONTACTS = 10
const firstNames = ['Emma','Noah','Olivia','Liam','Ava','William','Sophia','Mason','Isabella','James','Mia','Benjamin','Charlotte','Jacob','Abigail','Michael','Emily','Elijah','Harper','Ethan','Amelia','Alexander','Evelyn','Oliver','Elizabeth','Daniel','Sofia','Lucas','Madison','Matthew','Avery','Aiden','Ella','Jackson','Scarlett','Logan','Grace','David','Chloe','Joseph','Victoria','Samuel','Riley','Henry','Aria','Owen','Lily','Sebastian','Aubrey','Gabriel','Zoey','Carter','Penelope','Jayden','Lillian','John','Addison','Luke','Layla','Anthony','Natalie','Isaac','Camila','Dylan','Hannah','Wyatt','Brooklyn','Andrew','Zoe','Joshua','Nora','Christopher','Leah','Grayson','Savannah','Jack','Audrey','Julian','Claire','Ryan','Eleanor','Jaxon','Skylar','Levi','Ellie','Nathan','Samantha','Caleb','Stella','Hunter','Paisley','Christian','Violet','Isaiah','Mila','Thomas','Allison','Aaron','Alexa','Lincoln']
const lastNames = ['Smith','Jones','Brown','Johnson','Williams','Miller','Taylor','Wilson','Davis','White','Clark','Hall','Thomas','Thompson','Moore','Hill','Walker','Anderson','Wright','Martin','Wood','Allen','Robinson','Lewis','Scott','Young','Jackson','Adams','Tryniski','Green','Evans','King','Baker','John','Harris','Roberts','Campbell','James','Stewart','Lee','County','Turner','Parker','Cook','Mc','Edwards','Morris','Mitchell','Bell','Ward','Watson','Morgan','Davies','Cooper','Phillips','Rogers','Gray','Hughes','Harrison','Carter','Murphy']
// generate a random number between min and max
const rand = (max, min = 0) => Math.floor(Math.random() * (max - min + 1)) + min
// generate a name
const generateName = () => `${firstNames[rand(firstNames.length - 1)]} ${lastNames[rand(lastNames.length - 1)]}`
// generate a phone number
const generatePhoneNumber = () => `${rand(999, 100)}-${rand(999, 100)}-${rand(9999, 1000)}`
// create a person
const createContact = () => ({name: generateName(), phone: generatePhoneNumber()})
// compare two contacts for alphabetizing
export const compareNames = (contact1, contact2) => contact1.name > contact2.name
// add keys to based on index
const addKeys = (val, key) => ({key, ...val})
// create an array of length NUM_CONTACTS and alphabetize by name
export default Array.from({length: NUM_CONTACTS}, createContact).map(addKeys)
It looks like your component is triggering the render method multiple times. This happens mostly when you use setState method, since everytime your state changes the render method is triggered.
You may have 2 options to handle this:
1) Identify where your component is being rerendered and treat it if this behavior is unnecessary. You can use console.log to debug it.
2) You can avoid the random method to be called multiple times if you call it only in your componentDidMount method. Since componentDidMount is called only once in the component lifecycle, all functions inside of it will not be triggered when the component rerenders, only when it mount again. (I think this is the solution)
Let me know if it helps.