gilbarbara / react-joyride

Create guided tours in your apps
https://react-joyride.com/
MIT License
6.62k stars 519 forks source link

[Tooltip] Displayed in wrong position in next.js #957

Closed mtr1990 closed 8 months ago

mtr1990 commented 8 months ago

🐛 Bug Report

Displayed in wrong position in next.js

On my localhost the vite.js version does not have this error

To Reproduce

  const [run, setRun] = useState(true);

  const [callbackData, setCallbackData] = useState();

  const handleJoyrideCallback = (data: CallBackProps) => {
    const { status } = data;
    const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED];

    if (finishedStatuses.includes(status)) {
      setRun(false);
    }

    setCallbackData(data); // => When you delete or comment this line the code will work as expected. 
                                           // However, I want to get data so I need this function
  };

1.Result after deleting setCallbackData(data):

image

2.Result with setCallbackData(data):

image

Expected behavior

Displayed in the correct position with setCallbackData(data)

Link to repl or repo (highly encouraged)

https://codesandbox.io/p/sandbox/frosty-wozniak-w7sltw?file=%2Fapp%2Fpage.tsx%3A53%2C27

gilbarbara commented 8 months ago

Hey @mtr1990

Setting the callback data in a state is an anti-pattern since the callback receives all the lifecycle events before the beacon or tooltip is rendered, which probably creates a race condition and breaks the functionality. The callback changes significantly, and saving its data in a state will force the component to re-render like crazy.

Adding a "next tick" setTimeout to the state setter inside the callback handler proves that it is, in fact, a race condition.

setTimeout(() => {
    setCallbackData(data);
}, 0);
// This "works", but it's a huge red flag.

If you need to access the data outside the callback, I'd recommend saving it to a mutable ref instead.

function App() {
    const joyrideState = useRef<CallBackProps | undefined>();

    const handleJoyrideCallback = (data: CallBackProps) => {
        joyrideState.current = data;
    }
    ...
    console.log('joyrideState', joyrideState.current);
}
mtr1990 commented 8 months ago

Hi @gilbarbara ,

Thank you for your response.I tried your way but the status always returns undefined for joyrideState.current and helpers.current

https://codesandbox.io/p/sandbox/frosty-wozniak-w7sltw?file=%2Fapp%2Fpage.tsx%3A62%2C15

gilbarbara commented 8 months ago

Hey @mtr1990

The initial logs will be undefined because the refs weren't set yet. And changing a ref doesn't re-render the component. The Joyride component won't re-render the parent when it changes its internal state.

The example is too simple. Try adding this to see the logs:

useEffect(() => {
  setTimeout(() => {
    console.log("joyrideState", joyrideState.current);

    console.log("helpers", helpers.current);
  }, 1000);
}, []);

What are you trying to achieve? Have you tried it in your real-life project? If you try to use the helpers or the joyrideState in other methods, they will be available in the ref...

Anyway, this is not a problem with the library itself, just the implementation. So, good luck!

mtr1990 commented 8 months ago

Hi @gilbarbara

I am using in my real project. My desired goal is to access properties outside the callback.

But so far I have not achieved this despite trying many ways

Example: https://docs.react-joyride.com/callback

{
  action: 'start',
  controlled: true,
  index: 0,
  lifecycle: 'init',
  size: 4,
  status: 'running',
  step: { the.current.step },
  type: 'tour:start',
}

When I access inside the callback everything works fine in vite.js. But on next.js it doesn't work as expected

  const [run, setRun] = useState(true);

  const [callbackData, setCallbackData] = useState();

  const handleJoyrideCallback = (data: CallBackProps) => {
    const { status } = data;
    const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED];

    if (finishedStatuses.includes(status)) {
      setRun(false);
    }

    setCallbackData(data); // => vite.js version works fine but next.js doesn't

  };

I also updated the example using outside callback with refs but returns undefined.

Can you help based on codesanbox? https://codesandbox.io/p/sandbox/frosty-wozniak-w7sltw?file=%2Fapp%2Fpage.tsx%3A50%2C23

gilbarbara commented 8 months ago

You can't set the callback data in a state. That is an anti-pattern that forces the parent component to re-render dozens of times without needing and creates a race condition that breaks the internal state of the Joyride component... The vite version isn't working fine, it's just hiding the problem, and next.js isn't so forgiving.

Can't you call a method inside the callback when the state matches your condition? Why do you need the joyride state outside the callback? Did you check the other examples in the demo? The Controlled one has quite complex logic inside the callback.