Ref jest null kidy podaje go przez props

0

Hej, mam nastepujacy kod ktorego celem jest zmiana rozmiaru diva:

export const EditorResizer = ({
  targetId,
  ref,
  editor,
}: {
  targetId: string;
  editor: Editor;
  ref: any;
}) => {
  let resizerRef = useRef<HTMLDivElement>(null);

  let currentPosition: number;
  let mouseDown: boolean;
  let lastClientY: number;

  function touchMouseStart(e: any) {
    e.preventDefault();
    mouseDown = true;
    if (e.touches) currentPosition = e.touches[0].clientY;
    else currentPosition = e.clientY;
    // lastClientY = document.getElementById(targetId)!.offsetHeight;
    lastClientY = ref.current.offsetHeight;
  }

  function touchMouseEnd(e: any) {
    e.preventDefault();
    mouseDown = false;
    // lastClientY = document.getElementById(targetId)!.offsetHeight;
    lastClientY = ref.current.offsetHeight;
    editor?.commands.focus();
  }

  function touchMouseMove(e: any) {
    e.preventDefault();
    if (!mouseDown) return;
    if (e.touches) var position = e.touches[0].clientY;
    else var position = e.clientY;
    var diff = position - currentPosition;

    // document.getElementById(targetId)!.style.height = lastClientY + diff + 'px';
    ref.current.style.height = lastClientY + diff + 'px';
  }

  return (
    <div
      ref={resizerRef}
      style={{
        backgroundSize: '45px',
      }}
      className={`mr-0 h-[30px] w-full cursor-s-resize rounded-b-[6px] border-t-[1px] bg-muted bg-[url("/grabber-vertical.svg")] bg-center bg-no-repeat dark:bg-[#0a04040a] dark:bg-[url("/grabber-vertical-dark.svg")]`}
      onPointerDown={(e: any) =>
        resizerRef.current?.setPointerCapture(e.pointerId)
      }
      onPointerUp={(e: any) =>
        resizerRef.current?.releasePointerCapture(e.pointerId)
      }
      onMouseDown={touchMouseStart}
      onMouseUp={touchMouseEnd}
      onTouchStart={touchMouseStart}
      onTouchEnd={touchMouseEnd}
      onTouchCancel={touchMouseEnd}
      onClick={(e) => e.preventDefault()}
      onTouchMove={touchMouseMove}
      onMouseMove={touchMouseMove}
    ></div>
  );
};

Wolam go tak:

return (
  <div
    className={cn(
      'relative z-10 flex w-full flex-col rounded-[6px] border-[1px] border-border bg-background',
      editorVariants({ className, variant })
    )}
  >
    <MenuBar editor={editor!} />

    <EditorContent
      id='editor-content'
      ref={editorContentRef}
      editor={editor}
      className='prose w-full flex-grow-[1] overflow-auto p-[12px] pt-[6px]'
      onClick={() => editor?.commands.focus()}
      style={{ height: height + 'px' }}
    />
    {resizable && <EditorResizer ref={editorContentRef} editor={editor!} targetId='editor-content' />}
  </div>
);

Wszytko dziala kiedy uzywam document.getElementById() (zakomentowane linie), ale niestety nie dziala gdy uzywam refa ktory jest zawsze null wewnatrz touchMouseStart(). Co powinienem poprawic zeby nie byl null i zmiana rozmiaru dzialala?

0

React rezerwuje sobie kilka atrybutów w notacji JSX które zachowują się inaczej. Jednym z nich jest ref, innym np jest key. Kiedy przekażesz atrybut ref={} w notacji JSX, to react nie przekaże tej wartości jako prop do dziecka, tylko zrobi z tego aktualnego komponentu ref'a (żeby dziecko mogło być użyte z useRef() w parencie).

W pewnym sensie atrybuty ref oraz key są specjalne, i nie zachowują się jak "zwykłe" property (np nie są przekazywane do dzieci).

