expect(jest.fn()).toHaveBeenCalledWith(...expected) in react-native and expo - react-native

I'm trying to write the unit test for the Controller which is written in react-native and expo Following is the login method for which Unit test is failing
login.tsx
import React, { useState } from 'react';
import { StyleSheet, SafeAreaView, Text, Button, Image, View, Alert, TouchableWithoutFeedback, TouchableOpacity, NativeSyntheticEvent, TextInputChangeEventData, TextInput } from 'react-native';
import axios from "axios"
import { StackNavigationProp } from '#react-navigation/stack';
import { RouteProp } from '#react-navigation/native';
import { RootStackParamList } from '~/navigations/Navigations'
interface props {
navigation: StackNavigationProp<RootStackParamList, 'Login'>;
route: RouteProp<RootStackParamList, 'Login'>;
value: string;
name: string;
}
const instance = axios.create({
baseURL: '',
timeout: 3000,
timeoutErrorMessage: 'do again',
});
const Login: React.FC<props> = ({ value, name, ...props }) => {
const [inputValue, setInputValue] = useState({
email: "",
pwd: ""
});
const [errorMesseage, setErrorMesseage] = useState("");
const onChangeText = (e: string, name: string): void => {
let input = e;
setInputValue(prev => ({
...prev,
[name]: input,
}));
if (name === "email") {
const emailRegx = /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*#[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{3}$/i;
const emailList = ["naver.com", "gmail.com", "daum.net", "nante.com", "hanmail.net"];
let eMailAddress = input.slice(input.indexOf('#') + 1, input.length);
let eMailboolean = emailList.includes(eMailAddress) && name === 'email';
setErrorMesseage(eMailboolean && emailRegx.test(input) ? "" : "eamil regx");
}
}
const loginButton = async () => {
if (errorMesseage.length === 0) {
props.navigation.navigate("Main");
try {
const emailPost = await instance.post("/user", {
"email": inputValue.email,
"password": inputValue.pwd
});
console.log(emailPost);
} catch (error) {
if (error.response) {
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
console.log(error.request);
} else {
console.log('Error', error.message);
}
console.log(error.config);
}
} else {
Alert.alert("email regx");
}
}
return (
<>
<SafeAreaView style={styles.container} >
<View>
<View>
<TextInput onChangeText={text => onChangeText(text, 'email')} placeholder="email" placeholderTextColor={"#eee"} value={inputValue["email"]} autoCorrect={false} secureTextEntry={false} autoCapitalize="none" />
</View>
<Text>{errorMesseage}</Text>
<View>
<TextInput onChangeText={e => onChangeText(e, 'pwd')} placeholder="pwd" placeholderTextColor={"#eee"} value={inputValue["pwd"]} autoCorrect={false} secureTextEntry={true} autoCapitalize="none" />
</View>
<TouchableOpacity style={[styles.loginButton, { opacity: (!errorMesseage) && inputValue.pwd ? 1 : 0.3 }]} disabled={(!errorMesseage) && inputValue.pwd ? false : true} onPress={loginButton} >
<Text style={styles.loginText}>login</Text>
</TouchableOpacity>
<TouchableWithoutFeedback
onPress={() => props.navigation.navigate("Find")}
>
<Text>pwd find</Text>
</TouchableWithoutFeedback>
</View>
</SafeAreaView >
</>
);
};
export default Login;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
loginButton: {
backgroundColor: "#0095F6",
},
loginText: {
textAlign: "center",
color: "#fff"
}
});
Unit Test for the above code is written in react-native and expo
login.test.tsx
import { TextInput, SafeAreaView, TouchableOpacity, TouchableWithoutFeedback, View, Text } from 'react-native';
import React from 'react';
import { render, cleanup, fireEvent } from 'react-native-testing-library';
import '#testing-library/jest-dom/extend-expect'
import TestRenderer from 'react-test-renderer';
import Login from "../src/screen/login/Index";
afterEach(cleanup);
const createTestProps = (props: Object) => ({
navigation: {
navigate: jest.fn()
},
...props
});
describe("Login page rendering test", () => {
jest.useFakeTimers()
let props: any;
let rerender: any;
let testRenderer: any;
let testInstance: any;
beforeEach(() => {
rerender = render(<Login {...props} />);
testRenderer = TestRenderer.create(<Login {...props} />);
testInstance = testRenderer.root;
});
test('renders Login component', () => {
const component = rerender.toJSON();
expect(component).toBeTruthy();
});
it("SafeAreaView renders", () => {
expect(testInstance.findAllByType(SafeAreaView).length).toBe(1);
});
it("View renders", () => {
expect(testInstance.findAllByType(View).length).toBe(5);
});
it("textInput renders", () => {
const expectedPlaceholder = ['email', 'pwd'];
expect(testInstance.findAllByType(TextInput).length).toBe(2);
expectedPlaceholder.forEach((text: string) => {
rerender.findByPlaceholder(text);
});
});
it("TouchableOpacity renders", () => {
const element = testInstance.findByType(TouchableOpacity).findByType(Text);
expect(testInstance.findAllByType(TouchableOpacity).length).toBe(1);
expect(element.props.children).toEqual('login');
});
it("TouchableWithoutFeedback renders", () => {
const element = testInstance.findByType(TouchableWithoutFeedback).findByType(Text);
expect(testInstance.findAllByType(TouchableWithoutFeedback).length).toBe(1);
expect(element.props.children).toEqual('pwd find');
});
});
describe('login page funtion test', () => {
let props: any;
let rerender: any;
beforeEach(() => {
props = {
onChangeText: jest.fn(),
}
rerender = render(<Login {...props} />);
emailInput = rerender.getByPlaceholder('email');
fireEvent.changeText(emailInput, 'ab');
});
it('email input change', async () => {
rerender.getByDisplayValue("ab");
expect(props.onChangeText).toHaveBeenCalledWith('ab');
expect(props.onChangeText).toHaveBeenCalledTimes(1);
})
})
error part
describe('login page funtion test', () => {
let props: any;
let rerender: any;
beforeEach(() => {
props = {
onChangeText: jest.fn(),
}
rerender = render(<Login {...props} />);
emailInput = rerender.getByPlaceholder('email');
fireEvent.changeText(emailInput, 'ab');
});
it('email input change', async () => {
rerender.getByDisplayValue("ab");
expect(props.onChangeText).toHaveBeenCalledWith('ab');
expect(props.onChangeText).toHaveBeenCalledTimes(1);
})
})
I'm getting the following error. Need to know what I'm missing here?
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: "ab"
Number of calls: 0
116 |
> 117 | expect(props.onChangeText).toHaveBeenCalledWith('ab');
| ^
118 | expect(props.onChangeText).toHaveBeenCalledTimes(1);
119 |
120 | })
library
"expo": "^37.0.12",
"jest-dom": "^4.0.0",
"react": "~16.9.0",
"react-native": "https://github.com/expo/react-native/archive/sdk-37.0.1.tar.gz",
"react-test-renderer": "^16.13.1",
"react-native-testing-library": "^2.1.0",

Related

losing my mind - why doesn't my fetch re render (redux)

