2

Ok, I've officially spent hours trying to figure this out and yet I'm sure its a simple fix. I'm brand new to React and trying to create a custom component for Plotly Dash.

Problem

I am trying to update the token property of the LoginForm component which makes use of the Plaid link for anyone that is familiar. You will notice in the handleOnSuccess function in LoginForm.react.js I am able to retrieve the token and display it in the console. All I want to do is update the token property of the LoginForm with that value displayed in the console.

Below is the entire LoginForm.react.js:

import React, { Component } from 'react';
import Script from 'react-load-script';
import PropTypes from 'prop-types';


class LoginForm extends Component {
    constructor(props) {
        super(props);

        this.state = {
            linkLoaded: false,
            initializeURL: 'https://cdn.plaid.com/link/v2/stable/link-initialize.js',
        };

        this.onScriptError = this.onScriptError.bind(this);
        this.onScriptLoaded = this.onScriptLoaded.bind(this);

        this.handleLinkOnLoad = this.handleLinkOnLoad.bind(this);

        this.handleOnExit = this.handleOnExit.bind(this);
        this.handleOnEvent = this.handleOnEvent.bind(this);
        this.handleOnSuccess = this.handleOnSuccess.bind(this);

        this.renderWindow = this.renderWindow.bind(this);
    }

    onScriptError() {
        console.error('There was an issue loading the link-initialize.js script');
    }

    onScriptLoaded() {
        window.linkHandler = window.Plaid.create({
            apiVersion: this.props.apiVersion,
            clientName: this.props.clientName,
            env: this.props.env,
            key: this.props.publicKey,
            onExit: this.handleOnExit,
            onLoad: this.handleLinkOnLoad,
            onEvent: this.handleOnEvent,
            onSuccess: this.handleOnSuccess,
            product: this.props.product,
            selectAccount: this.props.selectAccount,
            token: this.props.token,
            webhook: this.props.webhook,
        });

        console.log("Script loaded");
    }

    handleLinkOnLoad() {
        console.log("loaded");
        this.setState({ linkLoaded: true });
    }
    handleOnSuccess(token, metadata) {
        console.log(token);
        console.log(metadata);
    }
    handleOnExit(error, metadata) {
        console.log('link: user exited');
        console.log(error, metadata);
    }
    handleOnLoad() {
        console.log('link: loaded');
    }
    handleOnEvent(eventname, metadata) {
        console.log('link: user event', eventname, metadata);
    }

    renderWindow() {
        const institution = this.props.institution || null;
        if (window.linkHandler) {
            window.linkHandler.open(institution);
        }
    }

    static exit(configurationObject) {
        if (window.linkHandler) {
            window.linkHandler.exit(configurationObject);
        }
    }

    render() {
        return (
            <div id={this.props.id}>
                {this.renderWindow()}
                <Script
                    url={this.state.initializeURL}
                    onError={this.onScriptError}
                    onLoad={this.onScriptLoaded}
                />
            </div>
        );
    }
}

LoginForm.defaultProps = {
    apiVersion: 'v2',
    env: 'sandbox',
    institution: null,
    selectAccount: false,
    style: {
        padding: '6px 4px',
        outline: 'none',
        background: '#FFFFFF',
        border: '2px solid #F1F1F1',
        borderRadius: '4px',
    },
};

LoginForm.propTypes = {
    // id
    id: PropTypes.string,

    // ApiVersion flag to use new version of Plaid API
    apiVersion: PropTypes.string,

    // Displayed once a user has successfully linked their account
    clientName: PropTypes.string.isRequired,

    // The Plaid API environment on which to create user accounts.
    // For development and testing, use tartan. For production, use production
    env: PropTypes.oneOf(['tartan', 'sandbox', 'development', 'production']).isRequired,

    // Open link to a specific institution, for a more custom solution
    institution: PropTypes.string,

    // The public_key associated with your account; available from
    // the Plaid dashboard (https://dashboard.plaid.com)
    publicKey: PropTypes.string.isRequired,

    // The Plaid products you wish to use, an array containing some of connect,
    // auth, identity, income, transactions, assets
    product: PropTypes.arrayOf(
        PropTypes.oneOf([
            // legacy product names
            'connect',
            'info',
            // normal product names
            'auth',
            'identity',
            'income',
            'transactions',
            'assets',
        ])
    ).isRequired,

    // Specify an existing user's public token to launch Link in update mode.
    // This will cause Link to open directly to the authentication step for
    // that user's institution.
    token: PropTypes.string,

    // Set to true to launch Link with the 'Select Account' pane enabled.
    // Allows users to select an individual account once they've authenticated
    selectAccount: PropTypes.bool,

    // Specify a webhook to associate with a user.
    webhook: PropTypes.string,

    // A function that is called when a user has successfully onboarded their
    // account. The function should expect two arguments, the public_key and a
    // metadata object
    onSuccess: PropTypes.func,

    // A function that is called when a user has specifically exited Link flow
    onExit: PropTypes.func,

    // A function that is called when the Link module has finished loading.
    // Calls to plaidLinkHandler.open() prior to the onLoad callback will be
    // delayed until the module is fully loaded.
    onLoad: PropTypes.func,

    // A function that is called during a user's flow in Link.
    // See
    onEvent: PropTypes.func,

    // Button Styles as an Object
    style: PropTypes.object,

    // Button Class names as a String
    className: PropTypes.string,
};

