Debanjan Chakraborty
Debanjan Chakraborty

Reputation: 584

Function passed as props is not a function

I am trying to create a login module. I have a LoginView, which defines the view and a LoginController where I have defined all the user interactions. Now I am trying to incorporate a logic where in the LoginController will change the state of LoginView, as in change the value of isLoading from false to true in case all the input data is valid

LoginView

import React, { Component, Fragment} from 'react';
import LoginController from '../Controller/LoginController.js';

import {
  View,
  ScrollView,
  StatusBar,
  SafeAreaView,
  TextInput,
  TouchableOpacity,
  Text,
  StyleSheet
} from 'react-native';

const styles = StyleSheet.create({
   container: {
      paddingTop: 23
   },
   input: {
      margin: 15,
      height: 40,
      borderColor: '#7a42f4',
      borderWidth: 1
   },
   submitButton: {
      backgroundColor: '#7a42f4',
      padding: 10,
      margin: 15,
      height: 40,
   },
   submitButtonText:{
      color: 'white'
   }
});
export default class LoginView extends Component {

   constructor(){
      super()
      this.state = {
         isLoading: false
       }
   }

   changeLoadingState = (currentLoadingState) => {

      /* Create a loader screen and incorporate it here.
      */
      this.setState({isLoading: currentLoadingState} , () => {
         console.log("This is called when this.setState has resolved");
         console.log(this.state.isLoading);
       });
   }

  render() {

    const con = new LoginController(this.changeLoadingState);

     return (
        <Fragment>
        <StatusBar barStyle="dark-content" />
        <SafeAreaView>
         <View style = {styles.container}>
            <TextInput style = {styles.input}
               underlineColorAndroid = "transparent"
               placeholder = "Email"
               placeholderTextColor = "#9a73ef"
               autoCapitalize = "none"
               onChangeText = {con.handleEmail}/>

            <TextInput style = {styles.input}
               underlineColorAndroid = "transparent"
               placeholder = "Password"
               placeholderTextColor = "#9a73ef"
               autoCapitalize = "none"
               onChangeText = {con.handlePassword}/>

            <TouchableOpacity
               style = {styles.submitButton}
               onPress = {
                    () => con.login()
               }>
               <Text style = {styles.submitButtonText}> Submit </Text>
            </TouchableOpacity>
         </View>
         </SafeAreaView>
         </Fragment>
      );
        }
}

LoginController.js

import React, { Component } from 'react';
import LoginNetworkManager from '../NetworkManager/LoginNetworkManager.js';
import Loader from '../../Utils/Loader.js';

export default class LoginController extends Component {

      constructor(props) {
         super(props);
         this.state = {
            email: null,
            password: null
         };
         this.changeLoadingState = this.changeLoadingState.bind(this);
      }

      changeLoadingState = (currentLoadingState) => {
         this.props.changeLoadingState(currentLoadingState);
      }

      handleEmail = (text) => {
         this.setState({email: text});
      }
      handlePassword = (text) => {
         this.setState({password: text});
      }

      login = () => {

         this.changeLoadingState(this.validate());
         if (this.validate() == true) {
            // Here in we will call the API
         } else {
            console.log(" It's false ");
            // Do nothing
         }
      }

      validate = () => {
         var reg = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
         var isValid = reg.test(this.email);


         if (isValid) {
            isValid = (this.password.trim().length > 0);
         }

         console.log(" Tis is Valid " + isValid);
         return isValid
      }
   }

The error when tapped the login button is

   _this.props.changeLoadingState is not a function
    handleException @ ExceptionsManager.js:86
    handleError @ setUpErrorHandling.js:23
    reportFatalError @ error-guard.js:42
    __guard @ MessageQueue.js:345
    callFunctionReturnFlushedQueue @ MessageQueue.js:105
    (anonymous) @ debuggerWorker.js:80

Upvotes: 1

Views: 3703

Answers (3)

Debanjan Chakraborty
Debanjan Chakraborty

Reputation: 584

Thanks to Dupocas's reply I understood when to utilise Components and when not to.

In my case, LoginController shouldn't be a component, because there is nothing to render in it's logic. It is purely a helper class.

The result code is now as follows

import React, { Component, Fragment} from 'react';
import LoginController from '../Controller/LoginController.js';
import Loader from '../../Utils/Loader';

import {
  View,
  StatusBar,
  SafeAreaView,
  TextInput,
  TouchableOpacity,
  Text,
  StyleSheet
} from 'react-native';

const styles = StyleSheet.create({
   container: {
      paddingTop: 23
   },
   input: {
      margin: 15,
      height: 40,
      borderColor: '#7a42f4',
      borderWidth: 1
   },
   submitButton: {
      backgroundColor: '#7a42f4',
      padding: 10,
      margin: 15,
      height: 40,
   },
   submitButtonText:{
      color: 'white'
   }
});
export default class LoginView extends Component {

