react-konva: React Konva doesn't work with React 16.3 Context API

I am trying to use React 16.3 Context API based on render props with React Konva:

import React from "react";
import { Layer, Stage, Circle, Group, Line } from "react-konva";

const { Consumer, Provider } = React.createContext({ width: 0, height: 0 });

const ToolsLayer = () => (
  <Consumer>
    {({ height, width }) => (
      <Layer>
        <Group offsetY={-height} y={-42}>
          <Line
            points={[0, 0, width, 0, width, 42, 0, 42]}
            closed
            stroke="black"
          />
          <Circle radius={11} fill="red" stroke="black" x={21} y={21} />
          <Circle radius={11} fill="green" stroke="black" x={21 + 42} y={21} />
          <Group />
        </Group>
      </Layer>
    )}
  </Consumer>
);

export default function Canvas({
  width = window.innerWidth,
  height = window.innerHeight
}) {
  return (
    <Provider value={{ width, height }}>
      <Stage width={width} height={height}>
        <ToolsLayer />
      </Stage>
    </Provider>
  );
}

And I get runtime error:

Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.

Check the render method of `ToolsLayer`.

Reproducible demo: https://codesandbox.io/s/2o9j1r6l30

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 13
  • Comments: 26 (7 by maintainers)

Commits related to this issue

Most upvoted comments

Demo how to “bridge” contexts into react-konva tree:

import React, { Component } from "react";
import Konva from "konva";
import { render } from "react-dom";
import { Stage, Layer, Rect } from "react-konva";

const ThemeContext = React.createContext("red");

const ThemedRect = () => {
  const value = React.useContext(ThemeContext);
  return (
    <Rect x={20} y={50} width={100} height={100} fill={value} shadowBlur={10} />
  );
};

const Canvas = () => {
  return (
    <ThemeContext.Consumer>
      {value => (
        <Stage width={window.innerWidth} height={window.innerHeight}>
          <ThemeContext.Provider value={value}>
            <Layer>
              <ThemedRect />
            </Layer>
          </ThemeContext.Provider>
        </Stage>
      )}
    </ThemeContext.Consumer>
  );
};

class App extends Component {
  render() {
    return (
      <ThemeContext.Provider value="blue">
        <Canvas />
      </ThemeContext.Provider>
    );
  }
}

render(<App />, document.getElementById("root"));

https://codesandbox.io/s/ykqw8r4r21

The usage of context api should probably be an example on the offical docs site, there is no mention there of this problem/pitfall and I only found this due to some google searches (which first led me in other directions).

Update on this issue. From react-konva@18.2.2, context bridge should work by default. It will be really cool if someone can try it and provide feedback.

@lyleunderwood Thanks! I actually had implemented it in such a way for the <Stage>, but was also using the <Html> component from react-konva-utils, which requires a second bridge. So I mistakenly thought it wasn’t bridging correctly.

Similarly for MUI we should be able to do something like:

import { ThemeProvider, useTheme } from '@material-ui/core/styles';

const Canvas = () => {
  const theme = useTheme();

  return (
    <Stage>
      <ThemeProvider theme={theme}>
        <Layer>
          {/* ... */}
        </Layer>
      </ThemeProvider>
    </Stage>
  );
};

I encountered this issue with my project. After playing with it, I realize that we can skip the bridge by declaring the context consumer inside Stage. So something like this will work just fine.

import React, { Component } from "react";
import { render } from "react-dom";
import { Stage, Layer, Rect } from "react-konva";

const ThemeContext = React.createContext();

const ThemedRect = () => {
  const value = React.useContext(ThemeContext);
  return (
    <Rect x={20} y={50} width={100} height={100} fill={value} shadowBlur={10} />
  );
};

const Canvas = () =>  (
  <Stage width={window.innerWidth} height={window.innerHeight}>
    <ThemeContext.Provider value="blue">
      <Layer>
        <ThemedRect />
      </Layer>
    </ThemeContext.Provider>
  </Stage>
)

class App extends Component {
  render() {
    return <Canvas />
  }
}

render(<App />, document.getElementById("root"));

https://codesandbox.io/s/react-konva-consume-context-demo-d5yht

@Guria basically facebook/react#13728 is an API change which will make the necessity of re-introducing your context into the hierarchy at a point below the point at which the context barrier happens (the Stage in the example of react-konva) less ugly. So instead of:

<MyConsumer>
  {value =>
    <Stage>
      <MyContext.Provider value={value}>
        <Stuff />
      </MyContext.Provider>
    </Stage>
  }
</MyConsumer>

you have:

// or whatever your top level react-konva component where you care about context is
class MyStage extends React.Component {
  static contextType = MyContext;
  render() {
    return <Stage>...</Stage>;
  }
}

// ...

<MyStage>
  <Stuff />
</MyStage>

Because facebook/react#13728 doesn’t implement any kind of introspection of context solution, I don’t think that it offers a fix that react-konva could implement, and I think that https://github.com/facebook/react/issues/13332 would be a potential direction for solving that problem.

Does this sound about right @gaearon ?

Maybe allowing for something like this:

function useForeignContext(context) {
  const { subscribeContext } = useContext(ROOTCONTEXT)
  const [wrappedContext, unsubscribe] = useMemo(
    () => subscribeContext(context)
  , [context])
  useEffect(() => unsubscribe, [context])
  return useContext(wrappedContext)
}

Which would transport context into the reconciler tree. The root component that still belongs to the dom (Stage in konvas case) could provide the subscribeContext function, which adds a dom-tied context into a list and returns a new context that provides changes values into the reconciler tree.

This is still broken on v1.7.2. You can see in the reproducible demo linked above that it’s picking up the default value defined by the initial creation of the context ({width: 0, height: 0}) rather than the values provided by Provider.