React Native Emptying Props - react-native

I have a screen that includes Signin and Signup buttons. When a user taps the SigninButton, I send him to login screen. In the login screen, there are two text inputs which receive email and password from user. If it is successful, user sent to main screen. If not, I throw an error text. But if user go back from navigation bar and come back to the login screen, email input and error message is still showing there. I am using redux, I can not empty the props that hold email and error text. I tried componentWillUnmount, componentWillMount etc. but still can't find the right place to empty these props. This is the code in my login screen;
class LoginScreen extends Component {
componentWillReceiveProps(nextProps) {
this.onAuthComplete(nextProps);
}
onAuthComplete(props) {
if (props.user) {
this.props.navigation.navigate('main');
}
}
render() {
return(
<View style={styles.container}>
<SigninForm />
</View>
);
}
}
const styles = StyleSheet.create({
........
}
});
const mapStateToProps = ({ auth }) => {
const { user, email, error } = auth;
return { user, email, error };
}
export default connect(mapStateToProps, { emailChanged, loginUser })(LoginScreen);
and also this is the code in the reducer;
import ......
const INITIAL_STATE = {
email: '',
password: '',
user: null,
error: '',
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case EMAIL_CHANGED:
return {...state, email: action.payload };
case PASSWORD_CHANGED:
return {...state, password: action.payload };
case LOGIN_USER:
return {...state, error: ''};
case LOGIN_USER_SUCCESS:
return {...state, ...INITIAL_STATE, user: action.payload};
case LOGIN_USER_FAIL:
return {...state, error: 'Authentication Failed', password: ''};
default:
return state;
}
};
And here is the SigninForm code;
import React, { Component } from 'react';
import { View, Dimensions, Text } from 'react-native';
import { connect } from 'react-redux';
import { FormInput, FormLabel } from 'react-native-elements';
import { AuthButton } from './';
import { emailChanged, passwordChanged, loginUser } from '../actions';
const SCREEN_WIDTH = Dimensions.get('window').width;
const SCREEN_HEIGHT = Dimensions.get('window').height;
class SigninForm extends Component {
onEmailChange(text) {
this.props.emailChanged(text);
}
onPasswordChange(text) {
this.props.passwordChanged(text);
}
onButtonPress() {
const { email, password } = this.props;
this.props.loginUser({ email, password });
}
render() {
return(
<View style={styles.viewStyle}>
<FormLabel labelStyle={styles.labelStyle}>Email</FormLabel>
<FormInput
placeholder='Enter your email'
keyboardType="email-address"
containerStyle={styles.containerStyle}
inputStyle={styles.inputStyle}
onChangeText={this.onEmailChange.bind(this)}
value={this.props.email}
/>
<FormLabel labelStyle={styles.labelStyle}>Password</FormLabel>
<FormInput
placeholder='Enter a password'
secureTextEntry={true}
containerStyle={styles.containerStyle}
inputStyle={styles.inputStyle}
onChangeText={this.onPasswordChange.bind(this)}
value={this.props.password}
/>
<Text style={styles.errorTextStyle}>
{this.props.error}
</Text>
<AuthButton
title='Sign In'
backgroundColor='#eb4454'
onPress={this.onButtonPress.bind(this)}
/>
</View>
);
}
}
const styles = {
viewStyle: {
top: SCREEN_HEIGHT * -0.15
},
containerStyle: {
marginBottom: 10,
width: SCREEN_WIDTH * 0.80,
borderBottomColor: '#3c143c'
},
labelStyle: {
color: '#3c143c',
fontFamily: 'System',
fontSize: 20
},
inputStyle: {
color: '#3c143c',
fontFamily: 'System',
fontSize: 20
},
errorTextStyle: {
fontSize: 20,
alignSelf: 'center',
color: 'red'
}
}
const mapStateToProps = ({ auth }) => {
const { email, password, error } = auth;
return { email, password, error };
}
export default connect(mapStateToProps, { emailChanged, passwordChanged, loginUser })(SigninForm);