Masz dwie alternatywy:

0
Riddle napisał(a):

React rezerwuje sobie kilka atrybutów w notacji JSX które zachowują się inaczej. Jednym z nich jest ref, innym np jest key. Kiedy przekażesz atrybut ref={} w notacji JSX, to react nie przekaże tej wartości jako prop do dziecka, tylko zrobi z tego aktualnego komponentu ref'a (żeby dziecko mogło być użyte z useRef() w parencie).

W pewnym sensie atrybuty ref oraz key są specjalne, i nie zachowują się jak "zwykłe" property (np nie są przekazywane do dzieci).

Masz dwie alternatywy:

Sprobowalem obu metod i w obu przypadkach tym razem ref.current (lub targetRef.current w przypadku 1 metody) jest null a nie sam ref.

0
Kokos12345 napisał(a):
    <EditorContent
      id='editor-content'
      ref={editorContentRef}
      editor={editor}
      className='prose w-full flex-grow-[1] overflow-auto p-[12px] pt-[6px]'
      onClick={() => editor?.commands.focus()}
      style={{ height: height + 'px' }}
    />
    {resizable && <EditorResizer ref={editorContentRef} editor={editor!} targetId='editor-content' />}
  </div>
);

Tak nie powinno być że masz ref={editorContentRef} jednocześnie w obu komponentach. Do <EditorResizer> powinieneś przekazać editorContentRef po prostu jako zwykły prop, nie ref.

Bo ref to jest jakby "zapięcie komponentu" w reacie, a Ty chcesz to zrobić tylko na <EditorContent> - więc tylko EditorContent powinien wystawić ref={}. Ten drugi komponent <EditorResizer>, on nie powinien wystawiać refa, tylko go przyjąć - a to powinieneś zrobić zwykłym propem, jak component={} albo nawet pewnie możesz w children={}.

0
Riddle napisał(a):
Kokos12345 napisał(a):
    <EditorContent
      id='editor-content'
      ref={editorContentRef}
      editor={editor}
      className='prose w-full flex-grow-[1] overflow-auto p-[12px] pt-[6px]'
      onClick={() => editor?.commands.focus()}
      style={{ height: height + 'px' }}
    />
    {resizable && <EditorResizer ref={editorContentRef} editor={editor!} targetId='editor-content' />}
  </div>
);

Tak nie powinno być że masz ref={editorContentRef} jednocześnie w obu komponentach. Do <EditorResizer> powinieneś przekazać editorContentRef po prostu jako zwykły prop, nie ref.

Bo ref to jest jakby "zapięcie komponentu" w reacie, a Ty chcesz to zrobić tylko na <EditorContent> - więc tylko EditorContent powinien wystawić ref={}. Ten drugi komponent <EditorResizer>, on nie powinien wystawiać refa, tylko go przyjąć - a to powinieneś zrobić zwykłym propem, jak component={} albo nawet pewnie możesz w children={}.

Tak zrobilem, w sensie zmienilem nazwe na targetRef i podalem przez to editorContentRef. Problem w tym ze current na tym obiekcie jest caly czas null kiedy wolam lastClientY = targetRef.current.offsetHeight;

0
Kokos12345 napisał(a):

Tak zrobilem, w sensie zmienilem nazwe na targetRef i podalem przez to editorContentRef. Problem w tym ze current na tym obiekcie jest caly czas null kiedy wolam lastClientY = targetRef.current.offsetHeight;

Pokaż kod.

0
Riddle napisał(a):
Kokos12345 napisał(a):

Tak zrobilem, w sensie zmienilem nazwe na targetRef i podalem przez to editorContentRef. Problem w tym ze current na tym obiekcie jest caly czas null kiedy wolam lastClientY = targetRef.current.offsetHeight;

Pokaż kod.