In my project I have many users and many resources (and many user_resources/the join between users and resources).When I POST to user_resources I see it work on my rails backend (as in I see that instance posted) but in my react native front end I don't see it listed upon update. However, once the app is completely refresh (when I stop and restart the expo server), I finally see those items rendered. ANY IDEAS? I've been working on this forever now to no avail and my project is due tmrw, so any help is appreciated.
screen where I post to user_resources:
import React from 'react';
import { ScrollView,SafeAreaView,StyleSheet, Text, View, FlatList, TouchableOpacity,Button, NativeEventEmitter} from 'react-native';
import {connect} from 'react-redux';
import {fetchResources,searchChanged} from '../actions';
import { addUserResource } from '../actions'
import {SearchBar} from 'react-native-elements';
import { MaterialIcons } from '#expo/vector-icons';
import { MaterialCommunityIcons } from '#expo/vector-icons';
class ResourcesScreen extends React.Component {
state = {
search: ''
}
componentDidMount = () =>{
this.props.fetchResources();
}
FlatListItemSeparator = () => {
return (
<View
style={{
height: 0.5,
width: "100%",
backgroundColor: "lightblue",
}}
/>
);
}
handlePress(item) {
debugger
fetch('http://localhost:3000/user_resources', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
body: JSON.stringify({
resource_id: item.id,
user_id: this.props.users.id,
name: item.name,
description:item.description,
link:item.link,
})
})
.then(res => res.json())
.then(data2 => {
console.log(data2)
this.props.addUserResource(data2)
console.log(this.props)
})
}
header = () => {
return <View>
<Text style={styles.header}>Resources</Text>
</View>
}
onSearchChange = text => {
this.setState({search:text})
}
render(){
return(
<SafeAreaView>
<SearchBar placeholderTextColor="white" placeholder="Enter resource name here" onChangeText={this.onSearchChange} value={this.state.search}/>
<TouchableOpacity onPress={() => this.props.navigation.navigate('Add A New Resource',{topicId:this.props.route.params.topicId})} style={styles.buttonitem}>
<Text style={styles.text}>
<MaterialIcons name="add-circle-outline" size={24} color="white"/>Add A New Resource
</Text>
</TouchableOpacity>
<FlatList keyExtractor={(item)=> item.id.toString()} data={this.props.resourceName} ItemSeparatorComponent = { this.FlatListItemSeparator } renderItem={({item}) => {
return <TouchableOpacity style={styles.material2}>
<Text onPress={() => this.props.navigation.navigate('Add A New Resource',{topicId:item.id})} style={styles.listitem}>{item.name}</Text>
<MaterialCommunityIcons name="bookmark-plus" size={50} color="#16a085" backgroundColor='black' onPress={()=>this.handlePress(item)}/>
</TouchableOpacity>
}}
ListHeaderComponent = {this.header}/>
</SafeAreaView>
)
}
}
const mapStateToProps = (state) => {
return {
resourceName: state.resourceReducer.resources,
users: state.userReducer,
search:state.resourceReducer.searchTerm
}
}
const mapDispatchToProps = (dispatch) => {
return {
fetchResources: () => dispatch(fetchResources()),
addUserResource,
searchChanged
}
}
export default connect(mapStateToProps,mapDispatchToProps)(ResourcesScreen)
After this I head to the profile page where the user_resources SHOULD be displayed, but aren't
import React from 'react';
import { ScrollView,StyleSheet, Text, View, FlatList, TouchableOpacity} from 'react-native';
import {connect} from 'react-redux';
import {SearchBar} from 'react-native-elements';
import { AntDesign } from '#expo/vector-icons';
class Profile extends React.Component{
handleDelete = (id) => {
debugger
fetch(`http://localhost:3000/user_resources/${id}`, {
method: "DELETE",
headers: {
"Authorization": this.props.users.token
}
})
.then(r => r.json())
.then((delResource) => {
console.log(delResource)
this.props.deleteOneFood(delResource)
console.log('deleted')
this.forceUpdate()
})
}
render(){
return(
<View>
{this.props.users.user_resources.map(singleResource=> {
return <Text key={singleResource.id}>{singleResource.name}</Text>
})}
</View>
)}
}
let deleteOneResource = (id) => {
return {
type: "DELETE_ONE_USER_RESOURCE",
payload: id
}
}
const mapDispatchToProps = {
deleteOneResource
}
const mapStateToProps = (state) => {
return {
users: state.userReducer,
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Profile)
I had a flatlist before but thought that may be causing the issues so rendered it another way, still no luck. I tried forceUpdates as well, still no luck. I'm not sure if the issue is coming from my reducer:
let userInitialState = {
id: 0,
username: "",
name: '',
category: '',
token: "",
user_resources:[],
}
let userReducer = (state = userInitialState, action) => {
switch(action.type){
case "ADD_USERS":
let singleNestedObject = {
...action.users.user,
token: action.users.token
}
return {
...state,
username: action.users.user.username,
token: action.users.token,
id: action.users.user.id,
name: action.users.user.name,
category: action.users.user.category,
user_resources: action.users.user.user_resources
}
case "ADD_ONE_USER_RESOURCE":
let copyOfResources = [...state.user_resources, action.userResources]
return {
...state,
user_resources: copyOfResources
}
default:
return state
}
}
and it's action
export const addUserResource = (resourceInfo) => {
return {
type: "ADD_ONE_USER_RESOURCE",
userResources: resourceInfo
}
}
Please help me find the issue here, I'm losing it.

Undefined is not an Object (evaluating 'props.contacts.reduce') - React Native

I'm learning React Native and trying to run this app which I learnt, but for me, an error is thrown like this.Undefined is not an Object (evaluating 'props.contacts.reduce')
Edit : I'm adding all other code used in this app. I'm unable to find the error. Please help. I'm just learning how this works and I'm not the one who created this.This is the progam that the error is pointing to :
SectionListContacts.js
import React from 'react'
import {SectionList, Text} from 'react-native'
import PropTypes from 'prop-types'
import Row from './Row'
const renderSectionHeader = ({section}) => <Text>{section.title}</Text>
const SectionListContacts = props => {
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 => ({
data: contactsByLetter[letter],
title: letter,
}))
return (
<SectionList
keyExtractor={item => item.phone}
sections={sections}
renderItem={({item}) => <Row {...item} onSelectContact={props.onSelectContact} />}
renderSectionHeader={renderSectionHeader}
/>
)
}
SectionListContacts.propTypes = {
contacts: PropTypes.array,
}
export default SectionListContacts
App.js
import React from 'react'
import {
createStackNavigator,
createSwitchNavigator,
createBottomTabNavigator,
} from 'react-navigation'
import Ionicons from 'react-native-vector-icons/Ionicons'
import {Provider} from 'react-redux'
import AddContactScreen from './screens/AddContactScreen'
import SettingsScreen from './screens/SettingsScreen'
import ContactListScreen from './screens/ContactListScreen'
import ContactDetailsScreen from './screens/ContactDetailsScreen'
import LoginScreen from './screens/LoginScreen'
import {fetchUsers} from './api'
import contacts from './contacts'
import store from './redux/store'
const MainStack = createStackNavigator(
{
ContactList: ContactListScreen,
ContactDetails: ContactDetailsScreen,
AddContact: AddContactScreen,
},
{
initialRouteName: 'ContactList',
navigationOptions: {
headerTintColor: '#a41034',
headerStyle: {
backgroundColor: '#fff',
},
},
}
)
MainStack.navigationOptions = {
tabBarIcon: ({focused, tintColor}) => (
<Ionicons name={`ios-contacts${focused ? '' : '-outline'}`} size={25} color={tintColor} />
),
}
const MainTabs = createBottomTabNavigator(
{
Contacts: MainStack,
Settings: SettingsScreen,
},
{
tabBarOptions: {
activeTintColor: '#a41034',
},
}
)
const AppNavigator = createSwitchNavigator({
Login: LoginScreen,
Main: MainTabs,
})
export default class App extends React.Component {
state = {
contacts,
}
/*
componentDidMount() {
this.getUsers()
}
getUsers = async () => {
const results = await fetchUsers()
this.setState({contacts: results})
}
*/
addContact = newContact => {
this.setState(prevState => ({
contacts: [...prevState.contacts, newContact],
}))
}
render() {
return (
<Provider store={store}>
<MainTabs />
</Provider>
)
}
}
api.js
const processContact = contact => ({
name: `${contact.name.first} ${contact.name.last}`,
phone: contact.phone,
})
export const fetchUsers = async () => {
const response = await fetch('https://randomuser.me/api/?results=50&nat=us')
const {results} = await response.json()
return results.map(processContact)
}
export const login = async (username, password) => {
const response = await fetch('http://localhost:8000', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({username, password}),
})
if (response.ok) {
return true
}
const errMessage = await response.text()
throw new Error(errMessage)
}
AddContactForm.js
import React from 'react'
import {Button, KeyboardAvoidingView, StyleSheet, TextInput, View} from 'react-native'
export default class AddContactForm extends React.Component {
state = {
name: '',
phone: '',
isFormValid: false,
}
componentDidUpdate(prevProps, prevState) {
if (this.state.name !== prevState.name || this.state.phone !== prevState.phone) {
this.validateForm()
}
}
getHandler = key => val => {
this.setState({[key]: val})
}
handleNameChange = this.getHandler('name') // val => { this.setState({name: val}) }
handlePhoneChange = this.getHandler('phone')
/*
handleNameChange = name => {
this.setState({name})
}
*/
handlePhoneChange = phone => {
if (+phone >= 0 && phone.length <= 10) {
this.setState({phone})
}
}
validateForm = () => {
console.log(this.state)
const names = this.state.name.split(' ')
if (
+this.state.phone >= 0 &&
this.state.phone.length === 10 &&
names.length >= 2 &&
names[0] &&
names[1]
) {
this.setState({isFormValid: true})
} else {
this.setState({isFormValid: false})
}
}
validateForm2 = () => {
if (+this.state.phone >= 0 && this.state.phone.length === 10 && this.state.name.length >= 3) {
return true
}
return false
}
handleSubmit = () => {
this.props.onSubmit(this.state)
}
render() {
return (
<KeyboardAvoidingView behavior="padding" style={styles.container}>
<TextInput
style={styles.input}
value={this.state.name}
onChangeText={this.getHandler('name')}
placeholder="Name"
/>
<TextInput
keyboardType="numeric"
style={styles.input}
value={this.state.phone}
onChangeText={this.getHandler('phone')}
placeholder="Phone"
/>
<Button title="Submit" onPress={this.handleSubmit} disabled={!this.state.isFormValid} />
</KeyboardAvoidingView>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
input: {
borderWidth: 1,
borderColor: 'black',
minWidth: 100,
marginTop: 20,
marginHorizontal: 20,
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 3,
},
})
contacts.js
const NUM_CONTACTS = 3
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 add keys
export default Array.from({length: NUM_CONTACTS}, createContact).map(addKeys)
FlatListContacts.js
import React from 'react'
import {FlatList} from 'react-native'
import PropTypes from 'prop-types'
import Row from './Row'
const renderItem = ({item}) => <Row {...item} />
const FlatListContacts = props => <FlatList renderItem={renderItem} data={props.contacts} />
FlatListContacts.propTypes = {
contacts: PropTypes.array,
}
export default FlatListContacts
Row.js
import React from 'react'
import {TouchableOpacity, StyleSheet, Text, View} from 'react-native'
import PropTypes from 'prop-types'
const styles = StyleSheet.create({
row: {padding: 20},
})
const Row = props => (
<TouchableOpacity style={styles.row} onPress={() => props.onSelectContact(props)}>
<Text>{props.name}</Text>
<Text>{props.phone}</Text>
</TouchableOpacity>
)
Row.propTypes = {
name: PropTypes.string,
phone: PropTypes.string,
}
export default Row
ScrollViewContacts.js
import React from 'react'
import {ScrollView} from 'react-native'
import PropTypes from 'prop-types'
import Row from './Row'
const ScrollViewContacts = props => (
<ScrollView>{props.contacts.map(contact => <Row {...contact} />)}</ScrollView>
)
ScrollViewContacts.propTypes = {
contacts: PropTypes.array,
}
export default ScrollViewContacts
(Directory : screens)
AddContactScreen.js
import React from 'react'
import AddContactForm from '../AddContactForm'
import {connect} from 'react-redux'
import {addContact} from '../redux/actions'
class AddContactScreen extends React.Component {
static navigationOptions = {
headerTitle: 'New Contact',
}
handleSubmit = formState => {
this.props.addContact({name: formState.name, phone: formState.phone})
this.props.navigation.navigate('ContactList')
}
render() {
return <AddContactForm onSubmit={this.handleSubmit} />
}
}
export default connect(null, {addContact: addContact})(AddContactScreen)
ContactDetailsScreen.js
import React from 'react'
import {Button, Text, View} from 'react-native'
export default class ContactDetailsScreen extends React.Component {
static navigationOptions = ({navigation}) => ({
headerTitle: navigation.getParam('name'),
})
render() {
return (
<View>
<Text>{this.props.navigation.getParam('phone')}</Text>
<Button title="Go to random contact" onPress={this.goToRandomContact} />
</View>
)
}
goToRandomContact = () => {
const {contacts} = this.props.screenProps
const phone = this.props.navigation.getParam('phone')
let randomContact
while (!randomContact) {
const randomIndex = Math.floor(Math.random() * contacts.length)
if (contacts[randomIndex].phone !== phone) {
randomContact = contacts[randomIndex]
}
}
// this.props.navigation.navigate('ContactDetails', {
// ...randomContact,
// });
this.props.navigation.push('ContactDetails', {
...randomContact,
})
}
}
ContactListScreen.js
import React from 'react'
import {Button, View, StyleSheet} from 'react-native'
import {connect} from 'react-redux'
import SectionListContacts from '../SectionListContacts'
class ContactListScreen extends React.Component {
static navigationOptions = ({navigation}) => ({
headerTitle: 'Contacts',
headerRight: (
<Button title="Add" onPress={() => navigation.navigate('AddContact')} color="#a41034" />
),
})
state = {
showContacts: true,
}
toggleContacts = () => {
this.setState(prevState => ({showContacts: !prevState.showContacts}))
}
handleSelectContact = contact => {
this.props.navigation.push('ContactDetails', contact)
}
render() {
return (
<View style={styles.container}>
<Button title="toggle contacts" onPress={this.toggleContacts} />
{this.state.showContacts && (
<SectionListContacts
contacts={this.props.contacts}
onSelectContact={this.handleSelectContact}
/>
)}
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
})
const mapStateToProps = state => ({
contacts: state.contacts,
})
export default connect(mapStateToProps)(ContactListScreen)
LoginScreen.js
import React from 'react'
import {Button, View, StyleSheet, Text, TextInput} from 'react-native'
import {login} from '../api'
export default class LoginScreen extends React.Component {
state = {
username: '',
password: '',
}
_login = async () => {
try {
const success = await login(this.state.username, this.state.password)
this.props.navigation.navigate('Main')
} catch (err) {
const errMessage = err.message
this.setState({err: errMessage})
}
}
handleUsernameUpdate = username => {
this.setState({username})
}
handlePasswordUpdate = password => {
this.setState({password})
}
render() {
return (
<View style={styles.container}>
<Text style={styles.error}>{this.state.err}</Text>
<TextInput
placeholder="username"
value={this.state.username}
onChangeText={this.handleUsernameUpdate}
autoCapitalize="none"
/>
<TextInput
placeholder="password"
value={this.state.password}
onChangeText={this.handlePasswordUpdate}
secureTextEntry
/>
<Button title="Press to Log In" onPress={this._login} />
</View>
)
}
}
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
flex: 1,
},
text: {
textAlign: 'center',
},
error: {
textAlign: 'center',
color: 'red',
},
})
SettingsScreen.js
import React from 'react'
import {Button, View, StyleSheet, Text} from 'react-native'
import Ionicons from 'react-native-vector-icons/Ionicons'
export default class SettingsScreen extends React.Component {
static navigationOptions = {
tabBarIcon: ({focused, tintColor}) => (
<Ionicons name={`ios-options${focused ? '' : '-outline'}`} size={25} color={tintColor} />
),
}
render() {
return (
<View style={styles.container}>
<Text style={styles.text}>Settings coming soon.</Text>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
flex: 1,
},
text: {
textAlign: 'center',
},
})
(End of Directory)
(Directory : Redux)
Actions.js
// action types
export const UPDATE_USER = 'UPDATE_USER'
export const UPDATE_CONTACT = 'UPDATE_CONTACT'
// action creators
export const updateUser = update => ({
type: UPDATE_USER,
payload: update,
})
export const addContact = newContact => ({
type: UPDATE_CONTACT,
payload: newContact,
})
store.js
import {createStore} from 'redux'
import {addContact} from './actions'
import reducer from './reducer'
const store = createStore(reducer)
/*
store.dispatch(updateUser({foo: 'foo'}))
store.dispatch(updateUser({bar: 'bar'}))
store.dispatch(updateUser({foo: 'baz'}))
*/
store.dispatch(addContact({name: 'jordan h', phone: '1234567890'}))
store.dispatch(addContact({name: 'jordan h', phone: '1234567890'}))
store.dispatch(addContact({name: 'david m', phone: '5050505050'}))
console.log(store.getState())
export default store
reducer.js
import {combineReducers} from 'redux'
import {UPDATE_USER, UPDATE_CONTACT} from './actions'
const merge = (prev, next) => Object.assign({}, prev, next)
const contactReducer = (state = [], action) => {
if (action.type === UPDATE_CONTACT) return [...state, action.payload]
return state
}
const userReducer = (state = {}, action) => {
switch (action.type) {
case UPDATE_USER:
return merge(state, action.payload)
case UPDATE_CONTACT:
return merge(state, {prevContact: action.payload})
default:
return state
}
}
const reducer = combineReducers({
user: userReducer,
contacts: contactReducer,
})
export default reducer
(End of Directory)
Your component code is not the problem: The component who is rendering SectionListContacts is not passing property contacts. Thats why in runtime props.contacts is undefined and the app complains about you cannot use method reduce on it.

