Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Problem with React Joyride on production Cannot read properties of undefined (reading '0')

We decided to implement a simple onboarding tour on our application, everything was going well on local but once we deploy it to production its crashing showing the next error on devtools:

 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading '0')

it's kind of hard to debug this and see what is the problem because it's caused on production and its like debugging with the eyes closed.

the bug/error that the console show it's and undefined and by logic will crash too on local, but this it's not the case

The error is caused after we pass the first step like you can see on this video(also you can se how it's working locally: VIDEO WITH THE ISSUE

They way the joyride its implemented consists on two big keys the first one its the joyride wrapper:

import { useEffect, useState } from 'react';
import Joyride, { CallBackProps, STATUS, Step } from 'react-joyride';
import { Tooltip } from './components/tool-tip';
import { OnboardingTourJoyrideProps, ValidSteps } from '../../types/onboarding-tour';
import { useGuidedTour } from '../../hooks/use-guided-tour';

export function OnboardingTourJoyride({ start, stepToCheck }: OnboardingTourJoyrideProps): JSX.Element {
  const { dashBoardSteps, terminalAppSteps, cloudAppSteps } = useGuidedTour();

  const [steps, setSteps] = useState<Step[]>(dashBoardSteps);
  const [run, setRun] = useState<boolean>(false);
  const [resetTour, setResetTour] = useState<boolean>(false);
  const [stepToInitRestart, setStepToInitRestart] = useState<number>(0);

  function handleSteps() {
    switch (stepToCheck) {
      case ValidSteps.DASHBOARD_STEPS:
        setSteps(dashBoardSteps);
        setStepToInitRestart(dashBoardSteps.length - 1);
        break;
      case ValidSteps.TERMINAL_APP_STEPS:
        setSteps(terminalAppSteps);
        setStepToInitRestart(terminalAppSteps.length - 1);
        break;
      case ValidSteps.CLOUD_APP_STEPS:
        setSteps(cloudAppSteps);
        setStepToInitRestart(cloudAppSteps.length - 1);
    }
  }

  useEffect(() => {
    const onboardinIsCompletedOrSkipped = handleCheckIfTourIsComplete();
    if (!onboardinIsCompletedOrSkipped) {
      handleSteps();
      if (start) setRun(true);
    }
  }, [start]);

  useEffect(() => {
    if (resetTour) {
      setRun(true);
      setResetTour(false);
    }
  }, [resetTour]);

  function handlePropertyToSaveOnLocalStorage() {
    switch (stepToCheck) {
      case ValidSteps.DASHBOARD_STEPS:
        localStorage.setItem('ONBOARDING_TOUR_DASHBOARD', 'true');
        break;
      case ValidSteps.TERMINAL_APP_STEPS:
        localStorage.setItem('ONBOARDING_TOUR_TERMINAL_APP', 'true');
        break;
      case ValidSteps.CLOUD_APP_STEPS:
        localStorage.setItem('ONBOARDING_TOUR_CLOUD_APP', 'true');
        break;
    }
  }

  function handleCheckIfTourIsComplete() {
    if (stepToCheck === ValidSteps.DASHBOARD_STEPS) {
      return localStorage.getItem('ONBOARDING_TOUR_DASHBOARD');
    } else if (stepToCheck === ValidSteps.TERMINAL_APP_STEPS) {
      return localStorage.getItem('ONBOARDING_TOUR_TERMINAL_APP');
    } else if (stepToCheck === ValidSteps.CLOUD_APP_STEPS) {
      return localStorage.getItem('ONBOARDING_TOUR_CLOUD_APP');
    }
  }

  function handleJoyrideCallback(data: CallBackProps) {
    const { status, action, index } = data;
    console.log(data);
    const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED];

    if (index === stepToInitRestart && status === STATUS.SKIPPED) {
      setRun(false);
      setResetTour(true);
    }

    if (finishedStatuses.includes(status)) {
      if (action === 'skip') {
        setRun(false);
        handlePropertyToSaveOnLocalStorage();
      }
      setRun(false);
      handlePropertyToSaveOnLocalStorage();
    }
  }

  return (
    <Joyride
      tooltipComponent={Tooltip}
      disableScrolling={true}
      callback={handleJoyrideCallback}
      continuous
      hideCloseButton
      run={run}
      scrollToFirstStep
      showProgress={false}
      showSkipButton={true}
      steps={steps}
      styles={{
        options: {
          zIndex: 10000,
        },
      }}
    />
  );
}

