Undefined is not an Object (evaluating 'props.contacts.reduce') - React Native - 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.
Related
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?
expect(jest.fn()).toHaveBeenCalledWith(...expected) in react-native and expo
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",
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),
React Native - Cannot read property [PROPERTY] of undefined (ignite 2, reduxsauce)
I'm having trouble using redux in my react native app. I cannot call an action in my component. I get the following error: This is my AuthRedux.js import { createReducer, createActions } from 'reduxsauce' import Immutable from 'seamless-immutable' const { Types, Creators } = createActions({ login: ['email', 'password'], logout: null }) export const AuthTypes = Types export default Creators export const INITIAL_STATE = Immutable({ isLoggedIn: false, email: null, password: null }) export const userLogin = (state, {email, password}) => { return Object.assign({}, state, { isLoggedIn: true });//state.merge({ isLoggedIn: true, email, password}) } export const userLogout = (state) => { return state.merge({ isLoggedIn: false, email: null, password: null }) } export const reducer = createReducer(INITIAL_STATE, { [Types.USER_LOGIN]: userLogin, [Types.USER_LOGOUT]: userLogout }) And this is my component LoginScreen.js import React, { Component } from 'react' import { ScrollView, Text, KeyboardAvoidingView, TextInput, TouchableOpacity, Button } from 'react-native' import { connect } from 'react-redux' import { AuthActions } from '../Redux/AuthRedux' // Add Actions - replace 'Your' with whatever your reducer is called :) // import YourActions from '../Redux/YourRedux' // Styles import styles from './Styles/LoginScreenStyle' class LoginScreen extends Component { constructor(props) { super(props); this.state = { email: '', password: '', opacity: 1.0, isLoggedIn: false } } render () { return ( <ScrollView style={styles.container}> <KeyboardAvoidingView behavior='position'> <Text>LoginScreen</Text> <TextInput style={{width: 100, backgroundColor: 'red', height: 50, marginTop: 10}} onChangeText={(text) => this.setState({email : text})}/> <TextInput style={{width: 100, backgroundColor: 'yellow', height: 50, marginTop: 10}} onChangeText={(text) => this.setState({password : text})}/> <Button title='Hola' onPress={this.onLogin}/> </KeyboardAvoidingView> </ScrollView> ) } onLogin = () => { console.log(this.state.email); this.setState({opacity: 0.5}) this.props.userLogin(this.state.email, this.state.password); } handleOnPress = () => { this.setState({opacity: 0.5}) } } const mapStateToProps = (state) => { return { isLoggedIn: state.auth.isLoggedIn } } const mapDispatchToProps = (dispatch) => { return { userLogin: (email, password) => dispatch(AuthActions.login(email, password)) } } export default connect(mapStateToProps, mapDispatchToProps)(LoginScreen) I'm trying to call userLogin function from the onPress button which is assigned in mapDispatchToProps. I also have my rootReducer configured like this: const rootReducer = combineReducers({ nav: require('./NavigationRedux').reducer, github: require('./GithubRedux').reducer, search: require('./SearchRedux').reducer, auth: require('./AuthRedux').reducer }) And the store is also given to the Provider in App.js class App extends Component { render () { return ( <Provider store={store}> <RootContainer /> </Provider> ) } } I don't know why login action is not detected.
Instead of import { AuthActions } from '../Redux/AuthRedux', do import AuthActions from '../Redux/AuthRedux', because you are doing export default on the actionCreators which are the ones that you want to import right now. You can also do export const AuthActions = Creators where you are doing export default Creators, and you can keep your import statement the same way you have right now.