export default LoginForm;

And here is App.js:

// /* eslint no-magic-numbers: 0 */
import React, { Component } from 'react';
import { LoginForm } from '../lib';

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            token: null
        }
    }

    render() {
        return (
            <LoginForm
                id="Test"
                clientName="Plaid Client"
                env="sandbox"
                product={['auth', 'transactions']}
                publicKey="7a3daf1db208b7d1fe65850572eeb1"
                className="some-class-name"
                apiVersion="v2"
                token={this.state.token}
            >
            </LoginForm>
        );
    }
}

export default App;

I believe that it is essential to prevent the assignment of any function to a property of LoginForm i.e. token={this.someFunction} is not acceptable

I also know that it's not preferable (if it's even possible) to directly change the value of a property i.e. logically the insertion of this.props.token=token into the handleOnSuccess function might work (LOGICALLY - I know it doesn't) but that still doesn't really provide a sound flow of updating components between the parent and child processes.

I appreciate any and all help as this is literally the LAST step in this little project and I really cant figure it out. Thanks in advance!

If it makes it easier - you can clone the repo here: https://github.com/SterlingButters/plaidash

Sterling Butters
  • 1,024
  • 3
  • 20
  • 41

2 Answers2

1

You can have a handleUpdateToken method in App, which you pass down as a prop to LoginForm:

class App extends Component {
  ...
  handleUpdateToken(token) {
    this.setState({ token });
  }

  ...
  render() {
    return (
      <LoginForm
        onUpdateToken={this.handleUpdateToken}
        ...other LoginForm props
      />
  }
}

in LoginForm:

handleOnSuccess(token, metadata) {
  console.log(token);
  console.log(metadata);
  this.props.onUpdateToken(token);
}
helloitsjoe
  • 6,264
  • 3
  • 19
  • 32
  • Ok that's what I thought - I DEFINITELY had tried that and I got this error in js console: `TypeError: this.setState is not a function. (In 'this.setState({ token: token })', 'this.setState' is undefined)` - any ideas? – Sterling Butters Feb 09 '19 at 04:57
  • 1
    You'll need to bind `handleUpdateToken` in `App`'s constructor like you're doing with the handlers in LoginForm: `this.handleUpdateToken = this.handleUpdateToken.bind(this)` – helloitsjoe Feb 09 '19 at 05:08
  • 1
    Interesting! I think I had done that previously and in fact, I must have already had it working! It just doesn't work like I expected it to because what happens is once the token is assigned to the component property, it rerenders the component causing a continuous loop since the component can be initiated with the token alone! Thanks for the help, Ill have to figure out what I want to do about this behavior – Sterling Butters Feb 09 '19 at 05:12
  • UPDATE: Turns out that's not actually what the problem is... at least from what I can tell. Maybe you might know this error and how to fix: `Blocked a frame with origin "http://localhost:8080" from accessing a frame with origin "https://cdn.plaid.com". The frame requesting access has a protocol of "http", the frame being accessed has a protocol of "https". Protocols must match.` Otherwise I'll probably need to make a separate post – Sterling Butters Feb 09 '19 at 16:05
  • Hmmm Chrome doesn't throw any error. I'm thinking now what is probably happening is once `handleOnSuccess` is called, a rerender is forced which is undesired – Sterling Butters Feb 09 '19 at 16:13
  • This sounds like a separate issue. I'm not sure what your `window.linkHandler` is doing, but it sounds like you need to configure your local server to use `https`. If you're using express, see this post: https://stackoverflow.com/questions/11744975/enabling-https-on-express-js – helloitsjoe Feb 10 '19 at 03:12
  • Yes, `handleOnSuccess` calls `setState`, which triggers a rerender. – helloitsjoe Feb 10 '19 at 03:16
  • yeah I'm not too worried about the protocol error. I am working on the rerender problem. https://stackoverflow.com/questions/54608458/react-set-state-or-set-prop-without-a-rerender?noredirect=1#comment96013670_54608458 – Sterling Butters Feb 10 '19 at 03:21
  • I'm sorry to hear you're having an issue with rerendering, but I'm not sure why you removed the solved flag, since this answer seems to solve your original question: "All I want to do is update the token property of the LoginForm with that value displayed in the console." – To avoid a rerender in the child, you could use `componentShouldUpdate`, or you might try moving your `linkHandler` logic into App, and only rendering LoginForm once you have the token. – helloitsjoe Feb 10 '19 at 04:23
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/188165/discussion-between-sterling-butters-and-helloitsjoe). – Sterling Butters Feb 10 '19 at 04:27
0

yes you're close - what you need is to define an updateToken function in the App component, which uses this.setState.

pass updateToken function as a prop to LoginForm. The LoginForm component should call this function in handleOnSuccess.

in App.react.js:

// pass this function as prop to LoginForm.
// don't forget to bind to 'this'.
updateToken(token, metadata) {
    ...
    this.setState({ token })
}

...

// in render function
<LoginForm updateToken={updateToken} ... />

in LoginForm.react.js:

handleOnSuccess(token, metadata) {
    this.props.updateToken(token, metadata)
}

you're quite right to avoid assigning to props. with this method, you delegate the responsibility of updating props back to the parent, and ensure that the state and the update function(s) live in the same component.

Yuan Ruo
  • 36
  • 2
  • 5