I did it in componentWillMount method. I called an action to trigger a reducer to update states of the relevant props. Here is code snippet in my component;
import React, { Component } from 'react';
import { View, Dimensions, Text } from 'react-native';
import { connect } from 'react-redux';
import { FormInput, FormLabel } from 'react-native-elements';
import { AuthButton } from './';
import { emailChanged, passwordChanged, loginUser, emptyInput } from '../actions';
const SCREEN_WIDTH = Dimensions.get('window').width;
const SCREEN_HEIGHT = Dimensions.get('window').height;
class SigninForm extends Component {
componentWillMount() {
//used for empty email and error text when user go back and forth
const { email, error } = this.props;
this.props.emptyInput(email, error);
}
onEmailChange(text) {
this.props.emailChanged(text);
}
onPasswordChange(text) {
this.props.passwordChanged(text);
}
onButtonPress() {
const { email, password } = this.props;
this.props.loginUser({ email, password });
}
render() {
console.log("render ediyorum");
return(
<View style={styles.viewStyle}>
<FormLabel labelStyle={styles.labelStyle}>Email</FormLabel>
<FormInput
placeholder='Enter your email'
keyboardType="email-address"
containerStyle={styles.containerStyle}
inputStyle={styles.inputStyle}
onChangeText={this.onEmailChange.bind(this)}
value={this.props.email}
/>
<FormLabel labelStyle={styles.labelStyle}>Password</FormLabel>
<FormInput
placeholder='Enter a password'
secureTextEntry={true}
containerStyle={styles.containerStyle}
inputStyle={styles.inputStyle}
onChangeText={this.onPasswordChange.bind(this)}
value={this.props.password}
/>
<Text style={styles.errorTextStyle}>
{this.props.error}
</Text>
<AuthButton
title='Sign In'
backgroundColor='#eb4454'
onPress={this.onButtonPress.bind(this)}
/>
</View>
);
}
}
const styles = {
viewStyle: {
top: SCREEN_HEIGHT * -0.15
},
containerStyle: {
marginBottom: 10,
width: SCREEN_WIDTH * 0.80,
borderBottomColor: '#3c143c'
},
labelStyle: {
color: '#3c143c',
fontFamily: 'System',
fontSize: 20
},
inputStyle: {
color: '#3c143c',
fontFamily: 'System',
fontSize: 20
},
errorTextStyle: {
fontSize: 20,
alignSelf: 'center',
color: 'red'
}
}
const mapStateToProps = ({ auth }) => {
const { email, password, error } = auth;
return { email, password, error };
}
export default connect(mapStateToProps, { emailChanged, passwordChanged, loginUser, emptyInput })(SigninForm);
This is emptyInput action creator code;
export const emptyInput = (email, error) => {
return(dispatch) => {
dispatch({ type: EMPTY_INPUT });
}
};
And finally the reducer code doing the real job;
import {
EMAIL_CHANGED,
PASSWORD_CHANGED,
CREATE_USER,
CREATE_USER_SUCCESS,
CREATE_USER_FAIL,
LOGIN_USER,
LOGIN_USER_FAIL,
LOGIN_USER_SUCCESS,
EMPTY_INPUT
} from '../actions/types';
const INITIAL_STATE = {
email: '',
password: '',
user: null,
error: '',
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case EMAIL_CHANGED:
return {...state, email: action.payload };
case PASSWORD_CHANGED:
return {...state, password: action.payload };
case CREATE_USER:
return {...state, error: '' };
case CREATE_USER_SUCCESS:
return {...state, ...INITIAL_STATE, user: action.payload};
case CREATE_USER_FAIL:
return {...state, error: 'Authentication Failed!', password: ''};
case LOGIN_USER:
return {...state, error: ''};
case LOGIN_USER_SUCCESS:
return {...state, ...INITIAL_STATE, user: action.payload};
case LOGIN_USER_FAIL:
return {...state, error: 'Authentication Failed', password: ''};
case EMPTY_INPUT:
return {...state, ...INITIAL_STATE};
default:
return state;
}
};

Related

how to show Loader till API get data using redux?

I am new to react and redux. I have implemented API fetching using redux but not sure where should i put code for loader till API gives data and error message if no network or API fails.
everything is working fine I am getting data too..only thing i stick is how to show activity indicator and error message. Is there any way to do that? Thanks in advance :)
reducer.js
export const GET_REPOS = 'my-awesome-app/repos/LOAD';
export const GET_REPOS_SUCCESS = 'my-awesome-app/repos/LOAD_SUCCESS';
export const GET_REPOS_FAIL = 'my-awesome-app/repos/LOAD_FAIL';
const initialState = {
repos: [],
loading: false,
error: null
};
export default function reducer(state = initialState , action) {
switch (action.type) {
case GET_REPOS:
return { ...state, loading: true };
case GET_REPOS_SUCCESS:
return { ...state, loading: false, repos: action.payload.data };
case GET_REPOS_FAIL:
return {
...state,
loading: false,
error: 'Error while fetching repositories',
};
default:
return state;
}
}
export function listRepos(photos) {
return {
type: GET_REPOS,
payload: {
request: {
url: `photos/`
}
}
};
}
export function listThumb(albumId) {
return {
type: GET_REPOS,
payload: {
request: {
url: `photos?albumId=${albumId}`
}
}
};
}
home.js
import React, { Component } from 'react';
import { ActivityIndicator } from 'react-native-paper';
import { View, Text, FlatList, StyleSheet, TouchableOpacity } from 'react-native';
import { connect } from 'react-redux';
import styles from '../HomeComponent/style';
import { Ionicons } from '#expo/vector-icons';
import { listRepos } from '../../../reducer';
class Home extends Component {
componentDidMount() {
this.props.dispatch(fetchProducts());
this.props.listRepos('');
}
FlatListItemSeparator = () => (
<View style={styles.flatListItemSeparator} />
)
renderItem = ({ item }) => (
<View style={styles.listRowContainer}>
<TouchableOpacity onPress={() => this.props.navigation.navigate('ThumbnailViewScreen', {
albumID: item.id,
})} style={styles.listRow}>
<View style={styles.listTextNavVIew}>
<Text style={styles.albumTitle}> {item.title} </Text>
<Ionicons name='md-arrow-dropright' style={styles.detailArrow} />
</View>
</TouchableOpacity>
</View>
);
render() {
const { error, loading, products } = this.props;
if (error) {
return <Text>adadf </Text>;
}
if (loading) {
return (
<View style={{ flex: 1, paddingTop: 30 }}>
<ActivityIndicator animating={true} size='large' />
</View>
);
}
const { repos } = this.props;
return (
<View style={styles.MainContainer} >
<FlatList
styles={styles.container}
data={repos}
renderItem={this.renderItem}
ItemSeparatorComponent={this.FlatListItemSeparator}
/>
</View>
);
}
}
const mapStateToProps = state => {
let storedRepositories = state.repos.map(repo => ({ key: repo.id, ...repo }));
return {
repos: storedRepositories
};
};
const mapDispatchToProps = {
listRepos
};
export default connect(mapStateToProps, mapDispatchToProps)(Home);
const mapStateToProps = state => {
let storedRepositories = state.repos.map(repo => ({ key: repo.id, ...repo }));
return {
repos: storedRepositories
};
};
loading may not have been passed as a prop to Home. To fix that,
add it to mapStateToProps:
return {
repos: storedRepositories,
loading: state.loading
};