   constructor(){
      super()
      this.state = {
         isLoading: false
       }
       con = new LoginController();
   }

   changeLoadingState = (currentLoadingState,completionBlock) => {
      this.setState({isLoading: currentLoadingState} , completionBlock);
   }

  render() {
     return (
        <Fragment>
        <StatusBar barStyle="dark-content" />
        <SafeAreaView>
         <View style = {styles.container}>
         <Loader
          loading={this.state.isLoading} />
            <TextInput style = {styles.input}
               underlineColorAndroid = "transparent"
               placeholder = "Email"
               placeholderTextColor = "#9a73ef"
               autoCapitalize = "none"
               onChangeText = {con.handleEmail}/>

            <TextInput style = {styles.input}
               underlineColorAndroid = "transparent"
               placeholder = "Password"
               placeholderTextColor = "#9a73ef"
               autoCapitalize = "none"
               onChangeText = {con.handlePassword}/>

            <TouchableOpacity
               style = {styles.submitButton}
               onPress = {
                    () => con.login(this.changeLoadingState)
               }>
               <Text style = {styles.submitButtonText}> Submit </Text>
            </TouchableOpacity>
         </View>
         </SafeAreaView>
         </Fragment>
      );
        }
}

LoginController is

import LoginNetworkManager from '../NetworkManager/LoginNetworkManager.js';
export default class LoginController  {

     email = null;
     password = null;

   changeLoadingState = (currentLoadingState,viewCallback,completionBlock) => {
      viewCallback(currentLoadingState,completionBlock);
      }

      handleEmail = (text) => {
         this.email = text
      }
      handlePassword = (text) => {
         this.password = text
      }

      login = (viewCallback) => {

         this.changeLoadingState(this.validate(),viewCallback);
         if (this.validate() == true) {

            let params = { email : this.email, password : this.password};

            LoginNetworkManager.loginAPI(params, (response,error) => {

               this.changeLoadingState(false,viewCallback,() => {

                  if (error){

                  }else{

                  }
                });
            });
         } 
      }

      validate = () => {
         var reg = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
         var isValid = reg.test(this.email);

         console.log(" this.email " + this.email);
         console.log(" this.password " + this.password);
         if (isValid) {
            isValid = (this.password.trim().length > 0);

            console.log(" password validation ----> " + isValid);
         }


         return isValid
      }
   }

Although, Dupocas's mentioned out about HOC and RenderProps and Hooks, I believe, that if I don't need a Component, I should try it in the non-component way, although it was insightful and might help me in future complex scenarios.

Upvotes: 0

Dupocas
Dupocas

Reputation: 21357

The problem here is that LoginController isn't a Component, if you want LoginController to be only a helper class, than you should remove state and props from it:

export default class LoginController {

      changeLoadingState = (currentLoadingState) => {

      }

      handleEmail = (text) => {

      }
      handlePassword = (text) => {

      }

      login = () => {

         this.changeLoadingState(this.validate());
         if (this.validate() == true) {
            // Here in we will call the API
         } else {
            console.log(" It's false ");
            // Do nothing
         }
      }

      validate = () => {
         var reg = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
         var isValid = reg.test(this.email);


         if (isValid) {
            isValid = (this.password.trim().length > 0);
         }

         console.log(" Tis is Valid " + isValid);
         return isValid
      }
   }

But if your goal is to abstract stateful logic, then you're doing it wrong. When you extend React.Component from a class you are explicitly telling React that this class is a Component therefore it should return JSX (render()) and should be initialized as a Component: <LoginController />, to abstract stateful logic you actually have a lot of really cool alternatives:

High Order Components (HOC)

This seens to be your use case, since you want a inject some props into LoginView, so you could abstract the logic to a HOC:

import React, { Component } from 'react';
import LoginNetworkManager from '../NetworkManager/LoginNetworkManager.js';
import Loader from '../../Utils/Loader.js';

export default withLogin = (ChildComponent) => {
    return class LoginController extends Component {
        constructor(props) {
            super(props);
            this.state = {
                email: null,
                password: null
            };
            this.changeLoadingState = this.changeLoadingState.bind(this);
        }
        /*
         Your logic
        */

        render(){
            return <ChildComponent {...this.state} />
        }
    }
}

Now inside LoginView you export like this: export default withLogin(LoginView) and LoginController's state will be serialized inside LoginView's props: this.props.email and this.props.password

And of course, everything that can be done with an HOC can also be done using renderProps and hooks.

Upvotes: 1

Chris Hawkes
Chris Hawkes

Reputation: 12430

Your not passing the function as a prop to your LoginController component.

Upvotes: 0

Related Questions