React TypeError is not a function with Onboarding implementation

Viewed 24

I want to expand a demo provided by some tutorial about React Design Patterns, subject: Controlled Onboarding Flows, to implement multiple forms on several steps via Onboarding. But unfortunately the tutor did stop at the exciting part when it comes to having two-directional flows. So I'm stuck and don't understand how to select the resp. function (marked with "// HOW TO DECIDE?!" in the 2nd code segment here).

So, every time I hit the prev. button, I receive the "Uncaught TypeError: goToPrevious is not a function" message, because both are defined. Any suggestions on how to handle this?

This is what I got so far. The idea behind this is to get the data from each form within the respo. Step Component and manage it witihin the parent component - which atm happens to be the App.js file.

Any help, tips, additional sources to learn this would be highly appreciated.

This is my template for the resp. controlled form components I want to use:

export const ControlledGenericForm = ({ formData, onChange }) => {
  
  return (
    <form>
      {Object.keys(formData).map((formElementKey) => (
        <input
          key={formElementKey}
          value={formData[formElementKey]}
          type="text"
          id={formElementKey}
          onInput={(event) => onChange(event.target.id, event.target.value)}
        />
      ))}
    </form>
  );
};

That's my controlled Onboarding component, I want to use:

import React from "react";

export const ControlledOnboardingFlow = ({
  children,
  currentIndex,
  onPrevious,
  onNext,
  onFinish,
}) => {
  const goToNext = (stepData) => {
    onNext(stepData);
  };

  const goToPrevious = (stepData) => {
    onPrevious(stepData);
  };

  const goToFinish = (stepData) => {
    onFinish(stepData);
  };

  const currentChild = React.Children.toArray(children)[currentIndex];

  if (currentChild === undefined) goToFinish();

  // HOW TO DECIDE?!

  if (currentChild && onNext)
    return React.cloneElement(currentChild, { goToNext });

  if (currentChild && onPrevious)
    return React.cloneElement(currentChild, { goToPrevious });

  return currentChild;
};

And that's the actual use of this two components within my App:

import { useState } from "react";
import { ControlledOnboardingFlow } from "./ControlledComponents/ControlledOnboardingFlow";
import { ControlledGenericForm } from "./ControlledComponents/ControlledGenericForm";

function App() {
  const [onboardingData, setOnboardingData] = useState({
    name: "Juh",
    age: 22,
    hair: "green",
    street: "Main Street",
    streetNo: 42,
    city: "NYC",
  });

  const [currentIndex, setCurrentIndex] = useState(0);

  const formDataPartOne = (({ name, age, hair }) => ({ name, age, hair }))(
    onboardingData
  );
  const formDataPartTwo = (({ street, streetNo, city }) => ({
    street,
    streetNo,
    city,
  }))(onboardingData);

  const onNext = (stepData) => {
    setOnboardingData({ ...onboardingData, ...stepData });
    setCurrentIndex(currentIndex + 1);
  };

  const onPrevious = (stepData) => {
    setOnboardingData({ ...onboardingData, ...stepData });
    setCurrentIndex(currentIndex - 1);
  };

  const onFinish = () => {
    console.log("Finished");
    console.log(onboardingData);
  };

  const handleFormUpdate = (id, value) => {
    setOnboardingData({ ...onboardingData, [id]: value });
  };

  const StepOne = ({ goToPrevious, goToNext }) => (
    <>
      <h1>Step 1</h1>
      <ControlledGenericForm
        formData={formDataPartOne}
        onChange={handleFormUpdate}
      />
      <button onClick={() => goToPrevious(onboardingData)} >
        Prev
      </button>
      <button onClick={() => goToNext(onboardingData)}>Next</button>
    </>
  );

  const StepTwo = ({ goToPrevious, goToNext }) => (
    <>
      <h1>Step 2</h1>
      <ControlledGenericForm
        formData={formDataPartTwo}
        onChange={handleFormUpdate}
      />
      <button onClick={() => goToPrevious(onboardingData)}>Prev</button>
      <button onClick={() => goToNext(onboardingData)}>Next</button>
    </>
  );

  const StepThree = ({ goToPrevious, goToNext }) => (
    <>
      <h1>Step 3</h1>
      <h3>
        Congrats {onboardingData.name} for being from, {onboardingData.city}
      </h3>
      <button onClick={() => goToNext(onboardingData)}>Next</button>
    </>
  );

  return (
    <ControlledOnboardingFlow
      currentIndex={currentIndex}
      onPrevious={onPrevious}
      onNext={onNext}
      onFinish={onFinish}
    >
      <StepOne />
      <StepTwo />

      {onboardingData.city === "NYC" && <StepThree />}
    </ControlledOnboardingFlow>
  );
}

export default App;
1 Answers
if (currentChild && onNext)
  return React.cloneElement(currentChild, { goToNext });

Since onNext exists, this is the code that will run. It clones the element and gives it a goToNext prop, but it does not give it a goToPrevious prop. So when you press the previous button and run code like onClick={() => goToPrevious(onboardingData)}, the exception is thrown.

It looks like you want to pass both functions into the child, which can be done like:

const currentChild = React.Children.toArray(children)[currentIndex];

if (currentChild === undefined) goToFinish();

if (currentChild) {
  return React.cloneElement(currentChild, { goToNext, goToPrevious });
} 

return currentChild;

If one or both of them happens to be undefined, then the child will get undefined, but that's what you would do anyway with the if/else.

Related