Edytor.tsx

  let editorContentRef = useRef<HTMLDivElement>(null);

  const [height, setHeight] = useState<number>(200);

  return (
    <div
      className={cn(
        'relative z-10 flex w-full flex-col rounded-[6px] border-[1px] border-border bg-background',
        editorVariants({ className, variant })
      )}
    >
      <MenuBar editor={editor!} />

      <EditorContent
        id='editor-content'
        ref={editorContentRef}
        editor={editor}
        className='prose w-full flex-grow-[1] overflow-auto p-[12px] pt-[6px]'
        onClick={() => editor?.commands.focus()}
        // style={{ height: height + 'px' }}
      />
      {resizable && (
        <EditorResizer
          targetRef={editorContentRef}
          editor={editor!}
          targetId='editor-content'
        />
      )}
    </div>
  );
}

EditorResizer.tsx

interface EditorResizerProps {
  targetId: string;
  editor: Editor;
  targetRef: any;
}

export const EditorResizer = (props: EditorResizerProps) => {
  const { targetId, editor, targetRef } = props;
  let resizerRef = useRef<HTMLDivElement>(null);

  let currentPosition: number;
  let mouseDown: boolean;
  let lastClientY: number;

  function touchMouseStart(e: any) {
    e.preventDefault();
    mouseDown = true;
    if (e.touches) currentPosition = e.touches[0].clientY;
    else currentPosition = e.clientY;
    // lastClientY = document.getElementById(targetId)!.offsetHeight;
    lastClientY = targetRef.current.offsetHeight;
  }

  function touchMouseEnd(e: any) {
    e.preventDefault();
    mouseDown = false;
    // lastClientY = document.getElementById(targetId)!.offsetHeight;
    lastClientY = targetRef.current.offsetHeight;
    editor?.commands.focus();
  }

  function touchMouseMove(e: any) {
    e.preventDefault();
    if (!mouseDown) return;
    if (e.touches) var position = e.touches[0].clientY;
    else var position = e.clientY;
    var diff = position - currentPosition;

    // document.getElementById(targetId)!.style.height = lastClientY + diff + 'px';
    targetRef.current.style.height = lastClientY + diff + 'px';
  }

  return (
    <div
      ref={resizerRef}
      style={{
        backgroundSize: '45px',
      }}
      className={`mr-0 h-[30px] w-full cursor-s-resize rounded-b-[6px] border-t-[1px] bg-muted bg-[url("/grabber-vertical.svg")] bg-center bg-no-repeat dark:bg-[#0a04040a] dark:bg-[url("/grabber-vertical-dark.svg")]`}
      onPointerDown={(e: any) =>
        resizerRef.current?.setPointerCapture(e.pointerId)
      }
      onPointerUp={(e: any) =>
        resizerRef.current?.releasePointerCapture(e.pointerId)
      }
      onMouseDown={touchMouseStart}
      onMouseUp={touchMouseEnd}
      onTouchStart={touchMouseStart}
      onTouchEnd={touchMouseEnd}
      onTouchCancel={touchMouseEnd}
      onClick={(e) => e.preventDefault()}
      onTouchMove={touchMouseMove}
      onMouseMove={touchMouseMove}
    ></div>
  );
};

