Load packages only client side

I am trying to install react-konva to my fronting app, however I am having some issue.

I did a simple npm install react-konva konva to install the package.
When I try to load my app I get the following error:

ModuleNotFoundError: Module not found: Error: Can't resolve 'canvas' in '/Users/donkoko/devs/JS/kh-frontity-theme/node_modules/konva/lib'
    at /Users/donkoko/devs/JS/kh-frontity-theme/node_modules/webpack/lib/Compilation.js:925:10
    at /Users/donkoko/devs/JS/kh-frontity-theme/node_modules/webpack/lib/NormalModuleFactory.js:401:22
    at /Users/donkoko/devs/JS/kh-frontity-theme/node_modules/webpack/lib/NormalModuleFactory.js:130:21
    at /Users/donkoko/devs/JS/kh-frontity-theme/node_modules/webpack/lib/NormalModuleFactory.js:224:22
    at /Users/donkoko/devs/JS/kh-frontity-theme/node_modules/neo-async/async.js:2830:7
    at /Users/donkoko/devs/JS/kh-frontity-theme/node_modules/neo-async/async.js:6877:13
    at /Users/donkoko/devs/JS/kh-frontity-theme/node_modules/webpack/lib/NormalModuleFactory.js:214:25
    at /Users/donkoko/devs/JS/kh-frontity-theme/node_modules/enhanced-resolve/lib/Resolver.js:213:14
    at /Users/donkoko/devs/JS/kh-frontity-theme/node_modules/enhanced-resolve/lib/Resolver.js:285:5
    at eval (eval at create (/Users/donkoko/devs/JS/kh-frontity-theme/node_modules/tapable/lib/HookCodeFactory.js:33:10), <anonymous>:15:1)

From what I understand this happens because canvas is not available server side. Makes sense, however my question is how can I go around this? Is there a way to load the package just client side?

I also made a sandbox for testing: wizardly-carson-2hy24 - CodeSandbox
and here is the repository: https://github.com/DonKoko/frontity-konva-test

EDIT: Here is the solution that the Konva devs suggested when using GatsbyJS. However I am not aware of a way to modify webpack with frontity https://github.com/jsdom/jsdom/issues/3042

2 Likes

Hey @juanma & @mburridge. Sorry to ping you guys like this but we are quite stuck with this and I was hoping you could take a quick look and let give us some direction on how to handle this?

Thanks in advance.

Hi @ni.bonev

Take a look at this repo which dynamically imports Google Fonts into a Frontity project.

It uses webfontloader which only works client side, i.e. the problem that you’re having with konva, and so uses dynamic import in a beforeCSR action.

I think a similar solution should work for you. Hope this helps.

Thank you very much @mburridge. I will test it tomorrow morning. That seems exactly what I was looking for.

@ni.bonev

More on dynamic importing here.

Hey @ni.bonev

How did you get on with this?

Hey @mburridge,

unfortunately after many struggles I still keep on facing the same issue. I have tried to import it in any way I can think/see in the examples, and the error still persists. I have updated the codesandbox so you can see the error as well. I also tried using loadable but I keep on getting the same error:

    ERROR in ./node_modules/konva/lib/index-node.js
    Module not found: Error: Can't resolve 'canvas' in '/sandbox/node_modules/konva/lib'
     @ ./node_modules/konva/lib/index-node.js 1:39-69 1:82-99 1:101-107
     @ ./node_modules/react-konva/es/ReactKonva.js
     @ ./packages/mars-theme/src/components/konva-test.js
     @ ./packages/mars-theme/src/index.js
     @ ./build/bundling/entry-points/server.ts
ℹ 「wdm」: Failed to compile.

Here is again the link to the sandbox: wizardly-carson-2hy24 - CodeSandbox

EDIT: I also tried simply running npm install canvas and we get a different error then, related to the same issue with rendering on the server.

    ERROR in ./node_modules/canvas/build/Release/canvas.node 1:0
    Module parse failed: Unexpected character '�' (1:0)
    You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
    (Source code omitted for this binary file)
ModuleParseError: Module parse failed: Unexpected character '�' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
(Source code omitted for this binary file)
    at handleParseError (/Users/donkoko/devs/JS/frontity-examples/demo-using-google-fonts/node_modules/webpack/lib/NormalModule.js:469:19)
    at /Users/donkoko/devs/JS/frontity-examples/demo-using-google-fonts/node_modules/webpack/lib/NormalModule.js:503:5
    at /Users/donkoko/devs/JS/frontity-examples/demo-using-google-fonts/node_modules/webpack/lib/NormalModule.js:358:12
    at /Users/donkoko/devs/JS/frontity-examples/demo-using-google-fonts/node_modules/loader-runner/lib/LoaderRunner.js:373:3
    at iterateNormalLoaders (/Users/donkoko/devs/JS/frontity-examples/demo-using-google-fonts/node_modules/loader-runner/lib/LoaderRunner.js:214:10)
    at Array.<anonymous> (/Users/donkoko/devs/JS/frontity-examples/demo-using-google-fonts/node_modules/loader-runner/lib/LoaderRunner.js:205:4)
    at Storage.finished (/Users/donkoko/devs/JS/frontity-examples/demo-using-google-fonts/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:55:16)
    at /Users/donkoko/devs/JS/frontity-examples/demo-using-google-fonts/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:91:9
    at /Users/donkoko/devs/JS/frontity-examples/demo-using-google-fonts/node_modules/graceful-fs/graceful-fs.js:123:16
    at FSReqCallback.readFileAfterClose [as oncomplete] (internal/fs/read_file_context.js:63:3)

EDIT: This is not the final solution. Please look at the comment below for the solution. I am leaving this here so we can have a full trace of the problem solving.

