MatthewHerbst / react-to-print

Print React components in the browser. Supports Chrome, Safari, Firefox and EDGE
MIT License
2.06k stars 221 forks source link

"react-to-print" did not receive a `contentRef` #724

Closed jnsereko closed 1 month ago

jnsereko commented 1 month ago

Description

@MatthewHerbst I am not able to print content using react-to-print's 3.0.0-beta-1.
This is because i have no control of the OptionalContent that i literally don't supply from my code.
As a result, the contentRef element passed to useReactToPrint is not picked up hence erroring

Error

Screenshot 2024-08-15 at 18 47 23
level:"warning",messages:['"react-to-print" received a `contentRef` option and a optional-content param passed to its callback. The `contentRef` option will be ignored.']}),o()):e?e.current:void t({messages:['"react-to-print" did not receive a `contentRef` option or a optional-content param pass to its callback.'],suppressErrors:n})}({contentRef:r,optionalContent:e,suppressErrors:d});if(!u)return void t({messages:["There is nothing to print"]

My code

const PrintComponent: React.FC<PrintComponentProps> = ({ closeModal, patient }) => {
  const contentToPrintRef = useRef<HTMLDivElement>(null);
  const onBeforeGetContentResolve = useRef<() => void | null>(null);
  const [isPrinting, setIsPrinting] = useState(false);
  const headerTitle = "Some Title";

  useEffect(() => {
    if (isPrinting && onBeforeGetContentResolve.current) {
      onBeforeGetContentResolve.current();
    }
  }, [isPrinting]);

  const handleBeforeGetContent = useCallback(
    () => {
      return new Promise<void>((resolve) => {
        if (patient && headerTitle) {
          onBeforeGetContentResolve.current = resolve;
          setIsPrinting(true);
        }
      });
    }, [headerTitle, patient],
  );

  const handleAfterPrint = useCallback(() => {
    onBeforeGetContentResolve.current = null;
    setIsPrinting(false);
    closeModal();
  }, [closeModal]);

  const handlePrintError = useCallback((errorLocation, error) => {
    onBeforeGetContentResolve.current = null;
    console.log('An error occurred in "{{errorLocation}}":  { errorLocation } + error');
    setIsPrinting(false);
  }, []);

  const handlePrint = useReactToPrint({
    contentRef: contentToPrintRef,
    documentTitle: `${headerTitle}`,
    onAfterPrint: handleAfterPrint,
    onBeforePrint: handleBeforeGetContent,
    onPrintError: handlePrintError,
  });

  return (
    <>
      <ModalHeader
        closeModal={closeModal}
        title=Print Stuff
      />
      <ModalBody>

      </ModalBody>
      <ModalFooter>
        <Button className={styles.some_styles} disabled={isPrinting} onClick={handlePrint} kind="primary">
          {isPrinting ? (
            <InlineLoading className={styles.some_styles} description={'Printing ' + '...'} />
          ) : (
            Print
          )}
        </Button>
      </ModalFooter>
      <div className={`${styles.some_styles}`}>
          <MyComponent
            ref={contentToPrintRef}
          />
      </div>
    </>
  );
};

cc @ibacher @denniskigen @pirupius

ibacher commented 1 month ago

@jnsereko I fixed this in your PR. The issue is that handlePrint is passed directly to onClick. onClick calls the supplied function with the event as the first argument, which handlePrint here interprets as "additionalContent" because that's what it's first argument is. The solution is easy: onClick={() => handlePrint()}

jnsereko commented 1 month ago

The solution is easy: onClick={() => handlePrint()}

I feel silly right now.
Are the react-to-print examples also showing this? Seams they do

@MatthewHerbst feel free to reopen this in case of anything else

MatthewHerbst commented 3 weeks ago

Hey friends, I was traveling the last few days so just seeing this now. Previously the function returned by useReactToPrint took the event as it's first parameter and additional content was passed as the second parameter. We never actually did anything with the event though so I removed it from v3. I wonder if it would make life easier for folks if we keep it around so this "workaround" isn't necessary

ibacher commented 3 weeks ago

I wonder if it would make life easier for folks if we keep it around so this "workaround" isn't necessary

It is a nice convenience, I think!

MatthewHerbst commented 3 weeks ago

The only downside is that the event is currently typed as unknown, meaning if someone were to put the optionalContent into the first prop instead of the event there wouldn't be a type error. Let me take a look at this later tonight and see if I can come up with a nice solution

ibacher commented 3 weeks ago

Could it be as easy as something like:

handlePrint(event?: unknown, content?: UseReactToPrintHookContent) {
  const contentHook === content ?? typeof event === "function" ?  event : undefined;
  // do stuff
};

I think this still works for the onClick handler, but also if the user submits a function as the first argument. It's not as type-safe as it might be.

MatthewHerbst commented 3 weeks ago

Yeah, an overload like that is what I'm imagining as well, make it super hard to screw up. I was imagining doing something along the lines of instanceof Event though not totally sure if that always works in React land with synthetic events. The is function approach you have above might be best!