0
   onPointerDown={(e: any) =>
        resizerRef.current?.setPointerCapture(e.pointerId)

po co używasz resizerRef.current, zamiast e.target? (i bez Reacta masz e.target, ale w React też można używać).

Ogólnie ten wątek to trochę jak używam Reacta, ale zamiast oczywistych wzorców, będę robił po swojemu, bo tak.

Tylko, że to się nie opłaca, tracisz czas, bo nie chce ci się nauczyć podstaw tej biblioteki. Któryś z kolei wątek. A gdybyś poczytał dokumentację, zobaczył też jakieś projekty pisane w React, próbował zrozumieć model mentalny Reacta (a nie tylko korzystał z escape hatch, jakim jest przypisanie ref do diva), to te wątki w ogóle nie byłyby potrzebne, bo sam byś wiedział, jak to należy zrobić.

0
LukeJL napisał(a):
   onPointerDown={(e: any) =>
        resizerRef.current?.setPointerCapture(e.pointerId)

po co używasz resizerRef.current, zamiast e.target? (i bez Reacta masz e.target, ale w React też można używać).

Ogólnie ten wątek to trochę jak używam Reacta, ale zamiast oczywistych wzorców, będę robił po swojemu, bo tak.

On nie z tym refem ma problem, tylko tym drugim. Tym editorContentRef.

0

Niby tak, ale na to samo wychodzi.
Zamiast bawić się w ref można użyć bardziej idiomatycznego sposobu.
Np. jeśli mamy coś takiego:

function touchMouseStart(e: any) {
    //...
    lastClientY = ref.current.offsetHeight;
}
function touchMouseMove(e: any) {
   //...
   ref.current.style.height = lastClientY + diff + 'px';

ten kod bym zamienił na taki:

function touchMouseStart(e: any) {
    //...
    onStart();
}
 
function touchMouseMove(e: any) {
   //...
  onMove(diff);

gdzie onStart i onMove byłyby to propsy komponentu EditorResizer. Natomiast to komponent wyżej by był odpowiedzialny za zapamiętanie ostatniego style.height i na ustawienie nowego (być może też za pomocą ref.current, ale być może za pomocą useState i ustawienia stylu w JSX:

const [currentHeight, setCurrentHeight] = useState(....);
...
<div style={{height: currentHeight}}>....

Albo jeszcze inaczej to rozwiązać (być może wysokość faktycznie należałoby zainicjalizować najpierw faktyczną wartością odczytaną z DOM). No ale chodzi mi o to, żeby nie próbować na siłę robić w React apki napisanej w stylu jQuery, gdzie wszystko jest zmieniane imperatywnie (zamiast deklaratywnie, jeśli się da) i bez poszanowania granic między komponentami.

0
LukeJL napisał(a):

Niby tak, ale na to samo wychodzi.
Zamiast bawić się w ref można użyć bardziej idiomatycznego sposobu.
Np. jeśli mamy coś takiego:

function touchMouseStart(e: any) {
    //...
    lastClientY = ref.current.offsetHeight;
}
function touchMouseMove(e: any) {
   //...
   ref.current.style.height = lastClientY + diff + 'px';

ten kod bym zamienił na taki:

function touchMouseStart(e: any) {
    //...
    onStart();
}
 
function touchMouseMove(e: any) {
   //...
  onMove(diff);

gdzie onStart i onMove byłyby to propsy komponentu EditorResizer. Natomiast to komponent wyżej by był odpowiedzialny za zapamiętanie ostatniego style.height i na ustawienie nowego (być może też za pomocą ref.current, ale być może za pomocą useState i ustawienia stylu w JSX:

const [currentHeight, setCurrentHeight] = useState(....);
...
<div style={{height: currentHeight}}>....

Albo jeszcze inaczej to rozwiązać (być może wysokość faktycznie należałoby zainicjalizować najpierw faktyczną wartością odczytaną z DOM). No ale chodzi mi o to, żeby nie próbować na siłę robić w React apki napisanej w stylu jQuery, gdzie wszystko jest zmieniane imperatywnie (zamiast deklaratywnie, jeśli się da) i bez poszanowania granic między komponentami.

Sprobowalem to przepisac zeby uzyc state i useState ale rozmiar zmienia sie o 1px i na tym sie konczy. Chyba przez to ze komponent sie re-renderuje. Jak to poprawic?

editor.tsx

  const [height, setHeight] = useState<number>(200);

  return (
    <div
      className={cn(
        'relative z-10 flex w-full flex-col rounded-[6px] border-[1px] border-border bg-input',
        editorVariants({ className, variant })
      )}
    >
      <MenuBar editor={editor!} />

      <EditorContent
        id='editor-content'
        ref={editorContentRef}
        editor={editor}
        className='prose w-full flex-grow-[1] overflow-auto bg-input p-[12px] pt-[6px]'
        onClick={() => editor?.commands.focus()}
        style={{ height: height + 'px' }}
      />
      {resizable && (
        <EditorResizer
          targetRef={editorContentRef}
          editor={editor!}
          targetId='editor-content'
          height={height}
          setHeight={setHeight}
        />
      )}
    </div>
  );
}

resizer.tsx

import { Editor } from '@tiptap/react';
import { forwardRef, useRef } from 'react';

interface EditorResizerProps {
  targetId: string;
  editor: Editor;
  targetRef: any;
  setHeight: any;
  height: any;
}

export const EditorResizer = (props: EditorResizerProps) => {
  const { targetId, editor, targetRef, setHeight, height } = props;
  let resizerRef = useRef<HTMLDivElement>(null);

  let currentPosition: number;
  let mouseDown: boolean;
  let lastClientY: number;

  function touchMouseStart(e: any) {
    e.preventDefault();
    mouseDown = true;
    if (e.touches) currentPosition = e.touches[0].clientY;
    else currentPosition = e.clientY;
    lastClientY = height; // document.getElementById(targetId)!.offsetHeight;
    // lastClientY = targetRef.current.offsetHeight;
  }

  function touchMouseEnd(e: any) {
    e.preventDefault();
    mouseDown = false;
    lastClientY = document.getElementById(targetId)!.offsetHeight;
    // lastClientY = targetRef.current.offsetHeight;
    editor?.commands.focus();
  }

  function touchMouseMove(e: any) {
    e.preventDefault();
    if (!mouseDown) return;
    if (e.touches) var position = e.touches[0].clientY;
    else var position = e.clientY;
    var diff = position - currentPosition;

    // document.getElementById(targetId)!.style.height = lastClientY + diff + 'px';
    // targetRef.current.style.height = lastClientY + diff + 'px';
    setHeight(lastClientY + diff);
  }

  return (
    <div
      ref={resizerRef}
      style={{
        backgroundSize: '45px',
      }}
      className={`mr-0 h-[30px] w-full cursor-s-resize rounded-b-[6px] border-t-[1px] bg-muted bg-[url("/grabber-vertical.svg")] bg-center bg-no-repeat dark:bg-[#0a04040a] dark:bg-[url("/grabber-vertical-dark.svg")]`}
      onPointerDown={(e: any) =>
        resizerRef.current?.setPointerCapture(e.pointerId)
      }
      onPointerUp={(e: any) =>
        resizerRef.current?.releasePointerCapture(e.pointerId)
      }
      onMouseDown={touchMouseStart}
      // onMouseUp={touchMouseEnd}
      onTouchStart={touchMouseStart}
      // onTouchEnd={touchMouseEnd}
      // onTouchCancel={touchMouseEnd}
      onClick={(e) => e.preventDefault()}
      onTouchMove={touchMouseMove}
      onMouseMove={touchMouseMove}
    ></div>
  );
};

0
 let lastClientY: number;

to akurat mógłbyś otoczyć ref, żeby zapamiętać stan tej zmiennej, coś jak:

const lastClientY = useRef();
//...
lastClientY.current = document.getElementById(targetId)!.offsetHeight; 

https://react.dev/learn/referencing-values-with-refs

Poza tym używanie document.getElementById(targetId) zadziała (jeśli masz prawidłowe id ustawione, gdzie trzeba), ale też jest niepotrzebne i przeciwko założeniom Reacta (piszesz w React, ale naprawdę skorzystałbyś na tym, jakbyś wywalił kompletnie Reacta i robił to na czystym DOM. Samo używanie Reacta jest antyproduktywne moim zdaniem w tym przypadku - nic nie zyskujesz, a tylko tracisz czas na debugowaniu).

1 użytkowników online, w tym zalogowanych: 0, gości: 1