I am posting this an update as I managed to make this work, however the solution involves a hack that I am unhappy with and will be looking to solve.
So in the end, no matter what approach I would try to take with the import, webpack would keep on giving me errors about canvas, even after I installed canvas specifically.

After many hours of digging, I realized that the only realistic solution would be to modify webpack config as per this comment: https://github.com/konvajs/react-konva/issues/102#issuecomment-308000612

So, as my webpack knowledge is quite basic, the only way I could do it is to actually modify the webpack files within node_modules/@frontity/core/dist/src/config/webpack/plugins.js.

NOTE: I know this is not the correct way of doing it and that updating will overwrite the changes, however it is the only way I could manage to make it work.

Inside plugins.js I modified the line with the ignoring of folders to this:
config.push(new webpack_1.WatchIgnorePlugin([new RegExp(outDir)]), new webpack_1.IgnorePlugin(/^encoding$/), new webpack_1.IgnorePlugin(/canvas|jsdom/, /konva/));

Once this was done webpack could actually compile the code.

The next step was using Konva. I have made a seperate package where Konva is used, called customizer. Inside that package the index.js file looks like this:

export default {
  name: "kh-product-customizer",
  roots: {},
  state: {
    customizer: {
      visible: false,
    },
  },
  libraries: {
    customizer: {
      components: {},
    },
  },
  actions: {
    customizer: {
      beforeCSR: async ({ libraries, roots }) => {
        let reactKonva = await loadable(() => import("./Components/index.js"));
        libraries.customizer.Component = reactKonva
      },
    },
  },
};

An extra note, I could only make it work with loadable. If i tried to use import as in the example above, it would keep giving me errors about the component not being exported properly.

Last but not least, in the component where I actually want to show the Konva component, I have the following:

const ProductCustomizer = libraries.customizer.Component

and within the return I have:

{ProductCustomizer && <ProductCustomizer/>}

As some extra info, for testing I was using one of the example components from Konva that looks like this:

import React from "react";
import { useConnect, connect } from 'frontity'
import { Stage, Layer, Rect, Transformer } from "react-konva";

const Rectangle = ({ shapeProps, isSelected, onSelect, onChange }) => {
  const shapeRef = React.useRef();
  const trRef = React.useRef();

  React.useEffect(() => {
    if (isSelected) {
      // we need to attach transformer manually
      trRef.current.nodes([shapeRef.current]);
      trRef.current.getLayer().batchDraw();
    }
  }, [isSelected]);

  return (
    <React.Fragment>
      <Rect
        onClick={onSelect}
        onTap={onSelect}
        ref={shapeRef}
        {...shapeProps}
        draggable
        onDragEnd={(e) => {
          onChange({
            ...shapeProps,
            x: e.target.x(),
            y: e.target.y(),
          });
        }}
        onTransformEnd={(e) => {
          // transformer is changing scale of the node
          // and NOT its width or height
          // but in the store we have only width and height
          // to match the data better we will reset scale on transform end
          const node = shapeRef.current;
          const scaleX = node.scaleX();
          const scaleY = node.scaleY();

          // we will reset it back
          node.scaleX(1);
          node.scaleY(1);
          onChange({
            ...shapeProps,
            x: node.x(),
            y: node.y(),
            // set minimal value
            width: Math.max(5, node.width() * scaleX),
            height: Math.max(node.height() * scaleY),
          });
        }}
      />
      {isSelected && (
        <Transformer
          ref={trRef}
          boundBoxFunc={(oldBox, newBox) => {
            // limit resize
            if (newBox.width < 5 || newBox.height < 5) {
              return oldBox;
            }
            return newBox;
          }}
        />
      )}
    </React.Fragment>
  );
};

const initialRectangles = [
  {
    x: 10,
    y: 10,
    width: 100,
    height: 100,
    fill: "red",
    id: "rect1",
  },
  {
    x: 150,
    y: 150,
    width: 100,
    height: 100,
    fill: "green",
    id: "rect2",
  },
];

const App = () => {
  const [rectangles, setRectangles] = React.useState(initialRectangles);
  const [selectedId, selectShape] = React.useState(null);
  const { state } = useConnect();
  const checkDeselect = (e) => {
    // deselect when clicked on empty area
    const clickedOnEmpty = e.target === e.target.getStage();
    if (clickedOnEmpty) {
      selectShape(null);
    }
  };
  console.log("state", state)
  return (
    <Stage
      width={window.innerWidth}
      height={window.innerHeight}
      onMouseDown={checkDeselect}
      onTouchStart={checkDeselect}
    >
      <Layer>
        {rectangles.map((rect, i) => {
          return (
            <Rectangle
              key={i}
              shapeProps={rect}
              isSelected={rect.id === selectedId}
              onSelect={() => {
                selectShape(rect.id);
              }}
              onChange={(newAttrs) => {
                const rects = rectangles.slice();
                rects[i] = newAttrs;
                setRectangles(rects);
              }}
            />
          );
        })}
      </Layer>

    </Stage>
  )
};

export default connect(App, { injectProps: false });

This is the best way I could make this work for now. If I have some improvements I will post them here for anyone needing this in the future.

Okey, after facing even more issues, I kept on digging further and it turns out, there is actually a way to modify the webpack config. It is not documented yet so it was tricky to find and understand but in the end it solved all issues including the need of doing a dynamic import.
So the final solution involved adding the frontity.config.js file with the following settings:

const webpack_1 = require("webpack");

export const webpack = ({ config, mode }) => {
  config.plugins.push(new webpack_1.IgnorePlugin(/canvas|jsdom/, /konva/));
};

I am not sure if i need to actually require webpack or I can access it somehow, but this approach works. Also once this is done, no dynamic imports are required. You can import Konva component in the normal way.

1 Like