invariant violation: element type is invalid React native

I'm trying to import a component into my App.js class but when I try I get the error in my android emulator
invariant violation element type is invalid: expected a string or a class/function but got undefined you likely forgot to export your component from the file its defined in, or you might have mixed up default and named imports
check the render method of 'login'
my problem is that I checked that i'm importing a default import of the login component in my App.js and I am exporting the Login component correctly from what I can tell:
import React, {Component} from 'react';
import {StyleSheet,ScrollView,View,Text,Input,Button,SecureTextEntry,ActivityIndicator} from 'react-native';
import * as firebase from 'firebase';
import auth from '#react-native-firebase/auth';
import {
Header,
LearnMoreLinks,
Colors,
DebugInstructions,
ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';
export default class Login extends Component {
state={
email: '',
password: '',
authenticating: false
};
componentDidMount(){
const firebaseConfig = {
apiKey: 'garbagekey',
authDomain: 'garbage auth domain'
}
firebase.initializeApp(firebaseConfig)
}
onButtonPress = () =>{
console.log("button pressed")
this.setState({authenticating:true})
}
contentBoolRender = () =>{
if(this.state.authenticating===true){
return(
<View>
<ActivityIndicator size="large"/>
</View>
)
}
return(
<View>
<Input
placeholder="Enter your Email..."
label = "Email"
onChangeText = {email => this.setState({email})}
value = {this.state.email}
/>
<Input
placeholder="Enter your Password..."
label = "Password"
onChangeText = {password => this.setState({password})}
value = {this.state.password}
SecureTextEntry
/>
<Button title="login" onpress={()=>this.onButtonPress()}></Button>
</View>
)
}
render() {
return(
<View>
{this.contentBoolRender()}
</View>
);
}
}
const styles = StyleSheet.create({
login:{
padding: 20,
backgroundColor: 'white'
},
});
App.js
import React, {Component} from 'react';
import {
SafeAreaView,
StyleSheet,
ScrollView,
View,
Text,
StatusBar,
Button,
} from 'react-native';
import * as firebase from 'firebase';
import auth from '#react-native-firebase/auth';
import {
Header,
LearnMoreLinks,
Colors,
DebugInstructions,
ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';
import Notes from "./notes.js";
import CreateNote from "./createnote.js";
import Login from "./login.js";
class App extends Component {
state = {
loggedin: false,
notes: [
{
id: 1,
text: "mow the lawn",
author: "dean",
time: "10am"
},
{
id: 2,
text: "feed the dog",
author: "sam",
time: "2pm"
}
]
};
componentDidMount(){
const firebaseConfig = {
apiKey: 'AIzaSyABmRXh2nlBt4FtjfOWNaoz7q5Wy5pGFlc',
authDomain: 'familytodo-28018.firebaseapp.com'
}
firebase.initializeApp(firebaseConfig)
}
confirmLogin=()=>{
this.setState({loggedin:true})
}
updateNotes = (title, name, eventTime) => {
console.log("making new note");
let newNote = {
text: title,
author: name,
time: eventTime
};
this.setState(state => {
const list = state.notes.concat(newNote);
console.log(list);
return {
notes: list
};
});
};
deleteNotes = note => {
console.log("note deleted", note.id);
this.setState(state => {
var list = this.state.notes;
for (let i = 0; i < this.state.notes.length; i++) {
if (this.state.notes[i].id === note.id) {
list.pop(i);
}
}
return {
notes: list
};
});
};
conditionalRender=()=>{
if(this.state.loggedin===false){
return (
<View>
<Login confirmlogin = {this.confirmLogin} />
</View>
)
}
return(
<View>
<CreateNote
handleName={this.handleName}
handleEvent={this.handleEvent}
handleTime={this.handleTime}
updateNotes={this.updateNotes}
/>
<Notes style={styles.notes} notes={this.state.notes} deleteNotes={this.deleteNotes} />
</View>
);
}
render() {
return(
<View>
{this.conditionalRender()}
</View>
);
}
}
const styles = StyleSheet.create({
app: {
marginHorizontal: "auto",
maxWidth: 500
},
logo: {
height: 80
},
header: {
padding: 20
},
title: {
fontWeight: "bold",
fontSize: 15,
marginVertical: 10,
textAlign: "center"
},
notes:{
marginHorizontal: '50%'
},
text: {
lineHeight: 15,
fontSize: 11,
marginVertical: 11,
textAlign: "center"
},
link: {
color: "#1B95E0"
},
code: {
fontFamily: "monospace, monospace"
}
});
export default App;
any help would be appreciate greatly please help me provide better info if possible :)
I think this is happening because react-native has no exported member named Input. I think you are looking for either TextInput (from react-native) or Input (from react-native-elements which has a label prop)
For the TextInput, try changing the login component to this:
import React, {Component} from 'react';
import {ActivityIndicator, Button, TextInput, View} from 'react-native';
import * as firebase from 'firebase';
export default class Login extends Component {
state = {
email: '',
password: '',
authenticating: false
};
componentDidMount() {
const firebaseConfig = {
apiKey: 'garbagekey',
authDomain: 'garbage auth domain'
};
firebase.initializeApp(firebaseConfig);
}
onButtonPress = () => {
console.log("button pressed");
this.setState({authenticating: true});
};
contentBoolRender = () => {
if (this.state.authenticating === true) {
return (
<View>
<ActivityIndicator size="large"/>
</View>
);
}
return (
<View>
<TextInput
placeholder="Enter your Email..."
onChangeText={email => this.setState({email})}
value={this.state.email}
/>
<TextInput
placeholder="Enter your Password..."
onChangeText={password => this.setState({password})}
value={this.state.password}
secureTextEntry
/>
<Button title="login" onPress={() => this.onButtonPress()}/>
</View>
);
};
render() {
return (
<View>
{this.contentBoolRender()}
</View>
);
}
}
or for Input try using this:
import React, {Component} from 'react';
import {ActivityIndicator, Button, View} from 'react-native';
import { Input } from 'react-native-elements';
import * as firebase from 'firebase';
export default class Login extends Component {
state = {
email: '',
password: '',
authenticating: false
};
componentDidMount() {
const firebaseConfig = {
apiKey: 'garbagekey',
authDomain: 'garbage auth domain'
};
firebase.initializeApp(firebaseConfig);
}
onButtonPress = () => {
console.log("button pressed");
this.setState({authenticating: true});
};
contentBoolRender = () => {
if (this.state.authenticating === true) {
return (
<View>
<ActivityIndicator size="large"/>
</View>
);
}
return (
<View>
<Input
placeholder="Enter your Email..."
label = "Email"
onChangeText={email => this.setState({email})}
value={this.state.email}
/>
<Input
placeholder="Enter your Password..."
label = "Password"
onChangeText={password => this.setState({password})}
value={this.state.password}
secureTextEntry
/>
<Button title="login" onPress={() => this.onButtonPress()}/>
</View>
);
};
render() {
return (
<View>
{this.contentBoolRender()}
</View>
);
}
}
Additionally, have a look through the code above as your Login component had a few other typos including: secureTextEntry and onPress

React Redux is not updating the state

I am searching for a couple of days and I can't find out why redux is not updating the state I don't see any problem in my code please help me to find the problem.
it is a simple login project. I can see that data changes inside the reducer when debugging but it's not being mapped to props and state is not changing.
this is my code:
actions.js
import {
LOGIN_SUCCESS,
LOGIN_FAILURE,
} from './types'
export const loginSuccess = (user) => ({
type: LOGIN_SUCCESS,
payload: user
})
export const loginFailure = (error) => ({
type: LOGIN_FAILURE,
payload: error
})
loginReducer.js
import {
LOGIN_SUCCESS,
LOGIN_FAILURE,
} from './types'
const initialState = {
user: null,
errorMessage: null
}
export const loginReducer = (state = initialState, action) => {
switch (action.type) {
case LOGIN_SUCCESS:
return {...state,user: action.payload, errorMessage: null }
case LOGIN_FAILURE:
return {...state,errorMessage: action.payload }
default:
return state;
}
}
loginScreen.js
import React from 'react'
import {
Text,
View,
ImageBackground,
Button,
StyleSheet
} from 'react-native'
import { TextField } from 'react-native-material-textfield';
import { loginSuccess, loginFailure } from './reduxx/actions'
import { connect } from "react-redux";
class LoginScreen extends React.Component {
constructor(props) {
super(props)
this.state = {
email: "",
password: "",
}
}
_handlePress() {
this.props.login(this.state.email, this.state.password)
// user in propa is undifined
this.props.user
}
render() {
let { email, password } = this.state
return (
<ImageBackground source={require('./images/loginBackground.jpg')} style={styles.imageBackground}>
<View style={styles.mainContainer}>
<View style={styles.box}>
<TextField label="Email" value={email} onChangeText={email => this.setState({ email })} />
<TextField label="Password" textContentType="password" value={password} onChangeText={password => this.setState({ password })} />
<Button onPress={() => {
this._handlePress()
}} color="green" title="Sign in" style={styles.button} />
</View>
<View style={styles.bottomTextContainer}>
<Text style={{ color: "white" }}>Don't have an account?</Text>
<Text style={{ color: "lightgreen" }} onPress={() => this.props.navigation.navigate("Register")}> Sign up</Text>
</View>
</View>
</ImageBackground>
)
}
}
function mapStateToProps(state) {
return {
user: state.user,
errorMessage: state.errorMessage
}
}
function mapDispatchToProps(dispatch) {
return {
login: (email, password) => {
try {
let user = //get user from database
dispatch(loginSuccess(user))
} catch (error) {
dispatch(loginFailure(error))
}
},
}
}
export default connect(mapStateToProps, mapDispatchToProps)(LoginScreen);
const styles = StyleSheet.create({
imageBackground: {
width: '100%',
height: '100%'
},
mainContainer: {
flex: 1,
justifyContent: "center"
},
box: {
backgroundColor: 'white',
padding: 8,
margin: 8
},
button: {
margin: 10
},
bottomTextContainer: {
position: "absolute",
bottom: 8,
alignSelf: "center",
flexDirection: "row"
}
});
app.js
import React from 'react';
import { Provider } from "react-redux"
import store from "./reduxx/store";
import AppContainer from './Navigator'
export default class App extends React.Component {
render() {
return (
<Provider store={store} >
<AppContainer />
</Provider>
);
}
}
store.js
import {createStore} from "redux";
import { combineReducers } from 'redux';
import {loginReducer} from './loginReducer'
const rootReducer = combineReducers({
loginReducer,
});
export default store = createStore(rootReducer)
The state which comes into mapStateToProps contains all the combined reducers, so we need to access the reducer name first from the state (like state.loginReducer.user), before trying to access the data of that reducer.
PFB code which should work:
function mapStateToProps(state) {
return {
user: state.loginReducer.user,
errorMessage: state.loginReducer.errorMessage
}
}
Change here :
function mapStateToProps(state) {
return {
user: state.user,
errorMessage: state.errorMessage
}
}
TO
function mapStateToProps(state) {
return {
login: state.loginReducer
}
}
And then this.props.user to this.props.login.user and this.props.errorMessage to this.props.login.errorMessage in all the occurrence.

Check if function is dispatching action with jest and enzyme

I have a Login Screen that has a method that when called dispatches an action. I want to test using jest and enzyme if when the button is pressed, this function is called and, therefore, the action is dispatched. I tried in many different ways, but I couldn't achieve this.
screens/login.js
import React, { Component } from 'react';
import {
KeyboardAvoidingView,
TextInput,
StyleSheet,
Text,
Button,
TouchableOpacity
} from 'react-native';
import { connect } from 'react-redux';
import { login } from 'actions/sessions.js';
export class LoginScreen extends Component {
constructor(props){
super(props);
this.state = {
email: '',
password: '',
error: ''
}
}
requestLogin = async () => {
if(this.checkFields()){
this.setState({error: ''});
this.props.loginRequest(this.state.email, this.state.password);
}
}
checkFields = () => {
let { email, password } = this.state;
const re = /^(([^<>()\[\]\\.,;:\s#"]+(\.[^<>()\[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if(!re.test(email)){
this.setState({error: 'E-mail must be valid'})
return false;
}else if(email == '' || password == ''){
this.setState({error: 'All fields are required!'});
return false
}else{
return true;
}
}
handleEmailText = (email) => {
this.setState({email})
}
handlePasswordText = (password) => {
this.setState({password})
}
render(){
return(
<KeyboardAvoidingView style={styles.container} enabled={true} behavior="padding">
<Text>{this.state.error || this.props.error}</Text>
<TextInput
onChangeText={(e) => this.handleEmailText(e)}
value={this.state.email}
keyboardType="email-address"
textContentType="emailAddress"
autoCapitalize="none"
placeholder="E-mail"
style={styles.input}
/>
<TextInput
onChangeText={(e) => this.handleEmailPassword(e)}
value={this.state.password}
placeholder="Password"
textContentType="password"
autoCapitalize="none"
secureTextEntry={true}
style={styles.input}
/>
<Button style={styles.button}
title="Sign In"
onPress={() => this.requestLogin()}/>
</KeyboardAvoidingView>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
input: {
width: "50%",
height: 40,
borderColor: 'gray',
borderWidth: 1,
paddingLeft: 20,
paddingRight: 20,
margin: 10
}
})
const mapDispatchToProps = dispatch => {
return {
loginRequest: (email, password) => dispatch(login(email, password))
}
}
const mapStateToProps = state => {
return {
error: state.sessions.error
}
}
export default connect(mapStateToProps, mapDispatchToProps)(LoginScreen);
tests/screens/login.test.js
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import renderer from 'react-test-renderer';
import configureStore from 'redux-mock-store';
import { LoginScreen } from 'screens/Login';
describe('LoginScreen', () => {
it('should dispatch login action when button clicked', async () => {
const mockStore = configureStore();
const initialState = {};
const store = mockStore(initialState);
const wrapper = shallow(<LoginScreen store={store}/>)
wrapper.setState({email: 'foo#bar.com', password: '1234'})
const component = await wrapper.dive();
component.find('button').simulate('click');
expect(store.getActions()).toMatchSnapshot();
});
})
When I have this approach, it says that simulate is meant to be run on 1 node. 0 found instead. I have no clue how to test it.
In react native you can use:
component.find('Button').props().onPress();
to simulate user interaction.
Also, you should use Button instead of button (your component name).

Could not navigate to another screen when using token in React Native

I'm currently developing an app using react native, right now my issue is that i couldn't navigate to main screen after login. Below is my code.
This is App.js (EDITED)
import React from 'react';
import { Loading } from './components/common/';
import TabNavigator from './screens/TabNavigator';
import AuthNavigator from './screens/AuthNavigator';
import MainNavigator from './screens/MainNavigator';
import deviceStorage from './services/deviceStorage.js';
import { View, StyleSheet } from 'react-native';
export default class App extends React.Component {
constructor() {
super();
this.state = {
token: '',
loading: true
}
this.newJWT = this.newJWT.bind(this);
this.deleteJWT = deviceStorage.deleteJWT.bind(this);
this.loadJWT = deviceStorage.loadJWT.bind(this);
this.loadJWT();
}
state = {
isLoadingComplete: false,
};
newJWT(token){
this.setState({
token: token
});
}
render() {
if (this.state.loading) {
return (
<Loading size={'large'} />
);
} else if (!this.state.token) {
return (
<View style={styles.container}>
<AuthNavigator screenProps = {{setToken:this.newJWT}} />
</View>
);
} else if (this.state.token) {
return (
<View style={styles.container}>
<MainNavigator screenProps = {{token: this.state.token,
deleteJWT:this.deleteJWT,}} />
</View>
);
}
}
}
This is Login.js (EDITED-v2)
import React, { Component, Fragment } from 'react';
import { Text, View, StyleSheet, ImageBackground, KeyboardAvoidingView,
TouchableOpacity, TextInput, Alert } from 'react-native';
import axios from 'axios';
import deviceStorage from '../services/deviceStorage';
class Login extends Component {
constructor(props) {
super(props)
this.state = {
username: '',
password: '',
error: '',
loading: false
};
this.loginUser = this.loginUser.bind(this);
this.onLoginFail = this.onLoginFail.bind(this);
}
loginUser() {
const { username, password, password_confirmation } = this.state;
this.setState({ error: '', loading: true });
// NOTE Post to HTTPS only in production
axios.post("http://192.168.1.201:8000/api/login",{
username: username,
password: password
})
.then((response) => {
console.log('response',response)
deviceStorage.saveKey("token", response.data.token);
console.log(response.data.token);
this.props.newJWT(response.data.token);
})
.catch((error) => {
const status = error.response.status
if (status === 401) {
this.setState({ error: 'username or password not recognised.' });
}
this.onLoginFail();
//console.log(error);
//this.onLoginFail();
});
}
onLoginFail() {
this.setState({
error: 'Login Failed',
loading: false
});
}
render() {
// other codes here
}
const styles = StyleSheet.create({
// other codes here
});
export { Login };
This is TabNavigator.js (Added)
import React from 'react';
import { Text } from 'react-native';
import { createMaterialTopTabNavigator } from 'react-navigation';
import Ionicons from 'react-native-vector-icons/Ionicons';
import Profile from '../screens/Profile';
const TabNavigator = createMaterialTopTabNavigator(
{
Profile: {
screen: props => <Profile {...props.screenProps} />,
navigationOptions: {
tabBarIcon: ({ tintColor, focused }) => (
<Ionicons
name={focused ? 'ios-person' : 'ios-person'} //TODO change to focused
icon
size={30}
style={{ color: tintColor }}
/>
),
}
},
},
{ initialRouteName: 'Profile',
tabBarPosition: 'top',
swipeEnabled: false,
animationEnabled: true,
lazy: true,
tabBarOptions: {
showLabel: false,
showIcon: true,
activeTintColor: 'orange',
inactiveTintColor: 'orange',
style: {
backgroundColor: '#555',
},
indicatorStyle: {
color: '#orange'
}
}
}
);
const screenTitles = {
Profile: { title: 'Profiler' },
Home: { title: 'Home' },
};
TabNavigator.navigationOptions = ({ navigation }) => {
const { routeName } = navigation.state.routes[navigation.state.index];
const headerTitle = screenTitles[routeName].title;
const tabBarVisible = false;
return {
headerTitle,
tabBarVisible
};
};
export default TabNavigator;
This is my AuthLoadingScreen.js
import React from 'react';
import { View } from 'react-native';
import { Login } from '../screens/Login';
class AuthLoadingScreen extends React.Component {
constructor(props){
super(props);
this.state = {
showLogin: true
};
this.whichForm = this.whichForm.bind(this);
this.authSwitch = this.authSwitch.bind(this);
}
authSwitch() {
this.setState({
showLogin: !this.state.showLogin
});
}
whichForm() {
if(this.state.showLogin){
return(
<Login newJWT={this.props.newJWT} authSwitch={this.authSwitch} />
);
} else {
}
}
render() {
return(
<View style={styles.container}>
{this.whichForm()}
</View>
);
}
}
export default AuthLoadingScreen;
const styles = {
// style codes here
};
Lastly, this is my Profile.js
import React, { Component } from 'react';
import { View, Text, TouchableOpacity, Alert, Platform } from
'react-native';
import { Button, Loading } from '../components/common/';
import axios from 'axios';
export default class Profile extends Component {
constructor(props){
super(props);
this.state = {
loading: true,
email: '',
name: '',
error: ''
}
}
componentDidMount(){
this.onLocationPressed();
const headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.props.token
};
axios({
method: 'GET',
url: 'http://192.168.1.201:8000/api/user',
headers: headers,
}).then((response) => {
console.log('response',response)
console.log('response2',this.props.token)
this.setState({
email: response.data.user.email,
name: response.data.user.name,
loading: false
});
}).catch((error) => {
console.log(error);
this.setState({
error: 'Error retrieving data',
loading: false
});
});
}
render() {
const { container, emailText, errorText } = styles;
const { loading, email, name, error } = this.state;
if (loading){
return(
<View style={container}>
<Loading size={'large'} />
</View>
)
} else {
return(
<View style={container}>
<View>
<Text style={emailText}>Your email: {email}</Text>
<Text style={emailText}>Your name: {name}</Text>
</View>
<Button onPress={this.props.deleteJWT}>
Log Out
</Button>
</View>
);
}
}
}
const styles = {
// style codes here
};
I've fixed the previous problem that couldn't start the app. Right now i can see the login screen, but when i pressed login, there's a yellow box that indicates some problem. I've included the screenshot below.
Lastly i've added the deviceStorage.js
deviceStorage.js
import { AsyncStorage } from 'react-native';
const deviceStorage = {
async saveKey(key, valueToSave) {
try {
await AsyncStorage.setItem(key, valueToSave);
} catch (error) {
console.log('AsyncStorage Error: ' + error.message);
}
},
async loadJWT() {
try {
const value = await AsyncStorage.getItem('token');
if (value !== null) {
this.setState({
token: value,
loading: false
});
} else {
this.setState({
loading: false
});
}
} catch (error) {
console.log('AsyncStorage Error: ' + error.message);
}
},
async deleteJWT() {
try{
await AsyncStorage.removeItem('token')
.then(
() => {
this.setState({
token: ''
})
}
);
} catch (error) {
console.log('AsyncStorage Error: ' + error.message);
}
}
};
export default deviceStorage;
Before navigate
After navigate
This is my setup. It works like a charm. Sorry if it's a bit messy. I removed some stuff for clarity and I may have missed something:
App.js
import React from 'react';
import { Platform, StatusBar, StyleSheet, View } from 'react-native';
import { Asset, Font, Icon } from 'expo';
import { ENDPOINT, USER_TYPE } from './src/config'
import { Loading } from './src/components/common/';
import deviceStorage from './src/services/deviceStorage.js';
import TabNavigator from './src/TabNavigator';
import AuthNavigator from './src/AuthNavigator';
import MainNavigator from './src/MainNavigator';
import globalStyles from './src/globalStyles';
import './ReactotronConfig';
export default class App extends React.Component {
constructor() {
super();
this.state = {
jwt: '',
loading: true,
};
this.newJWT = this.newJWT.bind(this);
this.deleteJWT = deviceStorage.deleteJWT.bind(this);
this.loadJWT = deviceStorage.loadJWT.bind(this);
this.loadJWT();
}
state = {
isLoadingComplete: false,
};
newJWT(jwt) {
this.setState({
jwt: jwt
});
}
render() {
if (this.state.loading) {
return (
<Loading size={'large'} />
);
} else if (!this.state.jwt) {
//console.log(this.props, '<=== app.js');
return (
<View style={styles.container}>
{Platform.OS === 'ios' && <StatusBar barStyle="default" />}
<AuthNavigator screenProps={{setToken: this.newJWT }} />
</View>
);
} else {
return (
<View style={styles.container}>
{Platform.OS === 'ios' && <StatusBar barStyle="default" />}
<MainNavigator
screenProps={{ jwt: this.state.jwt,
deleteToken: this.deleteJWT,
}}
/>
</View>
);
}
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
justifyContent: 'center',
},
});
AuthNavigator.js
import React from 'react';
import { createAppContainer, createBottomTabNavigator } from 'react-navigation';
import Ionicons from 'react-native-vector-icons/Ionicons';
import AuthScreen from './screens/AuthScreen';
const AuthNavigator = createBottomTabNavigator(
{
Auth: (props) => {
return <AuthScreen {...props.screenProps} />;
}
},
{ initialRouteName: 'Auth',
tabBarOptions: {
showLabel: false,
activeBackgroundColor: '#eee',
}
}
);
export default createAppContainer(AuthNavigator);
MainNavigator.js
import React from 'react';
import { createStackNavigator, createAppContainer } from 'react-navigation';
import TabNavigator from './TabNavigator';
const MainNavigator = createStackNavigator({
Main: TabNavigator },
{
initialRouteName: 'Main',
defaultNavigationOptions: {
headerTitleStyle: {
fontSize: 20,
textTransform: 'uppercase'
}
}
});
export default createAppContainer(MainNavigator);
TabNavigator.js
import React from 'react';
import { Text } from 'react-native';
import { createMaterialTopTabNavigator } from 'react-navigation';
import Ionicons from 'react-native-vector-icons/Ionicons';
import IconBadge from 'react-native-icon-badge';
import ProfileScreen from './screens/ProfileScreen';
import NotificationsScreen from './screens/NotificationsScreen';
import HomeStackNavigator from './HomeStackNavigator';
import CartStackNavigator from './CartStackNavigator';
import QuotesStackNavigator from './QuotesStackNavigator';
import InitialRoute from './InitialRoute';
const TabNavigator = createMaterialTopTabNavigator(
{
Profile: {
screen: props => <ProfileScreen {...props.screenProps} />,
navigationOptions: {
tabBarIcon: ({ tintColor, focused }) => (
<Ionicons
name={focused ? 'ios-person' : 'ios-person'} //TODO change to focused icon
size={30}
style={{ color: tintColor }}
/>
),
}
},
Home: HomeStackNavigator,
Quotes: QuotesStackNavigator,
Notifications: { screen: props => <NotificationsScreen {...props.screenProps} />,
navigationOptions: ({ screenProps }) => ({
tabBarIcon: ({ tintColor, focused }) => (
<IconBadge
MainElement={
<Ionicons
name={focused ? 'ios-notifications' : 'ios-notifications'}
size={30}
style={{ color: tintColor }}
/>
}
BadgeElement={
<Text style={{ color: '#FFFFFF' }}>{screenProps.unreadMessagesCount}</Text>
}
IconBadgeStyle={{ width: 15,
height: 15,
position: 'absolute',
top: 1,
left: -6,
marginLeft: 15,
backgroundColor: 'red' }}
Hidden={screenProps.unreadMessagesCount === 0}
/>
)
})
},
Cart: CartStackNavigator,
},
{ initialRouteName: 'Profile',
tabBarPosition: 'top',
swipeEnabled: false,
animationEnabled: true,
lazy: true,
tabBarOptions: {
showLabel: false,
showIcon: true,
activeTintColor: 'orange',
inactiveTintColor: 'orange',
style: {
backgroundColor: '#555',
},
indicatorStyle: {
color: '#orange'
}
}
}
);
const screenTitles = {
Profile: { title: 'Hola Maestro' },
Home: { title: 'Selecciona la Categoría' },
Quotes: { title: 'Mi Historial de Cotizaciones' },
Notifications: { title: 'Notificaciones' },
Cart: { title: 'Mi Pedido' },
};
TabNavigator.navigationOptions = ({ navigation }) => {
const { routeName } = navigation.state.routes[navigation.state.index];
const headerTitle = screenTitles[routeName].title;
const tabBarVisible = false;
return {
headerTitle,
tabBarVisible
};
};
export default TabNavigator;
Login.js
import React, { Component, Fragment } from 'react';
import { Text, View, StyleSheet, ImageBackground, KeyboardAvoidingView } from 'react-native';
import axios from 'axios';
import Ionicons from 'react-native-vector-icons/Ionicons';
//import Pusher from 'pusher-js/react-native';
import { ENDPOINT, USER_TYPE } from '../config'
import deviceStorage from '../services/deviceStorage';
import { Input, TextLink, Loading, Button } from './common';
import Header from '../components/Header';
class Login extends Component {
constructor(props){
super(props);
this.state = {
username: '',
password: '',
error: '',
loading: false
};
this.pusher = null; // variable for storing the Pusher reference
this.my_channel = null; // variable for storing the channel assigned to this user
this.loginUser = this.loginUser.bind(this);
}
loginUser() {
const { username, password, password_confirmation } = this.state;
axios.post(`${ENDPOINT}/login`, {
user: {
login: username,
password: password
}
})
.then((response) => {
deviceStorage.saveKey("id_token", response.headers.authorization);
this.props.newJWT(response.headers.authorization);
//this.setPusherData();
})
.catch((error) => {
this.onLoginFail();
});
}
onLoginFail() {
this.setState({
error: 'Usuario o contraseña incorrecta',
loading: false
});
}
}
render() {
const { username, password, error, loading } = this.state;
const { container, form, section, errorTextStyle, iconContainer, inputContainer, titleText } = styles;
return (
<View style={container}>
<Header title="¡Bienvenido Amigo Maestro!" />
<View style={form}>
<ImageBackground source={require('./cemento-login.jpg')} style={{ flex: 1, marginBottom: 30 }}>
<View style={{marginTop: 120}}>
<Text style={titleText}>INICIAR SESIÓN</Text>
<View style={section}>
<View style={iconContainer}>
<Ionicons
name={'ios-person'}
size={26}
style={{ color: '#fff', alignSelf: 'center' }}
/>
</View>
<View style={inputContainer}>
<Input
placeholder="Usuario"
value={username}
onChangeText={username => this.setState({ username })}
/>
</View>
</View>
<View style={section}>
<View style={iconContainer}>
<Ionicons
name={'ios-lock'}
size={26}
style={{ color: '#fff', alignSelf: 'center' }}
/>
</View>
<View style={inputContainer}>
<Input
secureTextEntry
placeholder="Contraseña"
value={password}
onChangeText={password => this.setState({ password })}
/>
</View>
</View>
</View>
</ImageBackground>
</View>
<KeyboardAvoidingView
behavior="padding"
keyboardVerticalOffset={30}
>
<TextLink style={{ }} onPress={this.props.formSwitch}>
Aún no estas registrado? Regístrate
</TextLink>
<TextLink style={{ }} onPress={this.props.forgotPassword}>
Olvidaste tu contraseña?
</TextLink>
<Text style={errorTextStyle}>
{error}
</Text>
{!loading ?
<Button onPress={this.loginUser}>
Ingresar
</Button>
:
<Loading size={'large'} />}
</KeyboardAvoidingView>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
form: {
flex: 0.8
},
section: {
flexDirection: 'row',
backgroundColor: '#eee',
borderRadius: 3,
marginTop: 10,
height: 40,
marginLeft: '10%',
marginRight: '10%',
},
titleText: {
color: '#fff',
alignSelf: 'center',
fontSize: 20,
marginBottom: 10
},
errorTextStyle: {
alignSelf: 'center',
fontSize: 18,
color: 'red'
},
iconContainer: {
flex: 0.1,
height: 40,
borderRadius: 3,
alignSelf: 'center',
justifyContent: 'center',
backgroundColor: 'orange',
},
inputContainer: {
flex: 0.8,
alignSelf: 'flex-start',
marginLeft: -70,
}
});
export { Login };
Inside App.js you never change loading to false so the render method never gets to any of the other conditions. Once you have received the token you need to update state and change loading.