which its a simple wrapper that contains all the logic to rehuse the same component across the app

and the other one it's the custom tooltip component:

import { Box, CloseButton, Flex, Text } from '@chakra-ui/react';
import { TooltipRenderProps } from 'react-joyride';
import { Button } from '../../button';
import { useIntl } from 'react-intl';

export function Tooltip({
  backProps,
  continuous,
  index,
  isLastStep,
  primaryProps,
  skipProps,
  step,
  tooltipProps,
  size,
}: TooltipRenderProps): JSX.Element {
  const intl = useIntl();

  const steps = {
    currentStep: index,
    stepsLength: size - 2,
  };

  function handleResetOrBackStep(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
    if (isLastStep) {
      skipProps.onClick(e);
    } else {
      backProps.onClick(e);
    }
  }

  function handleCloseButton(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
    if (isLastStep) {
      primaryProps.onClick(e);
    } else {
      skipProps.onClick(e);
    }
  }

  function handleFowardButton() {
    return continuous
      ? index !== 0 && !isLastStep
        ? intl.formatMessage({ id: 'buttons.next' })
        : isLastStep
        ? intl.formatMessage({ id: 'buttons.finish' })
        : intl.formatMessage({ id: 'buttons.startTour' })
      : null;
  }

  function handleShowStepsCounter() {
    return index !== 0 && !isLastStep ? (
      <Text fontSize="16px">
        {intl.formatMessage(
          { id: 'onboardingTour.toolTip.stepsCounter' },
          {
            currentStep: steps.currentStep,
            totalSteps: steps.stepsLength,
          },
        )}
      </Text>
    ) : (
      !isLastStep && (
        <Button variant="outline" {...skipProps}>
          {intl.formatMessage({ id: 'buttons.skipTour' })}
        </Button>
      )
    );
  }

  function handleBackwardButton() {
    return isLastStep
      ? intl.formatMessage({ id: 'buttons.restartTour' })
      : intl.formatMessage({ id: 'buttons.previous' });
  }

  return (
    <Box {...tooltipProps} background="white" p="48px" w="100%" borderRadius="6px">
      <Box padding="md">
        <Flex justifyContent="space-between" align="baseline" marginBottom="15px">
          {step.title && (
            <Text fontSize="20px" fontWeight="700">
              {step.title}
            </Text>
          )}
          <CloseButton onClick={e => handleCloseButton(e)} />
        </Flex>
        {step.content && (
          <Box>
            <Text w="504px" fontSize="16px" fontWeight="400" lineHeight="26px" marginBottom="48px">
              {step.content}
            </Text>
          </Box>
        )}
      </Box>
      <Box>
        <Flex justifyContent="space-between" align="center">
          {handleShowStepsCounter()}
          <Flex w={isLastStep ? '100%' : ''} justifyContent="space-between">
            {index !== 0 && (
              <Button marginRight="12px" variant="outline" onClick={e => handleResetOrBackStep(e)}>
                {handleBackwardButton()}
              </Button>
            )}
            <Button h="35px" {...primaryProps}>
              {handleFowardButton()}
            </Button>
          </Flex>
        </Flex>
      </Box>
    </Box>
  );
}

I don't know if anyone have experience with a similar issue, I google it and search all across SO and the issues on the library repository. Also it's weird because on the pass I worked with react-joyride and deployed to production and there is no any problem. To put you all in context, we are using next with a framework called Gasket.

like image 995
Alan Miranda Avatar asked Oct 28 '25 05:10

Alan Miranda


1 Answers

Had the same issue.

The problem is with the experimental SWC minification using the swcMinify config. It tries to optimize too much, leading to the offset variable not being used before it is declared. It's not a problem with the version of popper.js or any other dependency. To fix the problem you just need to set swcMinify: false in your next config (also updating nextjs to 13.1.1 fixes the problem in my case).

Here is discussion about: https://github.com/gilbarbara/react-joyride/issues/857

like image 169
Yana Avatar answered Oct 29 '25 20:10

Yana