I'm working on a React Native project with Apollo Client and GraphQL as the backend. The problem I'm having is when I register a new user using a mutation, I get back a positive response that the user has been created, but I also get thrown an error saying Error caught: TypeError: Cannot read property 'variables' of undefined
I'm not sure what the problem is, or why the error is being returned at all. Here's the code:
'use strict'
import React, { Component } from 'react';
import { Text, View, StyleSheet, AsyncStorage, Modal, TouchableHighlight } from 'react-native'
import { Actions, ActionConst } from 'react-native-router-flux'
import { Button } from 'react-native-elements'
// Apollo and GraphQL Packages
import ApolloClient, { graphql, withApollo } from 'react-apollo'
import gql from 'graphql-tag'
import { filter } from 'graphql-anywhere'
import { connect } from 'react-redux'
import Auth0Lock from 'react-native-lock'
// Styling Packages
import LinearGradient from 'react-native-linear-gradient'
import EStyleSheet from 'react-native-extended-stylesheet'
// View Packages
import ViewContainer from './ViewContainer'
// Auth0 Credentials
const clientId = "XXXX"
const domain = "XXXX"
// Props Declaration
type Props = {
client: ApolloClient,
createUser: ({
variables: {
idToken: string,
name: ?string,
email: ?string
}
}) => { id: string}
}
class Login extends Component {
_lock: Auth0Lock
props: Props
constructor(props) {
super(props)
this._lock = new Auth0Lock({
clientId: clientId,
domain: domain,
useBrowser: true
})
}
_showLogin() {
this._lock.show({
closable: true,
connections: ['Username-Password-Authentication']
}, async (err, profile, token) => {
if (!err) {
AsyncStorage.setItem('token', token.idToken)
.then(() => {
this.props.client.resetStore()
})
this.props.createUser({
variables: {
idToken: token.idToken,
name: profile.name
email: profile.email
}
})
.then((data) => {
console.log("Data received: " + data)
Actions.registration({ type: ActionConst.REPLACE })
})
.catch((err) => {
console.log("Error caught: " + err)
AsyncStorage.removeItem('token')
.then(() => this.props.client.resetStore())
// Actions.home({ type: ActionConst.REPLACE })
})
} else {
console.log(err)
}
})
}
render() {
return(
<LinearGradient colors={['#34E89E', '#0F3443']} style={styles.linearGradient}>
<ViewContainer style={styles.viewContainer}>
<Button
small
raised
buttonStyle= {styles.button}
color="#fff"
title="Login"
onPress={() => this._showLogin()} />
</ViewContainer>
</LinearGradient>
)
}
}
const styles = EStyleSheet.create({
viewContainer: {
flex: 1,
paddingTop: 70,
paddingLeft: 20,
paddingRight: 20
},
linearGradient: {
height: '$height',
},
logo: {
fontFamily: 'LiberatorHeavy',
fontSize: 52,
alignSelf: 'center',
marginBottom: 400,
color: 'white',
backgroundColor: 'transparent'
},
button: {
backgroundColor: '#222222'
}
});
const createUserMutation = gql`
mutation createUser($idToken: String!, $name: String, $email: String) {
createUser(
authProvider: {
auth0: {
idToken: $idToken
}
},
name: $name,
email: $email
){
id
}
}`
export default withApollo(
graphql(createUserMutation,
{ name: 'createUser' }
)(Login))
So the error was that the
createUser({variables...})
function was firing twice which resulted in the error being thrown once the variables were cleared. I changed my code to:
_showLogin() {
this._lock.show({
closable: true,
connections: ['Username-Password-Authentication']
}, async (err, profile, token) => {
if (!err) {
AsyncStorage.setItem('token', token.idToken)
.then(() => {
this.props.client.resetStore()
// Create user function was put in to AsyncStorage function to stop function from running twice. (Not sure why that was happening)
this.props.createUser({
variables: {
idToken: token.idToken,
name: profile.name,
email: profile.email
}
})
.then((data) => {
console.log("Data received: " + data)
Actions.registration({ type: ActionConst.REPLACE })
})
.catch((err) => {
console.log("Error caught: " + err)
AsyncStorage.removeItem('token')
.then(() => this.props.client.resetStore())
// Actions.home({ type: ActionConst.REPLACE })
})
})
} else {
console.log(err)
}
This solved the problem and I was getting the proper response, although I'm still not sure why it was running twice after the Async function.
If anyone has any ideas, please let me know!
Related
I've been following along with the tutorial, and i had been having some issues, but i was able to solve all of them on my own - but now I've come to this point. i ran the "meteor remove insecure" and i was pretty sure i updated my tasks.js correctly to reflect my meteor methods. i changed the import on my main.js and TaskForm.jsx and App.jsx
EDIT
**
The error i am receiving does not show up in vsCode, the error only shows in the console. but, interestingly, if you look at my methods, you see the warning message is supposed to say "Not Authorized", however the warning that appears in the console says "Update failed: Access denied"
MOST of my variables are named exactly the same as in the tutorial, some are not... and that is probably adding a layer of confusion on top of the learning process... for example i have Task, Tasks, tasksList, and taskList, are all different variables... i am aware i should make those more legible, just trying to make it "work" for now.
tasks.js:
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';
export const Tasks = new Mongo.Collection('taskList');
Meteor.methods({
'taskList.insert'(text) {
check(text, String);
if (!this.userId) {
throw new Meteor.Error('Not authorized.');
}
Tasks.insert({
text,
createdAt: new Date,
owner: this.userId,
username: Meteor.users.findOne(this.userId).username
})
},
'taskList.remove'(taskId) {
check(taskId, String);
if (!this.userId) {
throw new Meteor.Error('Not authorized.');
}
Tasks.remove(taskId);
},
'taskList.setChecked'(taskId, isChecked) {
check(taskId, String);
check(isChecked, Boolean);
if (!this.userId) {
throw new Meteor.Error('Not authorized.');
}
Tasks.update(taskId, {
$set: {
isChecked
}
});
}
});
App.jsx:
import React, { useState } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import { Task } from './Task';
import { Tasks } from '/imports/api/tasks';
import { TaskForm } from './TaskForm';
import { LoginForm } from './LoginForm';
const toggleChecked = ({ _id, isChecked }) => {
Tasks.update(_id, {
$set: {
isChecked: !isChecked
}
})
};
const deleteTask = ({ _id }) => Tasks.remove(_id);
const logoutFunction = (e) => {
Meteor.logout(e)
}
export const App = () => {
const filter = {};
const [hideCompleted, setHideCompleted] = useState(false);
if (hideCompleted) {
_.set(filter, 'isChecked', false);
}
const { tasksList, incompleteTasksCount, user } = useTracker(() => ({
tasksList: Tasks.find(filter, { sort: { createdAt: -1 } }).fetch(),
incompleteTasksCount: Tasks.find({ isChecked: { $ne: true }}).count(),
user: Meteor.user(),
}));
if (!user) {
return (
<div className="simple-todos-react">
<LoginForm/>
</div>
);
}
return (
<div className="simple-todos-react">
<button onClick ={logoutFunction}>Log Out</button>
<h1>Flight List ({ incompleteTasksCount })</h1>
<div className="filters">
<label>
<input
type="checkbox"
readOnly
checked={ Boolean(hideCompleted) }
onClick={() => setHideCompleted(!hideCompleted)}
/>
Hide Completed
</label>
</div>
<ul className="tasks">
{ tasksList.map(task1 => <Task
key={ task1._id }
task={ task1 }
onCheckboxClick={toggleChecked}
onDeleteClick={deleteTask}/>) }
</ul>
<TaskForm user={user}/>
</div>
);
};
TaskForm.jsx:
import React, { useState } from 'react';
import { Tasks } from '/imports/api/tasks';
export const TaskForm = ({ user }) => {
const [text, setText] = useState("");
const handleSubmit = () => {
if (!text) return;
Tasks.insert({
text: text.trim(),
createdAt: new Date(),
isChecked: false,
owner: user._id,
});
setText("");
};
return (
<form className="task-form" onSubmit={handleSubmit}>
<input
type="text"
placeholder="Type to add new tasks"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Add Task</button>
</form>
);
};
main.js:
import { Meteor } from 'meteor/meteor';
import { Tasks } from '/imports/api/tasks';
function insertTask({ text }) {
Tasks.insert({text});
}
Meteor.startup(() => {
if (!Accounts.findUserByUsername('meteorite')) {
Accounts.createUser({
username: 'meteorite',
password: 'password'
});
}
if (Tasks.find().count() === 0) { //this is for basic data that will never render once app is live.
[
{text:'updated THE Firstttt Task again this wont show'},
{text:'the Second Task'},
{text:'update 1 Third Task'},
{text:'Fourth Task'},
{text:'Fifth Task'},
{text:'Sixth Task'},
{text:'Seventh Task'}
].forEach(eachTask=>{insertTask(eachTask)})
}
});
Task.jsx:
import React from 'react';
import classnames from 'classnames';
export const Task = ({ task, onCheckboxClick, onDeleteClick }) => {
const classes = classnames('task', {
'checked': Boolean(task.isChecked)
});
return (
<li className={classes}>
<button onClick={ () => onDeleteClick(task) }>×</button>
<span>{ task.text }</span>
<input
type="checkbox"
checked={ Boolean(task.isChecked) }
onClick={ () => onCheckboxClick(task) }
readOnly
/>
</li>
);
};
I think i figured out SOME of it, but still having issues. These are my methods.
tasks.js:
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';
// export default new Mongo.Collection('taskList');
export const Tasks = new Mongo.Collection('tasks');
Meteor.methods({
'tasks.insert'(text) {
check(text, String);
if (!this.userId) {
throw new Meteor.Error('Not authorized.');
}
Tasks.insert({
text,
createdAt: new Date,
owner: this.userId,
username: Meteor.users.findOne(this.userId).username
})
},
'tasks.remove'(taskId) {
check(taskId, String);
if (!this.userId) {
throw new Meteor.Error('Not authorized.');
}
Tasks.remove(taskId);
},
'tasks.setChecked'(taskId, isChecked) {
check(taskId, String);
check(isChecked, Boolean);
if (!this.userId) {
throw new Meteor.Error('Not authorized.');
}
Tasks.update(taskId, {
$set: {
isChecked
}
});
}
});
those above are my methods.
below are my calls to those methods.
the ONLY ONE THAT WORKS is delete.
any ideas why the others are wrong?
App.jsx:
import React, { useState } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import { Task } from './Task';
import { Tasks } from '/imports/api/tasks';
import { TaskForm } from './TaskForm';
import { LoginForm } from './LoginForm';
const toggleChecked = ({ _id }) => Meteor.call('tasks.setChecked', _id)
const deleteTask = ({ _id }) => Meteor.call('tasks.remove',_id);
const logoutFunction = (e) => {
Meteor.logout(e)
}
export const App = () => {
const filter = {};
const [hideCompleted, setHideCompleted] = useState(false);
if (hideCompleted) {
_.set(filter, 'isChecked', false);
}
const { tasksList, incompleteTasksCount, user } = useTracker(() => ({
tasksList: Tasks.find(filter, { sort: { createdAt: -1 } }).fetch(),
incompleteTasksCount: Tasks.find({ isChecked: { $ne: true }}).count(),
user: Meteor.user(),
}));
if (!user) {
return (
<div className="simple-todos-react">
<LoginForm/>
</div>
);
}
return (
<div className="simple-todos-react">
<button onClick ={logoutFunction}>Log Out</button>
<h1>Flight List ({ incompleteTasksCount })</h1>
<div className="filters">
<label>
<input
type="checkbox"
readOnly
checked={ Boolean(hideCompleted) }
onClick={() => setHideCompleted(!hideCompleted)}
/>
Hide Completed
</label>
</div>
<ul className="tasks">
{ tasksList.map(task1 => <Task
key={ task1._id }
task={ task1 }
onCheckboxClick={toggleChecked}
onDeleteClick={deleteTask}/>) }
</ul>
<TaskForm user={user}/>
</div>
);
};
so again, function deleteTask works as expected.
however, function toggleChecked gives me the following error:
errorClass {message: "Match error: Expected boolean, got undefined", path: "", sanitizedError: errorClass, errorType: "Match.Error", stack: "Error: Match error: Expected boolean, got undefine…ea528700c66dd42ddcc29ef7434e9e62b909dc14:3833:16)"}errorType: "Match.Error"message: "Match error: Expected boolean, got undefined"path: ""sanitizedError: errorClass {isClientSafe: true, error: 400, reason: "Match failed", details: undefined, message: "Match failed [400]", …}stack: "Error: Match error: Expected boolean, got undefined↵ at check (http://localhost:3000/packages/check.js?hash=75acf7c24e10e7b3e7b30bb8ecc775fd34319ce5:76:17)↵ at MethodInvocation.tasks.setChecked (http://localhost:3000/app/app.js?hash=7e0d6e119e929408da1c048d1448a91b43b1a759:55:5)↵ at http://localhost:3000/packages/ddp-client.js?hash=5333e09ab08c9651b0cc016f95813ab4ce075f37:976:25↵ at Meteor.EnvironmentVariable.EVp.withValue (http://localhost:3000/packages/meteor.js?hash=857dafb4b9dff17e29ed8498a22ea5b1a3d6b41d:1207:15)↵ at Connection.apply (http://localhost:3000/packages/ddp-client.js?hash=5333e09ab08c9651b0cc016f95813ab4ce075f37:967:60)↵ at Connection.call (http://localhost:3000/packages/ddp-client.js?hash=5333e09ab08c9651b0cc016f95813ab4ce075f37:869:17)↵ at toggleChecked (http://localhost:3000/app/app.js?hash=7e0d6e119e929408da1c048d1448a91b43b1a759:149:17)↵ at onClick (http://localhost:3000/app/app.js?hash=7e0d6e119e929408da1c048d1448a91b43b1a759:318:20)↵ at HTMLUnknownElement.callCallback (http://localhost:3000/packages/modules.js?hash=ea528700c66dd42ddcc29ef7434e9e62b909dc14:3784:14)↵ at Object.invokeGuardedCallbackDev (http://localhost:3000/packages/modules.js?hash=ea528700c66dd42ddcc29ef7434e9e62b909dc14:3833:16)"proto: Error
Completely answered.
Updated my TaskForm.jsx submit function to:
const handleSubmit = () => {
if (!text) return;
Meteor.call('tasks.insert',text)
};
and updated my App.jsx to:
const toggleChecked = ({ _id, isChecked }) => Meteor.call('tasks.setChecked', _id, isChecked)
const deleteTask = ({ _id }) => Meteor.call('tasks.remove',_id);
I have a reusable component for Sign in with Apple Button
After user success, i navigate hem to Home screen
But i notes when i log navigation it's log undefined,
and when i log this.props i just got the two actions i made in redux!
So how can i access to navigation in this component and why it's not accessed by default!
Log
props => {"isLogin": [Function isLogin], "storeToken": [Function storeToken]}
navigation => undefined
Code
import appleAuth, {
AppleAuthCredentialState,
AppleAuthError,
AppleAuthRealUserStatus,
AppleAuthRequestOperation,
AppleAuthRequestScope,
AppleButton,
} from '#invertase/react-native-apple-authentication';
import React from 'react';
import {ActivityIndicator, StyleSheet, View} from 'react-native';
import {connect} from 'react-redux';
import API from '../../api/API';
import {isLoginFunc} from '../../redux/actions/isLoginAction';
import {saveToken} from '../../redux/actions/saveTokenAction';
class AppleAuth extends React.Component {
constructor(props) {
super(props);
this.authCredentialListener = null;
this.user = null;
this.state = {
credentialStateForUser: -1,
loading: false,
};
}
componentDidMount() {
const {navigation} = this.props;
console.log('did-navigation', navigation);
console.log('did- this.props', this.props);
/**
* subscribe to credential updates.This returns a function which can be used to remove the event listener
* when the component unmounts.
*/
this.authCredentialListener = appleAuth.onCredentialRevoked(async () => {
// console.warn('Credential Revoked');
this.fetchAndUpdateCredentialState().catch(error =>
this.setState({credentialStateForUser: `Error: ${error.code}`}),
);
});
this.fetchAndUpdateCredentialState()
.then(res => this.setState({credentialStateForUser: res}))
.catch(error =>
this.setState({credentialStateForUser: `Error: ${error.code}`}),
);
}
componentWillUnmount() {
/**
* cleans up event listener
*/
this.authCredentialListener();
}
signIn = async () => {
// start a login request
try {
const appleAuthRequestResponse = await appleAuth.performRequest({
requestedOperation: AppleAuthRequestOperation.LOGIN,
requestedScopes: [
AppleAuthRequestScope.EMAIL,
AppleAuthRequestScope.FULL_NAME,
],
});
this.setState({loading: true});
const {
user: newUser,
email,
nonce,
fullName: {familyName, givenName},
identityToken,
realUserStatus /* etc */,
} = appleAuthRequestResponse;
let username = `${givenName} ${familyName}`;
this.user = newUser;
this.fetchAndUpdateCredentialState()
.then(res => {
this.setState({credentialStateForUser: res});
console.log('res:::', res);
})
.catch(error => {
console.log(`Error: ${error.code}`);
this.setState({credentialStateForUser: `Error: ${error.code}`});
});
if (identityToken) {
console.log('email', email);
console.log('username', username);
console.log('nonce', nonce);
this.sendData(email, username, nonce);
// e.g. sign in with Firebase Auth using `nonce` & `identityToken`
} else {
// no token - failed sign-in?
}
if (realUserStatus === AppleAuthRealUserStatus.LIKELY_REAL) {
console.log("I'm a real person!");
}
// console.warn(`Apple Authentication Completed, ${this.user}, ${email}`);
} catch (error) {
if (error.code === AppleAuthError.CANCELED) {
alert('User canceled Apple Sign in');
// console.warn('User canceled Apple Sign in.');
} else {
console.error(error);
}
}
};
fetchAndUpdateCredentialState = async () => {
if (this.user === null) {
this.setState({credentialStateForUser: 'N/A'});
} else {
const credentialState = await appleAuth.getCredentialStateForUser(
this.user,
);
if (credentialState === AppleAuthCredentialState.AUTHORIZED) {
this.setState({credentialStateForUser: 'AUTHORIZED'});
} else {
this.setState({credentialStateForUser: credentialState});
}
}
};
// Send data "name,image,email" to API
sendData = async (Email, Name, Id) => {
try {
let response = await API.post('/apple', {
email: Email,
name: Name,
id: Id,
});
let {
data: {
data: {
response: {token},
},
},
} = response;
console.log('token:?>:', token);
console.log('props', this.props);
console.log('navigation', this.props.navigation);
this.setState({loading: false});
this.props.storeToken(token);
this.props.isLogin(true);
// this.props.navigation.push('BottomTabNavigator');
} catch (err) {
console.log(err);
alert('Unexpected Error, try again later.');
this.setState({loading: false});
}
};
render() {
return (
<View style={styles.container}>
{this.state.loading ? (
<ActivityIndicator />
) : (
<AppleButton
style={styles.appleButton}
cornerRadius={5}
buttonStyle={AppleButton.Style.WHITE}
buttonType={AppleButton.Type.SIGN_IN}
onPress={() => this.signIn()}
/>
)}
</View>
);
}
}
const styles = StyleSheet.create({
appleButton: {
width: 200,
height: 50,
// margin: 10,
},
container: {
flex: 1,
justifyContent: 'center',
},
});
const mapDispatchToProps = dispatch => {
// to excute the actions we want to invok
return {
isLogin: isLogin => {
dispatch(isLoginFunc(isLogin));
},
storeToken: token => {
dispatch(saveToken(token));
},
};
};
export default connect(
null,
mapDispatchToProps,
)(AppleAuth);
-
singin.js
<AppleAuth /> in the render method
if you render your component as component, not as a navigation screen, it will not receive navigation prop. It was like this in all versions of react-navigation
Access the navigation prop from any component
I'm trying to build a web app with react-admin and need to push data to the redux store. I read the React-admin docs (https://marmelab.com/react-admin/Actions.html) and when I try to do that, I get Failures.
Here is my code
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withStyles, MuiThemeProvider, createMuiTheme } from '#material-ui/core/styles';
import {
Menu,
Notification,
Sidebar,
setSidebarVisibility,
} from 'react-admin';
import {kidsLoad} from './customActions/KidsActions'
import AppBar from './MyAppBar';
const styles = theme => ({
root: {
display: 'flex',
flexDirection: 'column',
zIndex: 1,
minHeight: '100vh',
backgroundColor: theme.palette.background.default,
position: 'relative',
},
appFrame: {
display: 'flex',
flexDirection: 'column',
overflowX: 'auto',
},
contentWithSidebar: {
display: 'flex',
flexGrow: 1,
},
content: {
display: 'flex',
flexDirection: 'column',
flexGrow: 2,
padding: theme.spacing.unit * 3,
marginTop: '1em',
paddingLeft: 5,
},
});
class MyLayout extends Component {
componentWillMount() {
this.props.setSidebarVisibility(true);
}
componentDidMount(){
const { kidsLoad, record } = this.props;
kidsLoad({data: "HELLOOOOOOOOOOOO!!!!!"})
}
render() {
const {
children,
classes,
dashboard,
isLoading,
logout,
open,
title,
} = this.props;
return (
<div className={classes.root}>
<div className={classes.appFrame}>
<AppBar title={title} open={open} logout={logout} />
<main className={classes.contentWithSidebar}>
<div className={classes.content}>
{children}
</div>
</main>
<Notification />
</div>
</div>
);
}
}
MyLayout.propTypes = {
children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
dashboard: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string,
]),
isLoading: PropTypes.bool.isRequired,
// logout: componentPropType,
setSidebarVisibility: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
kidsLoad: PropTypes.func,
};
const mapStateToProps = state => ({ isLoading: state.admin.loading > 0 });
export default connect(mapStateToProps, { setSidebarVisibility, kidsLoad })(withStyles(styles)(MyLayout));
I did everything like in the documentation (https://marmelab.com/react-admin/Actions.html).
What did I do wrong?
How do you add data to the store in this framework?
This is going to be a quick answer that I leave for further improvement.
If I understand your question correctly you got some of your resources (entities) updated and you want react-admin to know about it and update its store accordingly triggering updates in app views if necessary.
The first thing we have to get is the dispatch function of the react-admin store. In my case, the source of resource update was a React component, so I used withDataProvider decorator to receive a reference to the dispatch function.
Once you have the dispatch function you dispatch, for example, CRUD_UPDATE_SUCCESS action for a particular resource update in the react-admin store.
import { CRUD_UPDATE_SUCCESS, FETCH_END, UPDATE } from 'react-admin';
dispatch({
type: CRUD_UPDATE_SUCCESS,
payload: { data },
meta: {
resource,
notification: {
body: 'ra.notification.dataSaved',
level: 'info'
},
fetchResponse: UPDATE,
fetchStatus: FETCH_END
}
});
You can also use action creators from react-admin. Like showNotification, for example.
import { showNotification } from 'react-admin';
dispatch(showNotification(errorMessage, 'warning', { autoHideDuration: 10000 }));
A bit more consistent piece of code here to show how all this can work together. The Updater component here renders its child components passing them a resource record and subscribes for their onSubmit callback to perform entity saving and updating react-admin store.
import React from 'react';
import {
CRUD_UPDATE_SUCCESS, FETCH_END, UPDATE, withDataProvider, showNotification
} from 'react-admin';
class Updater extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleUpdate(record) {
const { resource, dataProvider, dispatch } = this.props;
const payload = {
id: record.id,
};
payload.data = {...record};
return new Promise((resolve, reject) => {
dataProvider(UPDATE, resource, payload)
.then(({data}) => {
dispatch({
type: CRUD_UPDATE_SUCCESS,
payload: { data },
meta: {
resource,
notification: {
body: 'ra.notification.dataSaved',
level: 'info'
},
fetchResponse: UPDATE,
fetchStatus: FETCH_END
}
});
resolve(data);
})
.catch(e => {
const errorMessage = e.message || e;
this.setState({ errorMessage });
dispatch(
showNotification(errorMessage, 'warning', { autoHideDuration: 10000 })
);
reject(errorMessage);
});
});
}
render() {
const { record, children } = this.props;
const { errorMessage } = this.state;
return React.Children.only(React.cloneElement(children, {
record,
errorMessage,
onUpdate: this.handleUpdate
}));
}
}
export default withDataProvider(Updater);
HTH,
Ed
I am trying to combine React Redux with React Native, and I encountered this strange error while debugging the program:
TypeError: undefined is not a function (evaluating 'dispatch((0, _LoginActions.loginAction)(inputFormProp))')
The error is triggered in a login function from a component immediately after I run the program, and I don't know why I have it.
Here is my component code:
import React, { Component } from 'react';
import { Text, View, TextInput, ActivityIndicator, TouchableHighlight } from 'react-native';
import { getLogger, issueToText } from '../core/utils';
import styles from '../core/styles';
import { Card, Button, FormLabel, FormInput } from "react-native-elements";
import { connect } from 'react-redux'
import { loginAction } from '../actions/LoginActions'
export class LoginComponent extends Component {
constructor(props) {
super(props);
this.login = this.login.bind(this)
}
render() {
const { error, isLoading } = this.props;
const inputFormProp = {
username: '',
password: ''
};
return (
<View style={{ paddingVertical: 20 }}>
<Card>
<FormLabel>Email</FormLabel>
<FormInput value={inputFormProp.username} onChangeText={(text) => inputFormProp.username = text} />
<FormLabel>Password</FormLabel>
<FormInput value={inputFormProp.password} onChangeText={(text) => inputFormProp.password = text} />
<Button
buttonStyle={{ marginTop: 20 }}
backgroundColor="#03A9F4"
title="SIGN IN"
onPress={this.login(inputFormProp)}
/>
</Card>
<ActivityIndicator animating={this.props.isLoading} style={styles.activityIndicator} size="large" />
</View>
);
}
login(inputFormProp) {
const { store } = this.props.screenProps.store;
const { dispatch } = this.props
dispatch(loginAction(inputFormProp))
.then(() => {
if (this.props.error === null && this.props.isLoading === false) {
if (store.getState().auth.token) {
this.props.navigation.navigate('ProductList', { token: store.getState().auth.token });
}
}
})
.catch(error => {
});
}
}
function mapStateToProps(state) {
const { error, isLoading } = state.auth
return {
error,
isLoading,
}
}
export default connect(mapStateToProps)(LoginComponent)
and here is my app.js code:
const initialState = {
auth: { isLoading: false, error: null },
};
const rootReducer = combineReducers({ product: productReducer, auth: authReducer
});
const store = createStore(rootReducer, initialState, applyMiddleware(thunk,
createLogger()));
export const MyNavigator = StackNavigator({
Login: { screen: LoginComponent },
ProductList: { screen: ProductList },
});
export default class App extends Component {
render() {
return (
<MyNavigator screenProps={{ store: { store } }} />
);
}
};
From what I've already searched about the error, it seems that the cause is the connect() function in my component, but I don't know what is wrong with it.
Here is my directory structure:
Here is the LoginActions file:
import { loginService } from '../services/LoginService'
export function loginAction(data) {
return dispatch => {
loginService(data);
}
}
Here is the LoginService file:
import { httpApiUrl } from '../core/api';
import { getLogger } from "../core/utils";
import { Alert } from 'react-native';
const log = getLogger('auth/service');
export const loginService = (user) => (dispatch) => {
dispatch({ type: 'LOGIN_STARTED' });
return fetch(`${httpApiUrl}/api/userdata/verify`, {
method: 'POST',
headers: {
'Accept': '*/*',
'Content-Type': 'application/json',
},
body: JSON.stringify(user)
})
.then((response) => {
if (!response.ok) {
Alert.alert('ERROR', 'User or password is incorrect');
dispatch({ type: 'LOGIN_FAILED', data: 'User or password is incorrect' });
}
else return response;
}).then((response) => response.json).then((response) => {
dispatch({ type: 'LOGIN_SUCCEEDED', data: response.json });
})
.catch(error => {
dispatch({ type: 'LOGIN_FAILED', data: error.message });
});
};
Here is the output of this.props
21:10:48: Object {
21:10:48: "navigation": Object {
21:10:48: "dispatch": [Function anonymous],
21:10:48: "goBack": [Function goBack],
21:10:48: "navigate": [Function navigate],
21:10:48: "setParams": [Function setParams],
21:10:48: "state": Object {
21:10:48: "key": "Init-id-1515093047465-0",
21:10:48: "routeName": "Login",
21:10:48: },
21:10:48: },
21:10:48: "screenProps": Object {
21:10:48: "store": Object {
21:10:48: "store": Object {
21:10:48: "##observable": [Function observable],
21:10:48: "dispatch": [Function anonymous],
21:10:48: "getState": [Function getState],
21:10:48: "replaceReducer": [Function replaceReducer],
21:10:48: "subscribe": [Function subscribe],
21:10:48: },
21:10:48: },
21:10:48: },
21:10:48: }
You need to remove the prefixed export keyword before the class declaration
class LoginComponent extends Component { //<--- export was present here
constructor(props) {
super(props);
this.login = this.login.bind(this)
}
render() {
const { error, isLoading } = this.props;
const inputFormProp = {
username: '',
password: ''
};
return (
<View style={{ paddingVertical: 20 }}>
<Card>
<FormLabel>Email</FormLabel>
<FormInput value={inputFormProp.username} onChangeText={(text) => inputFormProp.username = text} />
<FormLabel>Password</FormLabel>
<FormInput value={inputFormProp.password} onChangeText={(text) => inputFormProp.password = text} />
<Button
buttonStyle={{ marginTop: 20 }}
backgroundColor="#03A9F4"
title="SIGN IN"
onPress={this.login(inputFormProp)}
/>
</Card>
<ActivityIndicator animating={this.props.isLoading} style={styles.activityIndicator} size="large" />
</View>
);
}
login(inputFormProp) {
const { store } = this.props.screenProps.store;
const { dispatch } = this.props
dispatch(loginAction(inputFormProp))
.then(() => {
if (this.props.error === null && this.props.isLoading === false) {
if (store.getState().auth.token) {
this.props.navigation.navigate('ProductList', { token: store.getState().auth.token });
}
}
})
.catch(error => {
});
}
}
function mapStateToProps(state) {
const { error, isLoading } = state.auth
return {
error,
isLoading,
}
}
export default connect(mapStateToProps)(LoginComponent)
Also make sure that You are importing the LoginComponent elsewhere as a default import
Seems to me like a messed up module import due to either babel settings or something else. You could set a breakpoint on a line the browser is complaining about and evaluate _LoginActions.loginAction in console. To make sure it is undefined.
Than find _LoginActions in scopes and the problem will be evident. If not, than please let us know what the corresponding scope looks like. Scope is a tab in chrome dev tools debugger Sources section
I'm working on a very simple react-native app where I type the name of an artist in a search bar and display a Flatlist of artists that I got using the spotify api.
I have 2 files my App.js that does the rendering and fetcher.js that implements the api calls.
But I'm unable to get the list to appear, I'm unable to set the state of artists.
App.js
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
FlatList,
StatusBar,
TextInput,
} from 'react-native';
import colors from './utils/colors';
import { List, ListItem, SearchBar } from 'react-native-elements';
import { searchArtist } from './utils/fetcher';
import { debounce } from 'lodash';
export default class spotilist extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
data: [],
query: '',
artists: [],
error: null,
refreshing: false,
};
}
render() {
return (
<View style={ styles.container }>
<StatusBar barStyle="light-content" />
<TextInput style={ styles.searchBox }
value={this.state.value}
onChangeText={ this.makeQuery }
/>
<Text> {this.state.artists} </Text>
</View>
);
}
makeQuery = debounce(query => {
searchArtist(query)
.then((artists) => {
this.setState({
artists: this.state.artists,
});
//console.log(artists)
})
.catch((error) => {
throw error;
});
}, 400);
}
const styles = StyleSheet.create({
container: {
paddingTop: 64,
flex: 1,
backgroundColor: colors.white,
},
searchBox: {
height: 40,
borderColor: colors.black,
borderWidth: 2,
margin: 16,
paddingLeft: 10,
fontWeight: '800',
},
row: {
flex: 1,
margin: 30,
alignSelf: 'stretch',
justifyContent: 'center',
},
});
fetch.js
export function searchArtist(query) {
const ClientOAuth2 = require('client-oauth2')
console.log("Query : " + query)
const spotifyAuth = new ClientOAuth2({
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
accessTokenUri: 'https://accounts.spotify.com/api/token',
authorizationUri: 'https://accounts.spotify.com/authorize',
scopes: []
})
spotifyAuth.credentials.getToken()
.then((user) => user.accessToken)
.then((token) => getQuery(token, query))
.then((result) => {
console.log(result) // No list :(
return result
});
}
function getQuery(token, query) {
console.log("Query2 : " + query)
const settings = {
"url": `https://api.spotify.com/v1/search?q=${ query }&type=artist`,
"method": "GET",
"headers": {
"authorization": "Bearer " + token,
"cache-control": "no-cache",
}
}
fetch(settings)
.then((res) => res.json())
.then(data => {
const artists = data.artists ? data.artists.items : [];
console.log(artists) // I get the list in the debbuger
return artists;
});
}
Thank you for your help.
You just need to return you fetch promise in getQuery
function getQuery(token, query) {
console.log("Query2 : " + query)
const settings = {
"url": `https://api.spotify.com/v1/search?q=${ query }&type=artist`,
"method": "GET",
"headers": {
"authorization": "Bearer " + token,
"cache-control": "no-cache",
}
}
return fetch(settings)
.then((res) => res.json());
}
And then when you call
spotifyAuth.credentials.getToken()
.then((user) => user.accessToken)
.then((token) => getQuery(token, query))
.then((result) => {
console.log(result) // No list :(
return result
});
getQuery will return this promise and you can handle it like you did before in getQuery:
return spotifyAuth.credentials.getToken()
.then((user) => user.accessToken)
.then((token) => getQuery(token, query))
.then(data => {
return data.artists ? data.artists.items : [];
});
then you can simple return this promise and handle wherever you want
You need to map through the array of artists. All react and react-native components cannot render data outside of data primitives (such as strings and numbers).
Such as:
{
this.state.artists.map(artist => {
return (
<Text key={artist.id}>{artist.name}</Text>
)
})
}
If the elements inside the state.artists array are just strings, just return the artist inside the text element.
The key value is for React to quickly assimilate the virtual dom to dom amidst state changes.