How to load contact details into multi select drop down in react native?

I am creating a react native app to load phone book contacts to my app using this library. I loaded contact correctly in my app. Now I wanted to load these contact details in to multi select drop down. I used react-native-multiple-select to load contact using this library. But I am not be able load contact into this library.
The UI that I need to load contact details.
This is what I tried,
import React, {Component} from 'react';
import {
View,
Text,
TouchableOpacity,
FlatList,
ActivityIndicator,
Image,
TextInput,
PermissionsAndroid,
Platform,
Modal,
TouchableHighlight,
Alert,
} from 'react-native';
import ContactsLib from 'react-native-contacts';
import {styles} from '../src/HomeTabs/ContactStyles';
import PropTypes from 'prop-types';
import {Header} from 'react-native-elements';
import MultiSelect from 'react-native-multiple-select';
//Import MultiSelect library
export class Tab2 extends Component {
constructor(props) {
super(props);
this.state = {
contactList: [],
selectedContact: [],
text: '',
isLoading: true,
show: false,
modalVisible: false,
};
this.arrayholder = [];
}
async componentDidMount() {
if (Platform.OS === 'android') {
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.READ_CONTACTS,
{
title: 'App Contact Permission',
message: 'This App needs access to your contacts ',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
},
);
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
this.getListOfContacts();
this.showconsole();
} else {
this.setState({isLoading: false});
this.getOtherContacts();
}
} catch (err) {
this.setState({isLoading: false});
}
} else {
ContactsLib.checkPermission((err, permission) => {
if (permission === 'denied') {
this.setState({isLoading: false});
this.getOtherContacts();
} else {
this.getListOfContacts();
}
});
}
}
// Mics Method
getOtherContacts = () => {
const {otherContactList} = this.props;
const arrFinal = [];
if (otherContactList.length > 0) {
otherContactList.map(listItem => {
arrFinal.push(listItem);
});
}
arrFinal.map((listItem, index) => {
listItem.isSelected = false;
listItem.id = index;
});
this.setState({contactList: arrFinal, isLoading: false});
this.arrayholder = arrFinal;
};
getListOfContacts = () => {
const {otherContactList} = this.props;
const arrFinal = [];
ContactsLib.getAll((err, contacts) => {
if (err) {
throw err;
}
contacts.map(listItem => {
arrFinal.push({
fullname: listItem.givenName + ' ' + listItem.familyName,
phoneNumber:
listItem.phoneNumbers.length > 0
? listItem.phoneNumbers[0].number
: '',
avatar: listItem.thumbnailPath,
});
});
if (otherContactList.length > 0) {
otherContactList.map(listItem => {
arrFinal.push(listItem);
});
}
arrFinal.map((listItem, index) => {
listItem.isSelected = false;
listItem.id = index;
});
this.setState({contactList: arrFinal, isLoading: false});
this.arrayholder = arrFinal;
});
};
getSelectedContacts = () => {
const {selectedContact} = this.state;
return selectedContact;
};
checkContact = item => {
const {onContactSelected, onContactRemove} = this.props;
let arrContact = this.state.contactList;
let arrSelected = this.state.selectedContact;
arrContact.map(listItem => {
if (listItem.id === item.id) {
listItem.isSelected = !item.isSelected;
}
});
if (item.isSelected) {
arrSelected.push(item);
if (onContactSelected) {
onContactSelected(item);
}
} else {
if (onContactRemove) {
onContactRemove(item);
}
arrSelected.splice(arrSelected.indexOf(item), 1);
}
this.setState({contactList: arrContact, selectedContact: arrSelected});
};
checkExist = item => {
const {onContactRemove} = this.props;
let arrContact = this.state.contactList;
let arrSelected = this.state.selectedContact;
arrContact.map(listItem => {
if (listItem.id === item.id) {
listItem.isSelected = false;
}
});
if (onContactRemove) {
onContactRemove(item);
}
arrSelected.splice(arrSelected.indexOf(item), 1);
this.setState({contactList: arrContact, selectedContact: arrSelected});
};
SearchFilterFunction = text => {
let newArr = [];
this.arrayholder.map(function(item) {
const itemData = item.fullname.toUpperCase();
const textData = text.toUpperCase();
if (itemData.indexOf(textData) > -1) {
newArr.push(item);
}
});
this.setState({
contactList: newArr,
text: text,
});
};
//Render Method
_renderItem = ({item}) => {
const {viewCheckMarkStyle} = this.props;
return (
<TouchableOpacity onPress={() => this.checkContact(item)}>
<View style={styles.viewContactList}>
<Image
source={
item.avatar !== ''
? {uri: item.avatar}
: require('../images/user.png')
}
style={styles.imgContactList}
/>
<View style={styles.nameContainer}>
<Text style={styles.txtContactList}>{item.fullname}</Text>
<Text style={styles.txtPhoneNumber}>{item.phoneNumber}</Text>
</View>
{item.isSelected && (
<Image
source={require('../images/check-mark.png')}
style={[styles.viewCheckMarkStyle, viewCheckMarkStyle]}
/>
)}
</View>
</TouchableOpacity>
);
};
state = {
//We will store selected item in this
selectedItems: [],
};
onSelectedItemsChange = selectedItems => {
this.setState({selectedItems});
//Set Selected Items
};
render() {
const {selectedItems} = this.state;
const {searchBgColor, searchPlaceholder, viewSepratorStyle} = this.props;
return (
<View style={styles.container}>
<MultiSelect
hideTags
items={this.contactList}
uniqueKey="id"
ref={component => {
this.multiSelect = component;
}}
onSelectedItemsChange={this.onSelectedItemsChange}
selectedItems={selectedItems}
selectText="Select Contacts"
searchInputPlaceholderText="Search Contacts..."
onChangeInput={text => console.log(text)}
tagRemoveIconColor="#ff0000"
tagBorderColor="#48d22b"
tagTextColor="#000"
selectedItemTextColor="#48d22b"
selectedItemIconColor="#48d22b"
itemTextColor="#000"
displayKey="name"
searchInputStyle={{color: '#48d22b'}}
submitButtonColor="#48d22b"
submitButtonText="Submit"
/>
<View>
{this.multiSelect &&
this.multiSelect.getSelectedItemsExt(selectedItems)}
</View>
{this.state.isLoading && (
<View style={styles.loading}>
<ActivityIndicator animating={true} size="large" color="gray" />
</View>
)}
</View>
);
}
}
Tab2.propTypes = {
otherContactList: PropTypes.array,
viewCloseStyle: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
]),
viewCheckMarkStyle: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
]),
sepratorStyle: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
]),
viewSepratorStyle: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
]),
searchBgColor: PropTypes.string,
searchPlaceholder: PropTypes.string,
onContactSelected: PropTypes.func,
onContactRemove: PropTypes.func,
};
Tab2.defaultProps = {
otherContactList: [],
viewCloseStyle: {},
viewCheckMarkStyle: {},
sepratorStyle: {},
viewSepratorStyle: {},
searchBgColor: 'rgb(202,201,207)',
searchPlaceholder: 'Search...',
onContactSelected: () => {},
onContactRemove: () => {},
};
export default Tab2;
your multiselect should be given the contacts. Try stripping out anything nonessential from your example
...
render() {
...
return (
...
<MultiSelect
items={this.state.contactList}
...
/>
...
);
}

