Dash Component Creation - JavaScript OK; Nothing Rendered in Python


#1

@chriddyp @nedned @xhlu

I’ve finished my first react component which runs successfully and acts as expected: https://github.com/SterlingButters/plaidash

For some reason, nothing is rendered in Python by running usage.py. The JS console doesn’t show anything out of the ordinary either. The only thing to note really was an error at build:

Error with path src/lib/components/LoginForm.react.jsTypeError: Cannot read property 'length' of undefined
TypeError: Cannot read property 'length' of undefined
    at checkWarn (/Users/sterlingbutters/anaconda3/lib/python3.6/site-packages/dash/extract-meta.js:45:15)
    at Object.entries.forEach (/Users/sterlingbutters/anaconda3/lib/python3.6/site-packages/dash/extract-meta.js:54:24)
    at Array.forEach (<anonymous>)
    at docstringWarning (/Users/sterlingbutters/anaconda3/lib/python3.6/site-packages/dash/extract-meta.js:53:31)
    at parseFile (/Users/sterlingbutters/anaconda3/lib/python3.6/site-packages/dash/extract-meta.js:70:9)
    at dirs.forEach.filename (/Users/sterlingbutters/anaconda3/lib/python3.6/site-packages/dash/extract-meta.js:92:17)
    at Array.forEach (<anonymous>)
    at collectMetadataRecursively (/Users/sterlingbutters/anaconda3/lib/python3.6/site-packages/dash/extract-meta.js:87:14)
    at componentPaths.forEach.componentPath (/Users/sterlingbutters/anaconda3/lib/python3.6/site-packages/dash/extract-meta.js:21:5)
    at Array.forEach (<anonymous>)

The only other root cause I can think of has to do with property definitions in python of the react component.
How are properties inserted into the App class of the React component with Python? Currently, I have some properties already defined in the react component:

render() {
    return (
        <LoginForm
            id="Test"
            access_token={this.state.access_token}

            clientName="Plaid Client"
            env="sandbox"
            product={['auth', 'transactions']}
            publicKey="7a3daf1db208b7d1fe65850572****"
            apiVersion="v2"
            onTokenUpdate={this.handleUpdateToken}
        >
        </LoginForm>
    );
}

and in python I try to “overwrite” those:

@app.callback(Output('login-container', 'children'),
              [Input('open-form-button', 'n_clicks'),])
def display_output(clicks):
    if clicks is not None and clicks > 0:
        return plaidash.LoginForm(
            id='plaid-link',
            clientName='Butters',
            env='sandbox',
            publicKey='7a3daf1db208b7d1fe65850572****',
            product=['auth', 'transactions'],
        ),

is this valid?

Thanks for any/all help


#2

That error come from the warning check for the prop types docstring because the docstring is missing, it is a bug.

Prop docstring should be in this format:

/**
* Prop description
*/
my_prop: PropType.string,

I’ll fix the bug soon, thanks for reporting. https://github.com/plotly/dash/issues/598


#3

@Philippe Roger that, can’t say my Prop docstring abides that format - will fix. Does this bug affect the resulting python component i.e. is it possible that this is the cause of my issue?


#4

Not sure, but I think so, are the props in the __init__ of the generated python class file ?


#5

Contents of __init.py__:

from __future__ import print_function as _

import os as _os
import sys as _sys
import json

import dash as _dash

# noinspection PyUnresolvedReferences
from ._imports_ import *
from ._imports_ import __all__

if not hasattr(_dash, 'development'):
    print('Dash was not successfully imported. '
          'Make sure you don\'t have a file '
          'named \n"dash.py" in your current directory.', file=_sys.stderr)
    _sys.exit(1)

_basepath = _os.path.dirname(__file__)
_filepath = _os.path.abspath(_os.path.join(_basepath, 'package.json'))
with open(_filepath) as f:
    package = json.load(f)

package_name = package['name'].replace(' ', '_').replace('-', '_')
__version__ = package['version']

_current_path = _os.path.dirname(_os.path.abspath(__file__))

_this_module = _sys.modules[__name__]


_js_dist = [
    {
        'relative_package_path': 'plaidash.min.js',
        'dev_package_path': 'plaidash.dev.js',
        
        'namespace': package_name
    }
]

_css_dist = []


for _component in __all__:
    setattr(locals()[_component], '_js_dist', _js_dist)
    setattr(locals()[_component], '_css_dist', _css_dist)

Doesn’t look like it, however, props are in LoginForm.py:

@_explicitize_args
    def __init__(self, id=Component.UNDEFINED, apiVersion=Component.UNDEFINED, clientName=Component.REQUIRED, env=Component.UNDEFINED, institution=Component.UNDEFINED, publicKey=Component.REQUIRED, product=Component.REQUIRED, token=Component.UNDEFINED, access_token=Component.UNDEFINED, selectAccount=Component.UNDEFINED, webhook=Component.UNDEFINED, onSuccess=Component.UNDEFINED, onExit=Component.UNDEFINED, onLoad=Component.UNDEFINED, onEvent=Component.UNDEFINED, onTokenUpdate=Component.UNDEFINED, style=Component.UNDEFINED, **kwargs):
        self._prop_names = ['id', 'apiVersion', 'clientName', 'env', 'institution', 'publicKey', 'product', 'token', 'access_token', 'selectAccount', 'webhook']
        self._type = 'LoginForm'
        self._namespace = 'plaidash'
        self._valid_wildcard_attributes =            []
        self.available_properties = ['id', 'apiVersion', 'clientName', 'env', 'institution', 'publicKey', 'product', 'token', 'access_token', 'selectAccount', 'webhook']
        self.available_wildcard_properties =            []
...

This is in build after adjusted docstring in LoginForm.react.js:

...
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,

    /**
     * This is the token that will be returned for use in Dash. Can also be
     * understood as the "output" of this component
     */
    access_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.
     */
    onEvent: PropTypes.func,

    /**
     * Function that is run when the token is updated
     */
    onTokenUpdate: PropTypes.func,
};

Hoping this is source of my problem :crossed_fingers:


#6

That was not the cause of the problem, your component seems to generate ok with or without the docstrings.

I tried usage.py and got a blank page, but your component is in the tree:


#7

Yes I know!! It is frustrating because I don’t know what the issue could be. Js console doesn’t give any indicators either. I would welcome ANY thoughts you might have


#8

Debugging the js show that no handle functions are ever called beside onScriptLoaded.

You are not opening the plaid thing.

changed this:

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,
        });
        const institution = this.props.institution || null;
        window.linkHandler.open(institution);
    }

And got:


#9

Oh, btw, you cannot use PropType.func with dash components, if you have logic that depends on it, it won’t work in dash but may work in the App.react.js.

I suggest removing that js demo and only using usage.py for testing if you don’t plan on using the component without dash, the way props are handled is really different when running without the renderer.


#10

Very Nice!! Thank you so much! I just hope that in assigning/changing the token property Plaid does not rerender window as this was the intention of the chooseRender() function in the render block.


#11

And that it is probably the clear explanation for the following error:
TypeError: this.props.onTokenUpdate is not a function. (In 'this.props.onTokenUpdate(token)', 'this.props.onTokenUpdate' is undefined)

So dumb question here but does that apply for ALL PropType.func's or just those called by the Parent in App.js? I ask because I only get an error for the property whose has a function assigned to it in App.js (onTokenUpdate)

Also, if you have suggestions/examples on how to convert use of PropType.func into JSON serializable PropType, that would be most helpful :slight_smile:

UPDATE (new files):

LoginForm.react.js:

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

/**
 * Description of LoginForm
 */
class LoginForm extends Component {
    constructor(props) {
        super(props);

        this.state = {
            access_token: null,
            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);
    }

    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,
        });
        const institution = this.props.institution || null;
        window.linkHandler.open(institution);
    }

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

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

    render() {
        return (
            <div id={this.props.id}
                 access_token={this.state.access_token}>

                <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,

    /**
     * This is the token that will be returned for use in Dash. Can also be
     * understood as the "output" of this component
     */
    access_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.
     */
    onEvent: PropTypes.func,
};

export default LoginForm;

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 = {
        };
    }

    render() {
        return (
            <LoginForm
                id="Test"

                clientName="Plaid Client"
                env="sandbox"
                product={['auth', 'transactions']}
                publicKey="****af1db208b7d1fe65850572****"
                apiVersion="v2"
            >
            </LoginForm>
        );
    }
}

export default App;

Works as expected in React and access_token is updated. This is not the case in Dash. Is it because of:

/**
     * 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,

???


#12

Also, if you have suggestions/examples on how to convert use of PropType.func into JSON serializable PropType , that would be most helpful

You have to think about it as a prop based api, you use this.props.setProps to send updates to the dash back end.

Let’s say you have a success object that is the result of a onSuccess handler of a wrapped component. You have a handler for that in your component and you call setProps with the success object. The public_key and metadata argument are either state or prop. If you need logic when a prop or state update, do it in componentDidUpdate or componentWillReceiveProps.


#13

As I understand it, setProps() is deprecated:

TypeError: this.props.setProps is not a function. (In 'this.props.setProps({ access_token: token })', 'this.props.setProps' is undefined)

from

handleOnSuccess(token, metadata) {
    console.log(token);
    console.log(metadata);
    // this.setState({access_token: token});
    this.props.setProps({access_token: token})

The component property access_token updates in React but not in Dash so I feel like I shouldn’t need to supply additional logic, right?


#14

As I understand it, setProps() is deprecated

setProps is the dash API, it’s not deprecated. It is undefined when no callbacks are connected to it, you need to this.setState as a fallback logic in that case.

https://dash.plot.ly/react-for-python-developers section Handling the case when setProps isn't defined for more info.


#15

@Philippe I receive the undefined error only when testing in React. In Dash, I get no errors, yet the property is not updated even though there is callback connected to it:

...
# CALLBACK FOR setProps
@app.callback(Output('display-transactions', 'children'),
             [Input('plaid-link', 'access_token'),])
def display_output(token):
    print(token)

    start_date = '{:%Y-%m-%d}'.format(datetime.datetime.now() + datetime.timedelta(-30))
    end_date = '{:%Y-%m-%d}'.format(datetime.datetime.now())
    try:
        transactions_response = client.Transactions.get(access_token=token, start_date=start_date, end_date=end_date)
    except plaid.errors.PlaidError as e:
        return jsonify(format_error(e))

    pretty_print_response(transactions_response)
    return html.P(jsonify({'error': None, 'transactions': transactions_response}))
...

[UPDATE]: OK IM OFFICIALLY SUPER CONFUSED…

[Log] loaded

[Log] PlaidLink: user event – "OPEN" – {error_code: null, error_message: null, error_type: null, …}
{error_code: null, error_message: null, error_type: null, exit_status: null, institution_search_query: null, …}Object

[Log] PlaidLink: user event – "TRANSITION_VIEW" – {error_code: null, error_message: null, error_type: null, …}
{error_code: null, error_message: null, error_type: null, exit_status: null, institution_search_query: null, …}Object

[Log] PlaidLink: user event – "SELECT_INSTITUTION" – {error_code: null, error_message: null, error_type: null, …}
{error_code: null, error_message: null, error_type: null, exit_status: null, institution_id: "ins_3", …}Object

[Log] PlaidLink: user event – "TRANSITION_VIEW" – {error_code: null, error_message: null, error_type: null, …}
{error_code: null, error_message: null, error_type: null, exit_status: null, institution_id: "ins_3", …}Object

[Log] PlaidLink: user event – "TRANSITION_VIEW" – {error_code: null, error_message: null, error_type: null, …}
{error_code: null, error_message: null, error_type: null, exit_status: null, institution_id: "ins_3", …}Object

[Log] PlaidLink: user event – "TRANSITION_VIEW" – {error_code: null, error_message: null, error_type: null, …}
{error_code: null, error_message: null, error_type: null, exit_status: null, institution_id: "ins_3", …}Object

[Log] PlaidLink: user event – "HANDOFF" – {error_code: null, error_message: null, error_type: null, …}

{error_code: null, error_message: null, error_type: null, exit_status: null, institution_id: "ins_3", …}Object

[Log] public-sandbox-00f3e7da-5634-42b6-9a15-****

[Log] {institution: {name: "Chase", institution_id: "ins_3"}, account: Object, account_id: null, accounts: Array, link_session_id: "2af437bd-0528-4f59-9f8c-****", …}

[Log]  action ON_PROP_CHANGE @ 16:16:12.921 (dash_renderer.dev.js, line 31525)
[Log]  prev state (dash_renderer.dev.js, line 31537)
Object
appLifecycle: "HYDRATED"
config: {url_base_pathname: null, requests_pathname_prefix: "/"}
dependenciesRequest: {status: 200, content: Array}
graphs: {InputGraph: Object}
history: {past: Array, present: {id: "plaid-link", props: {access_token: undefined}}, future: []}
layout: {props: Object, type: "Div", namespace: "dash_html_components"}
layoutRequest: {status: 200, content: Object}
paths: {login-container: ["props", "children", 0], open-form-button: ["props", "children", 1], plaid-link: Array}
reloadRequest: {}
requestQueue: [Object] (1)
Object Prototype

[Log]  action     (dash_renderer.dev.js, line 31541)
    Object
    payload: {props: {access_token: "public-sandbox-00f3e7da-5634-42b6-9a15-****"}, id: "plaid-link", itempath: Array}

type: "ON_PROP_CHANGE"
Object Prototype
[Log]  next state (dash_renderer.dev.js, line 31549)
Object
app lifecycle: "HYDRATED"
config: {url_base_pathname: null, requests_pathname_prefix: "/"}
dependenciesRequest: {status: 200, content: Array}
graphs: {InputGraph: Object}
history: {past: Array, present: {id: "plaid-link", props: {access_token: "public-sandbox-00f3e7da-5634-42b6-9a15-****"}}, future: []}
layout: {props: Object, type: "Div", namespace: "dash_html_components"}
layoutRequest: {status: 200, content: Object}
paths: {login-container: ["props", "children", 0], open-form-button: ["props", "children", 1], plaid-link: Array}
reloadRequest: {}
requestQueue: [Object] (1)
Object Prototype

CLEARLY the access_token is being changed… Why is this not reflected in Dash… please see dev branch: https://github.com/SterlingButters/plaidash/tree/dev

UPDATE 2: Ok apparently it IS being updated but the callback is not triggered when the value of access_token property is changed so for now I have to retrieve value in callback using State. Would appreciate advice on how to use access_token as Input