📣 Introducing TypeScript Dash component generation

Hi everyone,

I want to share a new way to create Dash components using TypeScript to define props.

To use the typescript generation, add --ts argument to dash-generate-components, ie: dash-generate-components path/to/components projectname --ts. All components should be in .tsx.

Currently in PR, but you can try it now with this cookiecutter template:

cookiecutter gh:t4rk1n/dash-typescript-component-template

The template requirements.txt will install Dash from the PR, as such the setup will error, the components can still be generated but Dash will fail to start. You can install the components library with pip install -e . in another venv with the latest Dash version installed to test the components.

Example TypeScript component:

import React, {useCallback} from 'react';

type Props = {
    id?: string;
    /**
     * JSX.Element is equivalent to PropTypes.node, also React.ReactNode & React.ReactElement.
     */
    children: JSX.Element;
    n_clicks?: number;
    class_name?: string;
    /**
     * Call to update props in callbacks.
     */
    setProps?: (props: Record<string, any>) => void;
}

const MyComponent = (props: Props) => {
    const {
        setProps,
        children,
        n_clicks,
        id,
        class_name,
    } = props;

    const onClick = useCallback(
        () => setProps({n_clicks: n_clicks + 1}),
        [n_clicks]
    );

    return (
        <div
            id={id}
            className={class_name}
            onClick={onClick}
        >
            {children}
        </div>
    )
}


MyComponent.defaultProps = {
    n_clicks: 0,
}

export default MyComponent;
10 Likes

This is great. I’ll try it with dash-mantine-components.

This looks great, and I would love to re-write my components with TypeScript. I had a quick play with it, and I seem to get resolve issues whenever I try to write a component with a dependent library. Do you have any examples of using this as a wrapper for another library?

Here’s a wrapper I created for react-colorful:

3 Likes

Sorry to bump an old conversation, but I’ve just started playing around with this and am really enjoying it. The one thing I’m missing in the process is to be able to run a javascript demo (using webpack-serve). I know very, very little about web development, is it possible to re-enable this? I’ve messed around without success so far.

1 Like

@Philippe This template is awesome! I would like to achieve behavior similar to use_async of the (normal) template repo. I tried merging the relevant bits, but so far I haven’t been able to get it working. Is there an example anywhere I could look at anywhere? :slight_smile:

I don’t think there’s an example of the async components with the typescript template, best would be to generate one of both template and look at the diff in package.json, webpack.config.js and __init__.js.

