0

hoping someone can help lead me in the right direction with this or point to me to any docs/learnings that might be helpful.

I have two React components:
<Widget/>
<Modal/>

When <Widget/> mounted, <Modal/> is hidden (and vice-versa) using conditional rendering and the same boolean in my ShowModal state value (this boolean gets switched by a button). I made a simplified CodeSandbox for this example here

Where I am getting stuck is I have an async function takeScreenShot() which needs to run after <Widget/> is unmounted completely and before <Modal/> is mounted. Neither <Widget/> (just unmounted) nor <Modal/> (about to mount) should be visible in the DOM when takeScreenShot() is called. I need to make sure this is the case because I am taking a screenshot of the underlying page where I do not want to include either of the components in this. In my CodeSandbox example the output of my screenshot function would render a the gray background without showing the blue or red box

What I have tried
I have tried running a cleanup function in the useEffect hook in the <Widget/> component like so.

 useEffect(() => {
    return () => takeScreenShot();
  }, []);

However it doesn't work because the cleanup function, similar to componentWillUnmount() runs right as the component is about to unmount, not fully unmounted from the DOM. This causes my screenshot to capture the un-mounting component in the image. Does anyone have an ideas to point me in the right direction on this?

mcdev
  • 121
  • 2
  • 7
  • In your `useEffect` implementation, did you check to see if the `showModal` value was false before calling? Also, you would need to include `showModal` in the dependencies array if you did. – Jason Jul 25 '20 at 16:53
  • What should trigger the `takeScreenShot` function? – devserkan Jul 25 '20 at 17:02
  • @devserkan the `takeScreenShot` function should trigger after the widget unmounts and before the Modal mounts. – mcdev Jul 25 '20 at 21:09

4 Answers4

2

Move the code from your App into the Modal so that it gets called when the Modal Loads for the first time. I removed all references to useEffect or useLayoutEffect, although the useLayoutEffect may still be required in the modal.

Code Sample Here ...

Jason
  • 711
  • 6
  • 14
1

You could try running it in a useLayoutEffect hook like ...

  useLayoutEffect(() => {
    if (!showModal) takeScreenshot();
  }, [showModal]);

See ... https://reactjs.org/docs/hooks-reference.html#uselayouteffect

According to the Docs ...

The signature is identical to useEffect, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.

You will probably need to keep a separate state of whether or not the modal has been loaded to ensure that the effect hook doesn't execute on initial load of your component.

Code Sample Here ...

Jason
  • 711
  • 6
  • 14
  • really appreciate your answer here and tried a few ways to implement it, might be something I am still doing wrong but just to clarify - In my `` component I used your `useLayoutEffect ` block and a `useRef` to skip the first effect. When the `showModal` boolean flips, the screenshot is taken as expected but not sure react evaluates `{showModal ? : }` in time to hide the Widget before the screenshot is snapped. Is this something I can only do with a setTimeout workaround? I would prefer not to but wanted to get your thoughts. – mcdev Jul 25 '20 at 21:07
  • I created a copy of the sample you provided with the implementation in the answer. Let me know if that's what you tried. Look at the console message to view execution order. – Jason Jul 25 '20 at 22:55
0

You could have another state bool that indicates if a screenshot is being taken, if thats true render neither of the two elements.

so something like this:

() => this.setState({screenshot: true}, () => this.takeScreenshot())

then within your takeScreenshot function, after you're done with its execution it sets it back to false

takeScreenshot = () => {
  // ...
  // ...
  this.setState({screenshot: false})
}

now while rendering

{!screenshot && (showModal ? <Modal /> : <Widget />)}
  • Hi thank you for this. Similar to some of the other answers, I was not able to get this one working. The issue is while the setState callback does trigger the screenshot function after you set the `{screenshot : true}` the async function runs immediately which handles the screenshot and captures the screen before react gets a chance to evaluate this `{!screenshot && (showModal ? : )}` and re-render. Which misses by only a little bit. Any thoughts? – mcdev Jul 25 '20 at 19:20
  • well in that case, you could add a tiny timeout delay - (say maybe 100-200ms - you could play around with the exact duration and see what fits) within your `takeScreenshot` function in addition to my above answer if it doesn't mess with your UX. Although not the most elegant solution it should get the job done – Karan Wadhwa Jul 26 '20 at 17:22
  • just saw @jasons answer - thats definitely a much better solution and you should follow that instead. – Karan Wadhwa Jul 26 '20 at 17:26
0

Here's how you can (and should) do it.

async function takeScreenshot() {
  console.log("Taking screenshot...");

  return new Promise(resolve => {
    window.setTimeout(() => {
      resolve();
      console.log("Screenshot captured!");
    }, 3000);
  });
}

function Widget({ onUnmount }) {
  React.useEffect(() => {
    return () => onUnmount();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return <p>Widget</p>;
}

function Modal() {
  return <p>Modal</p>;
}

function App() {
  const [open, setOpen] = React.useState('widget');
  
  const handleUnmount = async () => {
    await takeScreenshot();
    setOpen('modal');
  }
 
  return (
    <React.Fragment>
      {open === 'widget' && <Widget onUnmount={handleUnmount} />}
      {open === 'modal' && <Modal />}

      <button
        onClick={() => {
          if (open !== "widget") return;
          setOpen("");
        }}
      >
        Show modal
      </button>
    </React.Fragment>
  )
}

You can run the demo here.

Ioannis Potouridis
  • 1,246
  • 6
  • 7
  • @loannis Hi thanks a ton for this reply. I tried implementing this and it still didn't solve the issue unfortunately. The response from the async call is very quick so the screenshot (unlike the longer setTimeout trigger in your example) captures the components before react gets a chance to detect the ```open``` state and hide the component. Essentially the screenshot is faster than react realizing the state update and re-rendering.. does that make sense? – mcdev Jul 25 '20 at 19:11
  • Look, this is a clean way that ensures that `takeScreenshot` will get called after `Widget` unmounts, and before `Modal` mounts. If you want to get a little bit dirty, you can set the state that controls Modal's visibility with a `setTimeout`, but this needs to be in the handler. Async functions and `setTimeout`s usage in `useEffect` is not encouraged. – Ioannis Potouridis Jul 25 '20 at 19:25
  • After all, I'm confident that if you follow by line the example you'll get the desired result. – Ioannis Potouridis Jul 25 '20 at 19:26