1

I have App.tsx which contains 2 sibling components:

  • Konva.tsx: It has the Canvas
  • Options.tsx: It has a Download Canvas button

So I created a ref named stageRef in App.tsx to pass it to Konva.tsx & Options.tsx. I use React.forwardRef to forward refs to child components.

App.tsx

import * as React from 'react'
import type { Stage as StageType } from 'konva/types/Stage'

import { Konva, Options } from '@/components/index'
import { FrameItProvider } from '@/store/index'

const App = () => {
  const stageRef = React.createRef<StageType>()

  return (
    <>
      <Konva ref={stageRef} />
      <Options ref={stageRef} />
    </>
  )
}

export default App

In Konva.tsx, the ref points to the Canvas so it can access the element in the DOM.

Konva.tsx

import * as React from 'react'
import { observer } from 'mobx-react'

import { useFrameItStore } from '@/store/index'
import { BrowserWindow } from '@/components/index'

import type { Window } from '@/types/index'
import type { Stage as StageType } from 'konva/types/Stage'

interface IProps {
  className?: string
}

export const Konva = observer(
  React.forwardRef<StageType, IProps>(({ className }: IProps, forwardedRef) => {
    const frameItStore = useFrameItStore()
    const browser: Window = frameItStore.browser

    return (
      <>
        <Stage
          width={browser.width}
          height={browser.height}
          ref={forwardedRef}
          className={className}
        >
          <Layer>
            <BrowserWindow />
          </Layer>
        </Stage>
      </>
    )
  })
)

In Options.tsx, I trigger the download call using downloadImage with the forwardedRef.

Options.tsx

import * as React from 'react'
import { observer } from 'mobx-react'
import type { Stage as StageType } from 'konva/types/Stage'

import { useFrameItStore } from '@/store/index'
import type { TrafficSignalStyle } from '@/types/index'

interface IProps {
  className?: string
}

export const Options = observer(
  React.forwardRef<StageType, IProps>((props: IProps, forwardedRef) => {
    const frameItStore = useFrameItStore()

    const downloadImage: (stageRef: React.ForwardedRef<StageType>) => void =
      frameItStore.downloadImage

    return (
      <div>
        <button onClick={() => downloadImage(forwardedRef)}>
          Download Canvas
        </button>
      </div>
    )
  })
)

I'm using MobX to manage my store. However, the forwardRef causes problem.

store/index.ts

import type { Stage as StageType } from 'konva/types/Stage'

import type { IFrameItStore } from '@/types/index'

export class FrameItStore implements IFrameItStore {
  downloadImage(stageRef: React.ForwardedRef<StageType>) {
    console.log(stageRef)
    stageRef
      .current!.getStage()
      .toDataURL({ mimeType: 'image/jpeg', quality: 1 })
  }
}

types/index.ts

export interface IFrameItStore {
    downloadImage(stageRef: React.ForwardedRef<StageType>): void
}

I get 2 TypeScript errors in store/index.ts:

TS2531: Object is possibly 'null'.

on stageRef when I try to access stageRef.current and

  TS2339: Property 'current' does not exist on type '((instance: Stage | null) => void) | MutableRefObject<Stage | null>'.

Property 'current' does not exist on type '(instance: Stage | null) => void'.

on current

I tried not using ForwardedRef but it gave error that the types do not match so I have to use ForwardedRef but I'm not sure how to solve this?

deadcoder0904
  • 7,232
  • 12
  • 66
  • 163

1 Answers1

0

I made a ref in App.tsx & then passed it to Konva.tsx. I catched the ref using React.forwardRef function.

Then I used the lesser known useImperativeHandle hook in Konva.tsx to access the function downloadImage in App.tsx & then pass the function downloadImage directly to Options.tsx.

I didn't keep anything in MobX store. Just kept it locally in Konva component where its easily accessible. The old rule is that if it isn't going to be used in any other component, then it should be kept as close to the component as possible.

App.tsx

import * as React from 'react'

import { Konva, Options } from '@/components/index'

const App = () => {
  const stageRef = React.useRef<{ downloadImage: Function }>(null)

  return (
    <>
      <Konva ref={stageRef} />
      <Options downloadImage={() => stageRef?.current?.downloadImage()} />
    </>
  )
}

export default App

Konva.tsx

import * as React from 'react'
import { observer } from 'mobx-react'

import { useFrameItStore } from '@/store/index'
import { BrowserWindow } from '@/components/index'

import type { Window } from '@/types/index'
import type { Stage as StageType } from 'konva/types/Stage'

interface IProps {
  className?: string
}

interface ForwardedRef {
  downloadImage: Function
}

export const Konva = observer(
  React.forwardRef<ForwardedRef, IProps>(
    ({ className }: IProps, forwardedRef) => {
      const frameItStore = useFrameItStore()
      const browser: Window = frameItStore.browser

      const stageRef = React.useRef<StageType>(null)

      React.useImperativeHandle(
        forwardedRef,
        () => ({
          downloadImage: () =>
            stageRef.current
              ?.getStage()
              .toDataURL({ mimeType: 'image/jpeg', quality: 1 }),
        }),
        []
      )

      return (
        <Stage
          width={browser.width}
          height={browser.height}
          ref={stageRef}
          className={className}
        >
          <Layer>
            <BrowserWindow />
          </Layer>
        </Stage>
      )
    }
  )
)

Options.tsx

import * as React from 'react'
import { observer } from 'mobx-react'

interface IProps {
  className?: string
  downloadImage: Function
}

export const Options = observer((props: IProps) => {
  const { downloadImage } = props

  return (
    <div>
      <button onClick={() => console.log(downloadImage())}>
        Download Image
      </button>
    </div>
  )
})

Here's a complete working demo on CodeSandBox → https://codesandbox.io/s/react-konva-share-refs-between-siblings-dsd97?file=/src/App.tsx

deadcoder0904
  • 7,232
  • 12
  • 66
  • 163