Basically you will need to install the npm dependency:
npm i -D @plotly/webpack-dash-dynamic-import.
In webpack.config.js you add to plugins and optimizations:

        optimization: {
            splitChunks: {
                name: '[name].js',
                cacheGroups: {
                    async: {
                        chunks: 'async',
                        minSize: 0,
                        name(module, chunks, cacheGroupKey) {
                            return `${cacheGroupKey}-${chunks[0].name}`;
                        }
                    },
                    shared: {
                        chunks: 'all',
                        minSize: 0,
                        minChunks: 2,
                        name: '{{cookiecutter.project_shortname}}-shared'
                    }
                }
            }
        },
        plugins: [
            new WebpackDashDynamicImport(),
...

For the component it should be separated in two files, the one in components load another file with React.lazy(() => import("path")).
You can put the lazy component like so:

const LazyComponent = React.lazy(() => import("../fragments/MyComponent"));

const MyComponent = (props) => (
    <React.Suspense fallback={null}>
          <LazyComponent {props}/>
    </React.Suspense>
);

In __init__.py you need to add the async chunk to the _js_dist:


async_resources = [
    "MyComponent",
]

_js_dist = []

_js_dist.extend(
    [
        {
            "relative_package_path": "async-{}.js".format(async_resource),
            "external_url": (
                "https://unpkg.com/{0}@{2}"
                "/{1}/async-{3}.js"
            ).format(package_name, __name__, __version__, async_resource),
            "namespace": package_name,
            "async": True,
        }
        for async_resource in async_resources
    ]
)

# TODO: Figure out if unpkg link works
_js_dist.extend(
    [
        {
            "relative_package_path": "async-{}.js.map".format(async_resource),
            "external_url": (
                "https://unpkg.com/{0}@{2}"
                "/{1}/async-{3}.js.map"
            ).format(package_name, __name__, __version__, async_resource),
            "namespace": package_name,
            "dynamic": True,
        }
        for async_resource in async_resources
    ]
)

I think that should be all that is needed if you started from the ts template.

4 Likes

@Philippe Thank you for the tips! They are very much in line with what i did based on the non-typescript template. The only differences that I notice are that I also added chunkFilename to the webpack config output,

and that I also added a magic comment to specify the webpackChunkName in the lazy component import,

However, when I build the project, I only get a single asset[1], i.e. no splitting occurs. So something is still missing (or there’s a bug on my end). Do you see anything wrong?

[1] This is the log output,

> dash-leaflet@1.0.0 build
> npm run build:js && npm run build:backends


> dash-leaflet@1.0.0 build:js
> webpack

asset dash_leaflet.js 560 KiB [emitted] [minimized] [big] (name: main) 2 related assets
orphan modules 516 KiB [orphan] 486 modules
runtime modules 3.08 KiB 8 modules
built modules 1.31 MiB [built]
  modules by path ./node_modules/ 1.26 MiB
    javascript modules 1.25 MiB 86 modules
    asset modules 8.69 KiB 5 modules
  modules by path ./src/ts/ 50.4 KiB 30 modules
  modules by mime type image/svg+xml 1.37 KiB
    data:image/svg+xml;charset=UTF-8,<svg xmlns="http.. 272 bytes [built] [code generated]
    data:image/svg+xml;charset=UTF-8,<svg xmlns="http.. 555 bytes [built] [code generated]
    data:image/svg+xml;charset=UTF-8,<svg xmlns="http.. 288 bytes [built] [code generated]
    data:image/svg+xml;charset=UTF-8,<svg xmlns="http.. 288 bytes [built] [code generated]
  external "React" 42 bytes [built] [code generated]
  external "ReactDOM" 42 bytes [built] [code generated]

WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets: 
  dash_leaflet.js (560 KiB)

WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
  main (560 KiB)
      dash_leaflet.js


WARNING in webpack performance recommendations: 
You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.
For more info visit https://webpack.js.org/guides/code-splitting/

webpack 5.88.1 compiled with 3 warnings in 11065 ms

Starting from the Typescript template, I managed to get the async chunk generation working (!). In addition to the previously mentioned steps, I had to add

"module": "nodenext"

in the tsconfig.json file. With this setup, the chunk(s) are generated as intended for the (small) template. However, this change results in a change in the (default) value of moduleResolution from node to classic, which causes tons of errors like,

[tsl] ERROR in /home/emher/Code/dash-leaflet/src/ts/components/FeatureGroup.tsx(3,58)
      TS2792: Cannot find module 'react-leaflet'. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option?

in my larger project due to 3rd party library imports. If I switch back to node, i.e. change my tsconfig.json to[1],

"module": "nodenext",
"moduleResolution": "node",

The chunks are no longer generated :frowning: . I guess I could try to adopt my project to use modeResolution: "classic", but I really prefer modeResolution: "node". What do you think @Philippe ?

[1] There is a bug in the extract-meta.js script when it parses the compiler options from the tsconfig.json file. Instead of parsing the option into an option, it parses it into a string. It can be corrected using code like,

tsconfig.moduleResolution = tsconfig.moduleResolution == 'node' ? ts.ModuleResolutionKind.NodeJs : tsconfig.moduleResolution;

If you don’t add this fix, you’ll get an error like,

/home/emher/Code/tsconvert/node_modules/typescript/lib/typescript.js:2572
            throw e;
            ^

Error: Debug Failure. Unexpected moduleResolution: node
    at Object.resolveModuleName (/home/emher/Code/tsconvert/node_modules/typescript/lib/typescript.js:44046:37)
    at loader_1 (/home/emher/Code/tsconvert/node_modules/typescript/lib/typescript.js:119377:117)
    at loadWithModeAwareCache (/home/emher/Code/tsconvert/node_modules/typescript/lib/typescript.js:118997:46)
    at actualResolveModuleNamesWorker (/home/emher/Code/tsconvert/node_modules/typescript/lib/typescript.js:119378:149)
    at resolveModuleNamesWorker (/home/emher/Code/tsconvert/node_modules/typescript/lib/typescript.js:119667:26)
    at resolveModuleNamesReusingOldState (/home/emher/Code/tsconvert/node_modules/typescript/lib/typescript.js:119765:24)
    at processImportedModules (/home/emher/Code/tsconvert/node_modules/typescript/lib/typescript.js:121296:35)
    at findSourceFileWorker (/home/emher/Code/tsconvert/node_modules/typescript/lib/typescript.js:121076:17)
    at findSourceFile (/home/emher/Code/tsconvert/node_modules/typescript/lib/typescript.js:120922:26)
    at /home/emher/Code/tsconvert/node_modules/typescript/lib/typescript.js:120871:85

Node.js v18.16.1

when you run npm run build:backends.

Hmm, not sure if this could help you, but we had to lazy load AG Grid enterprise vs community (two components to one python class).

It is a little different.

@jinnyzor Thanks for the reference. However, I can see that you are not using Typescript? I guess that’s why you don’t see any issues :slight_smile:

I am not, just referring to how we did it.

Not sure if you can try something similar.

This will definitely need a patch to extract-meta.js for the different ModuleResolutionKind.

Maybe can try this config instead of classic:

"module": "es2022",
"moduleResolution": "NodeNext",
"target": "es2017",

With that config, I get lot’s of this kind of errors,

      TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("@react-leaflet/core")' call instead.
  To convert this file to an ECMAScript module, change its file extension to '.mts', or add the field `"type": "module"` to '/home/emher/Code/dash-leaflet/package.json'.

Since .mtsxis not a thing, that config would also require a significant amount of code changes :confused: . I’ll look into making a PR for extract-meta.js this week enabling different options for ModuleResolutionKind.

@Philippe With that config, I get lot’s of this kind of errors,

      TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("@react-leaflet/core")' call instead.
  To convert this file to an ECMAScript module, change its file extension to '.mts', or add the field `"type": "module"` to '/home/emher/Code/dash-leaflet/package.json'.

Since .mtsx is not a thing, that config would also require a significant amount of code changes :confused: . I have created a PR for the ModuleResolutionKind mapping bug.

EDIT: Using

    "moduleResolution": "node",
    "module": "esnext",

it seems to work :slight_smile:

T

This change needs to added where?

In the Dash library itself. Here is a small patch you can use until the fix is released,