The component for route 'ActivityFeed' must be a React component

I have looked through various similar posts here on SO regarding similar issue, but none of the answers solved it for me.
This is the full error:
So in my src/navigation/feed/stack.js it's being defined like so:
import React from 'react';
import {StackNavigator} from 'react-navigation';
import ActivityFeed from 'activity-feed/session-user/screens/Main';
import HamburgerButton from 'navigation-components/HamburgerButton';
import HeaderTitle from 'navigation-components/HeaderTitle';
import ActionAlertIndicator from 'navigation-components/ActionAlertIndicator';
import * as navConfig from '../config';
import * as cache from 'utils/cache';
const stack = StackNavigator(
{
ActivityFeed: {
screen: ActivityFeed,
navigationOptions: ({navigation}) => ({
header: (
<HeaderTitle
headerLeft={() => (
<HamburgerButton
onPress={() => navigation.navigate('DrawerOpen')}
/>
)}
headerRight={() => (
<ActionAlertIndicator
onPress={() => {
cache.setRouteStarter('MainDrawer');
navigation.navigate('ActionAlertsStack');
}}
/>
)}
/>
),
}),
},
},
{
navigationOptions: {
...navConfig.defaultStackConfig,
},
}
);
export default stack;
The actual component or screen is defined like so inside of src/activity-feed/session-user/screens/Main.js:
import React, {PureComponent} from 'react';
import {
FlatList,
StyleSheet,
AppState,
Platform,
Dimensions,
View,
Alert,
} from 'react-native';
import PropTypes from 'prop-types';
import OneSignal from 'react-native-onesignal';
import {Loading, SwippableCard, BottomAlert} from 'common-components';
import EmptyState from 'activity-feed/session-user/components/EmptyState';
import EventFeedCard from 'events/components/EventFeedCard';
import SurveyBallotFeedCard from 'surveys-ballots/components/FeedCard';
import MicroSurvey from 'surveys-ballots/components/MicroSurvey';
import ActionAlertFeedCard from 'action-alerts/components/ActionAlertFeedCard';
import MissingAddressCard from 'action-alerts/components/MissingAddressCard';
import ArticleFeedCard from 'articles/components/ArticleFeedCard';
import GetInvolvedFeedCard from 'account-settings/components/GetInvolvedFeedCard';
import {connect} from 'react-redux';
import {
fetchFeed,
handleContentSwipe,
undoSwipeAction,
hideUndoAlert,
} from 'activity-feed/actions';
import {setSelectedEvent} from 'events/actions';
import {setSelectedSurvey} from 'surveys-ballots/actions';
import {setSelectedAlert, getCampaignDetails} from 'action-alerts/actions';
import * as cache from 'utils/cache';
import {setSelectedArticle} from 'articles/actions';
import {
handleUpdateTopics,
handleUpdateGetInvoved,
} from 'account-settings/preferencesActions';
import {scale} from 'react-native-size-matters';
import {emptyStateStyles} from 'theme';
const {height} = Dimensions.get('window');
export class ActivityFeed extends PureComponent {
static propTypes = {
displayAlert: PropTypes.bool,
feed: PropTypes.array,
fetchFeed: PropTypes.func,
getCampaignDetails: PropTypes.func,
handleContentSwipe: PropTypes.func,
handleUpdateGetInvoved: PropTypes.func,
handleUpdateTopics: PropTypes.func,
hideUndoAlert: PropTypes.func,
lastSwippedElement: PropTypes.object,
loading: PropTypes.bool,
navigation: PropTypes.object,
setSelectedAlert: PropTypes.func,
setSelectedArticle: PropTypes.func,
setSelectedEvent: PropTypes.func,
setSelectedSurvey: PropTypes.func.isRequired,
undoSwipeAction: PropTypes.func,
userEmailIsValidForVoterVoice: PropTypes.bool,
};
constructor(props) {
super(props);
this.prompted = false;
this.state = {
refreshing: false,
appState: AppState.currentState,
};
}
async componentDidMount() {
AppState.addEventListener('change', this._handleAppStateChange);
if (!this.props.loading) {
const doRefresh = await cache.shouldRefresh('feed');
if (this.props.feed.length === 0 || doRefresh) {
this.props.fetchFeed();
}
cache.incrementAppViews();
}
}
componentWillUnmount() {
AppState.removeEventListener('change', this._handleAppStateChange);
}
_handleAppStateChange = async appState => {
if (
this.state.appState.match(/inactive|background/) &&
appState === 'active'
) {
cache.incrementAppViews();
const doRefresh = await cache.shouldRefresh('feed');
if (doRefresh) {
this.props.fetchFeed();
}
}
this.setState({appState});
};
_keyExtractor = ({Entity}) =>
(Entity.Key || Entity.Id || Entity.CampaignId || Entity.Code).toString();
_gotoEvent = event => {
cache.setRouteStarter('MainDrawer');
this.props.setSelectedEvent(event);
const title = `${event.LegislatureType} Event`;
this.props.navigation.navigate('EventDetails', {title});
};
_gotoSurveyBallot = survey => {
cache.setRouteStarter('MainDrawer');
this.props.setSelectedSurvey(survey);
this.props.navigation.navigate('SurveyDetails');
};
_gotoArticle = article => {
cache.setRouteStarter('MainDrawer');
this.props.setSelectedArticle(article);
this.props.navigation.navigate('ArticleDetails');
};
_onAlertActionButtonPress = async item => {
cache.setRouteStarter('MainDrawer');
await this.props.setSelectedAlert(item.Entity);
this.props.getCampaignDetails();
if (this.props.userEmailIsValidForVoterVoice) {
this.props.navigation.navigate('Questionnaire');
} else {
this.props.navigation.navigate('UnconfirmedEmail');
}
};
_onSwipedOut = (swippedItem, index) => {
this.props.handleContentSwipe(this.props, {swippedItem, index});
};
_handleGetInvolved = (response, entity) => {
if (response !== entity.IsSelected) {
const isTopic = entity.Category !== 'GetInvolved';
const items = [
{
...entity,
IsSelected: response,
},
];
if (isTopic) {
this.props.handleUpdateTopics({topics: items});
} else {
this.props.handleUpdateGetInvoved({involved: items});
}
}
};
renderItem = ({item, index}) => {
const {Type, Entity} = item;
if (Type === 'EVENT') {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<EventFeedCard
style={styles.push}
mainActionButtonPress={() => this._gotoEvent(Entity)}
event={Entity}
/>
</SwippableCard>
);
}
if (['SURVEY_SURVEY', 'SURVEY_BALLOT'].includes(Type)) {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<SurveyBallotFeedCard
style={styles.push}
survey={Entity}
handleViewDetails={() => this._gotoSurveyBallot(Entity)}
/>
</SwippableCard>
);
}
if (Type === 'SURVEY_MICRO') {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<MicroSurvey style={styles.push} selectedSurvey={Entity} />
</SwippableCard>
);
}
if (Type === 'ALERT') {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<ActionAlertFeedCard
datePosted={Entity.StartDateUtc}
style={styles.push}
title={Entity.Headline}
content={Entity.Alert}
mainActionButtonPress={() => this._onAlertActionButtonPress(item)}
secondaryActionButtonPress={() => {
this.props.setSelectedAlert(Entity);
// eslint-disable-next-line
this.props.navigation.navigate("ActionAlertDetails", {
content: Entity.Alert,
id: Entity.CampaignId,
title: Entity.Headline,
});
}}
/>
</SwippableCard>
);
}
if (Type === 'ARTICLE') {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<ArticleFeedCard
content={Entity}
style={styles.push}
mainActionButtonPress={() => this._gotoArticle(Entity)}
/>
</SwippableCard>
);
}
//prettier-ignore
if (Type === 'NOTIFICATION' && Entity.Code === 'INDIVIDUAL_ADDRESS_HOME_MISSING') {
return (
<MissingAddressCard
style={styles.push}
navigate={() => this.props.navigation.navigate('HomeAddress')}
/>
);
}
if (['PREFERENCE_TOPIC', 'PREFERENCE_INVOLVEMENT'].includes(Type)) {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<GetInvolvedFeedCard
style={styles.push}
title={Entity.DisplayText}
onPress={response => this._handleGetInvolved(response, Entity)}
/>
</SwippableCard>
);
}
return null;
};
_onRefresh = async () => {
try {
this.setState({refreshing: true});
this.props
.fetchFeed()
.then(() => {
this.setState({refreshing: false});
})
.catch(() => {
this.setState({refreshing: false});
});
} catch (e) {
this.setState({refreshing: false});
}
};
_trackScroll = async event => {
try {
if (this.prompted) {
return;
}
const y = event.nativeEvent.contentOffset.y;
const scrollHeight = height * 0.8;
const page = Math.round(Math.floor(y) / scrollHeight);
const alert = await cache.shouldPromtpPushNotificationPermissions();
const iOS = Platform.OS === 'ios';
if (alert && iOS && page > 1) {
this.prompted = true;
this._openPromptAlert();
}
} catch (e) {
return false;
}
};
_openPromptAlert = () => {
Alert.alert(
'Push Notifications Access',
'Stay engaged with NFIB on the issues and activities you care about by allowing us to notify you using push notifications',
[
{
text: 'Deny',
onPress: () => {
cache.pushNotificationsPrompted();
},
style: 'cancel',
},
{
text: 'Allow',
onPress: () => {
OneSignal.registerForPushNotifications();
cache.pushNotificationsPrompted();
},
},
],
{cancelable: false}
);
};
_getAlertTitle = () => {
const {lastSwippedElement} = this.props;
const {Type} = lastSwippedElement.swippedItem;
if (Type.startsWith('PREFERENCE')) {
return 'Preference Dismissed';
}
switch (Type) {
case 'EVENT':
return 'Event Dismissed';
case 'SURVEY_BALLOT':
return 'Ballot Dismissed';
case 'SURVEY_SURVEY':
return 'Survey Dismissed';
case 'SURVEY_MICRO':
return 'Micro Survey Dismissed';
case 'ARTICLE':
return 'Article Dismissed';
case 'ALERT':
return 'Action Alert Dismissed';
default:
return 'Dismissed';
}
};
render() {
if (this.props.loading && !this.state.refreshing) {
return <Loading />;
}
const contentStyles =
this.props.feed.length > 0 ? styles.content : emptyStateStyles.container;
return (
<View style={styles.container}>
<FlatList
contentContainerStyle={contentStyles}
showsVerticalScrollIndicator={false}
data={this.props.feed}
renderItem={this.renderItem}
keyExtractor={this._keyExtractor}
removeClippedSubviews={false}
onRefresh={this._onRefresh}
refreshing={this.state.refreshing}
ListEmptyComponent={() => (
<EmptyState navigation={this.props.navigation} />
)}
scrollEventThrottle={100}
onScroll={this._trackScroll}
/>
{this.props.displayAlert && (
<BottomAlert
title={this._getAlertTitle()}
onPress={this.props.undoSwipeAction}
hideAlert={this.props.hideUndoAlert}
/>
)}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
paddingHorizontal: scale(8),
paddingTop: scale(16),
paddingBottom: scale(20),
},
push: {
marginBottom: 16,
},
});
const mapState2Props = ({
activityFeed,
auth: {userEmailIsValidForVoterVoice},
navigation,
}) => {
return {
...activityFeed,
userEmailIsValidForVoterVoice,
loading: activityFeed.loading || navigation.deepLinkLoading,
};
};
export default connect(mapState2Props, {
fetchFeed,
getCampaignDetails,
handleUpdateGetInvoved,
handleUpdateTopics,
setSelectedAlert,
setSelectedArticle,
setSelectedEvent,
setSelectedSurvey,
handleContentSwipe,
undoSwipeAction,
hideUndoAlert,
})(ActivityFeed);
I don't see anything apparent with my code and I am wondering if it's some change that the react-navigation team did.
I am using react-navigation version 1.5.11 with react-native version 0.60.4.
Is this a compatibility issue with the RN version? Do I have no choice but to upgrade?
And this problem seems prevalent throughout my application. I also get the error here:
This is the src/auth/screens/ResetLinkConfirmationAlert.js file:
import React from 'react';
import {connect} from 'react-redux';
import ResetPasswordLinkConfirmationAlert from 'auth/components/ResetPasswordLinkConfirmationAlert';
import PropTypes from 'prop-types';
const ResetLinkConfirmationAlert = ({resetEmail, navigation}) => {
const {params} = navigation.state;
return <ResetPasswordLinkConfirmationAlert email={resetEmail} {...params} />;
};
ResetLinkConfirmationAlert.propTypes = {
navigation: PropTypes.object,
resetEmail: PropTypes.string,
};
const mapStateToProps = ({registrations}) => {
const {resetEmail} = registrations.resetPasswordData;
const email = resetEmail || registrations.verificationEmail;
return {resetEmail: email};
};
export default connect(mapStateToProps)(ResetLinkConfirmationAlert);
and it's being imported here in src/navigation/auth/stack.js:
import React from "react";
import { StackNavigator, NavigationActions } from "react-navigation";
import { Intro } from "auth/screens/Intro";
import { Login } from "auth/screens/Login";
import { PasswordReset } from "auth/screens/PasswordReset";
import { RegisterNoEmail } from "auth/screens/RegisterNoEmail";
import AskForMembership from "auth/screens/AskForMembership";
import { CompleteAccount } from "auth/screens/CompleteAccount";
import { ConfirmMemberAccount } from "auth/screens/ConfirmMemberAccount";
import { Register } from "auth/screens/Register";
import SetNewPassword from "auth/screens/SetNewPassword";
import { RegisterEmailPassword } from "auth/screens/RegisterEmailPassword";
import ResetLinkConfirmationAlert from "auth/screens/ResetLinkConfirmationAlert";
import DetailsConfirmation from "auth/screens/DetailsConfirmation";
import AccountCreated from "auth/screens/AccountCreated";
import BackButton from "navigation-components/BackButton";
import CustomHeader from "navigation-components/CustomHeader";
import HeaderTitle from "navigation-components/HeaderTitle";
import { v2Colors } from "theme";
import { defaultStackConfig, defaultHeaderStyles } from "../config";
const leftRegiterNavOptions = {
title: "Register",
headerStyle: defaultStackConfig.authHeaderStyle
};
const stack = StackNavigator(
{
Intro: {
screen: Intro,
navigationOptions: {
header: null
}
},
Register: {
screen: Register,
navigationOptions: ({ navigation }) => ({
header: <CustomHeader onPress={() => navigation.goBack(null)} />,
headerStyle: defaultStackConfig.authHeaderStyle
})
},
RegisterNoEmail: {
screen: RegisterNoEmail,
navigationOptions: leftRegiterNavOptions
},
RegisterEmailPassword: {
screen: RegisterEmailPassword,
navigationOptions: leftRegiterNavOptions
},
AskForMembership: {
screen: AskForMembership,
navigationOptions: {
header: <HeaderTitle />,
headerStyle: defaultStackConfig.authHeaderStyle
}
},
ConfirmMemberAccount: {
screen: ConfirmMemberAccount,
navigationOptions: ({ navigation }) => ({
header: (
<HeaderTitle
headerLeft={() => (
<BackButton onPress={() => navigation.goBack(null)} />
)}
/>
),
headerStyle: defaultStackConfig.authHeaderStyle
})
},
CompleteAccount: {
screen: CompleteAccount,
navigationOptions: {
header: <HeaderTitle />,
headerStyle: defaultStackConfig.authHeaderStyle
}
},
Login: {
screen: Login,
navigationOptions: ({ navigation }) => ({
title: "Log In",
headerLeft: <BackButton onPress={() => navigation.goBack(null)} />,
headerStyle: defaultStackConfig.authHeaderStyle
})
},
PasswordReset: {
screen: PasswordReset,
navigationOptions: ({ navigation }) => ({
title: "Password Reset",
headerLeft: <BackButton onPress={() => navigation.goBack(null)} />,
headerStyle: defaultStackConfig.authHeaderStyle
})
},
ResetLinkConfirmationAlert: {
screen: ResetLinkConfirmationAlert,
navigationOptions: ({ navigation }) => ({
title: "Password Reset",
headerLeft: (
<BackButton
onPress={() => {
const resetNavAction = NavigationActions.reset({
index: 0,
key: null,
actions: [NavigationActions.navigate({ routeName: "Intro" })]
});
navigation.dispatch(resetNavAction);
}}
/>
),
headerStyle: defaultStackConfig.authHeaderStyle
})
},
Upgrading to react-navigation 2.0.0 is not the answer because I already tried that and if you are going to suggest upgrading to 3.x please explain how that will solve this issue.
It was suggested that in the changelog for react-redux 7.1.0 they mention a note stating PropTypes.func has to be changed to PropTypes.elementType if an element is being passed as a prop
github.com/reduxjs/react-redux/releases/tag/v7.0.1
So in the case where I am getting the error for SetNewPassword, I refactored it like so:
export class CompleteAccount extends PureComponent {
static propTypes = {
loading: PropTypes.bool,
newConfirmResetPassword: PropTypes.string,
newResetPassword: PropTypes.string,
resetUserPassword: PropTypes.elementType.isRequired,
setConfirnResetPassword: PropTypes.elementType.isRequired,
setNewResetPassword: PropTypes.elementType.isRequired,
validationErrors: PropTypes.object
};
and then in navigation/auth/stack.js I added curly braces to the import statement like so:
import { SetNewPassword } from "auth/screens/SetNewPassword";
but I am still getting that error message, although I am not sure if I applied that correctly. At the same time I have noticed that SetNewPassword.js file only has the named export of CompleteAccount:
export class CompleteAccount extends PureComponent {
static propTypes = {
loading: PropTypes.bool,
newConfirmResetPassword: PropTypes.string,
newResetPassword: PropTypes.string,
resetUserPassword: PropTypes.elementType.isRequired,
setConfirnResetPassword: PropTypes.elementType.isRequired,
setNewResetPassword: PropTypes.elementType.isRequired,
validationErrors: PropTypes.object
};
.......
export default connect(
mapStateToProps,
{
resetUserPassword,
setNewResetPassword,
setConfirnResetPassword
}
)(CompleteAccount);
Not sure how this file was working before in that manner. I usually name my files the same name as the class or functional screen and import it with the same name.
On further inspection I see that there are two screens with the same class name function.
CompleteAccount.js:
export class CompleteAccount extends PureComponent {
static propTypes = {
cellPhone: PropTypes.string,
cellPhoneChanged: PropTypes.func.isRequired,
city: PropTypes.string,
cityChanged: PropTypes.func.isRequired,
errors: PropTypes.object,
firstName: PropTypes.string.isRequired,
homeAddress: PropTypes.string,
homeAddressChanged: PropTypes.func.isRequired,
homePhone: PropTypes.string,
homePhoneChanged: PropTypes.func.isRequired,
registeredUser: PropTypes.object,
registerUser: PropTypes.func.isRequired,
state: PropTypes.string,
stateChanged: PropTypes.func.isRequired,
zipCode: PropTypes.string.isRequired,
zipCodeChanged: PropTypes.func.isRequired,
};
which is exported as:
export default connect(mapStateToProps, {
cityChanged,
homeAddressChanged,
homePhoneChanged,
cellPhoneChanged,
stateChanged,
zipCodeChanged,
registerUser,
})(CompleteAccount);
and then there is SetNewPassword.js:
which is also named:
export class CompleteAccount extends PureComponent {
static propTypes = {
loading: PropTypes.bool,
newConfirmResetPassword: PropTypes.string,
newResetPassword: PropTypes.string,
resetUserPassword: PropTypes.func.isRequired,
setConfirnResetPassword: PropTypes.func.isRequired,
setNewResetPassword: PropTypes.func.isRequired,
validationErrors: PropTypes.object
};
.....
export default connect(
mapStateToProps,
{
resetUserPassword,
setNewResetPassword,
setConfirnResetPassword
}
)(CompleteAccount);
even though the file name is completely different. That is confusing, why didn't they just give the second one the class name of SetNewPassword?
The immediate issue looks like the multiple exports in the component files. Try removing the export before the class definition and only keep export default at the end.
Regarding the confusion about SetNewPassword.js and CompleteAccount.js having the same exports, that'll not cause an issue as long you import the default exported component.
To put it simply,
If you export a component as default, then you can import it without the {} curly braces, like
import CompleteAccount from '.../CompleteAccount.js'
Here you can name the import anything you want.
If you use the curly braces, that'll import the named export, like
import {CompleteAccount} from '.../CompleteAccount.js'
After a long grueling 6 days at this, and attempting fixes that went against our understanding of using curly braces when you got named exports, I always suspected that the problem was with react-navigation because I did not mess with the react-navigation version or the codebase.
The problem is how react-navigation works or does not work with react-redux version 7.
React Navigation does not recognize the object returned by React-Redux version 7.
The solution was to downgrade to React-Redux version 5.1.1.

React native - Can't dispatch action in component because state gets undefined

In my react native android app, when I try to dispatch an action in BoardsScreen or in the root of the app, the following error pops up:
However, when I remove it, the app doesn't crashes.
BoardsScreen.js
import React from 'react';
import { connect } from 'react-redux';
import { Container, Content, Text, List, Button, Icon, ListItem } from 'native-base';
import { ListView, StatusBar } from 'react-native';
import { ConfirmDialog } from 'react-native-simple-dialogs';
import ActionButton from 'react-native-action-button';
import { removeBoard } from '../actions/configurationActions';
class BoardsScreen extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
boardDeleteDialog: false,
secId: null,
rowId: null,
rowMap: null,
};
}
deleteRow(secId, rowId, rowMap) {
rowMap[`${secId}${rowId}`].props.closeRow();
const newData = [...this.props.boards];
newData.splice(rowId, 1);
this.props.removeBoard(newData);
this.setState({
rowId: null,
secId: null,
rowMap: null,
boardDeleteDialog: false,
});
}
dataSource = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
render() {
console.log(this.props.boards);
return (
<Container>
<StatusBar backgroundColor="#00C853" />
<ConfirmDialog
title="Delete board?"
animationType="fade"
visible={this.state.boardDeleteDialog}
positiveButton={{
title: 'Delete',
titleStyle: {
color: '#2ecc71',
},
onPress: () => this.deleteRow(this.state.secId, this.state.rowId, this.state.rowMap),
}}
negativeButton={{
titleStyle: {
color: '#2ecc71',
},
title: 'Cancel',
onPress: () =>
this.setState({
boardDeleteDialog: false,
secId: null,
rowId: null,
rowMap: null,
}),
}}
/>
<Content>
{this.props.boards.length >= 1 ? (
<List
style={{ backgroundColor: '#D9534F' }}
dataSource={this.dataSource.cloneWithRows(this.props.boards)}
renderRow={data => (
<ListItem
style={{ paddingLeft: 14, backgroundColor: 'transparent' }}
button
onPress={() =>
this.props.navigation.navigate('Board', {
board: data.board,
boardName: data.boardName,
})
}
>
<Text>{data.boardName}</Text>
</ListItem>
)}
renderRightHiddenRow={(data, secId, rowId, rowMap) => (
<Button
full
danger
onPress={() =>
this.setState({
boardDeleteDialog: true,
secId,
rowId,
rowMap,
})
}
>
<Icon active name="trash" />
</Button>
)}
disableRightSwipe
rightOpenValue={-75}
/>
) : (
<Text>No boards added.</Text>
)}
</Content>
<ActionButton
buttonColor="#2ecc71"
fixNativeFeedbackRadius
onPress={() => this.props.navigation.navigate('AddBoard')}
/>
</Container>
);
}
}
const mapStateToProps = state => ({
boards: state.configurationReducer.boards,
});
const mapDispatchToProps = dispatch => ({
removeBoard: (board) => {
dispatch(removeBoard(board));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(BoardsScreen);
App.js
import React from 'react';
import { connect } from 'react-redux';
import MainNavigator from './src/config/Router';
import { addBoardToList } from './src/actions/configurationActions';
import { Board } from './src/API';
class App extends React.PureComponent {
componentDidMount() {
Board.getList(true).then(response => this.parseDataFromJSONResponse(response));
}
parseDataFromJSONResponse(response) {
for (let i = 0; i < response.length; i += 1) {
this.props.addBoardToList(response[1]);
}
}
render() {
return <MainNavigator />;
}
}
const mapStateToProps = state => ({
boards: state.configurationReducer.boards,
});
const mapDispatchToProps = dispatch => ({
addBoardToList: (board) => {
dispatch(addBoardToList(board));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
configurationReducer.js
const initialState = {
theme: 1,
obscure: false,
boards: [],
boardsList: [],
};
const configurationReducer = (state = initialState, action) => {
let newState = { ...state };
switch (action.type) {
case 'ADD_BOARD':
newState = {
boards: [...state.boards, action.payload],
};
return newState;
case 'REMOVE_BOARD':
newState = {
boards: action.payload,
};
return newState;
case 'ADD_BOARD_TO_LIST':
newState = {
boardsList: [...state.boardsList, action.payload],
};
return newState;
default:
return state;
}
};
export default configurationReducer;
configurationActions.js
function addBoard(board) {
return {
type: 'ADD_BOARD',
payload: board,
};
}
function removeBoard(board) {
return {
type: 'REMOVE_BOARD',
payload: board,
};
}
function addBoardToList(board) {
return {
type: 'ADD_BOARD_TO_LIST',
payload: board,
};
}
export { addBoard, removeBoard, addBoardToList };
I really don't have a clue what is causing this, maybe it's a bug but I don't know if is react-redux fault or react native itself.
When you remove the board, it looks like in you reducer, you return a strange new state:
case 'REMOVE_BOARD':
newState = {
boards: action.payload,
};
return newState;
Should the boards to be an array always? I think you missed something, for example:
boards: state.boards.filter ((it) => it.id !== action.payload.id),