3

Problem

I am trying to uplift an HOC from javascript to typescript. The HOC adds a confirmation dialog into the component that uses it, providing a prop showConfirmationDialog which when called, displays the dialog and runs the callback when hitting confirm.

The code compiles fine, but when I open the app in the browser, I get an error "Invalid hook call. Hooks can only be called inside of the body of a function component."

The code worked fine in javascript. I cannot understand the error, and I have followed all recommended steps but nothing is fixing it.

Code

ConfirmationDialog/index.tsx

type ExtraProps = {
    showConfirmationDialog: (params: RequiredParameters) => void
}

type ConfirmationCallback = () => void

interface RequiredParameters {
    dialogTitle: string,
    dialogContent: string,
    confirmationButtonText: string,
    onConfirm: ConfirmationCallback
}

const WithConfirmationDialog = <P extends ExtraProps>(Component: React.ComponentType<P>) => {

    const [open, setOpen] = useState(false)
    const [title, setTitle] = useState('')
    const [content, setContent] = useState('')
    const [confirmationButtonText, setConfirmationButtonText] = useState('')
    const [onConfirm, setOnConfirm] = useState<ConfirmationCallback>()

    const handleShow = (params: RequiredParameters) => {
        setTitle(params.dialogTitle)
        setContent(params.dialogContent)
        setConfirmationButtonText(params.confirmationButtonText)
        setOnConfirm(params.onConfirm)
        setOpen(true)
    }

    const handleConfirm = () => {
        if (onConfirm) {
            onConfirm()
        }
        setOpen(false)
    }

    const handleClose = () => {
        setOpen(false)
    }

    const ComponentWithConfirmationDialog = (props: P) => (
        <>
            <Dialog
                open={open}
                onClose={handleClose}
            >
                <DialogTitle>{title}</DialogTitle>
                <DialogContent>
                    <DialogContentText>{content} </DialogContentText>
                </DialogContent>
                <DialogActions>
                    <Button onClick={handleConfirm} color="primary">
                        {confirmationButtonText}
                    </Button>
                    <Button onClick={handleClose} color="primary">
                        Cancel
                    </Button>
                </DialogActions>
            </Dialog>
            <Component {...props} showConfirmationDialog={handleShow} />
        </>
    )

    return ComponentWithConfirmationDialog
}

export default WithConfirmationDialog

Sample of where the code is used after clicking a button in another component:

import withConfirmationDialog from '../ConfirmationDialog'

const MyComponent = (props) => {
  const classes = useStyles();

  const handleDeleteBooking = () => {
    // ...make api calls and handle results...
  };

  // left out everything else for brevity

  return (
    <Fab // material-ui
      className={classes.deleteButton}
      aria-label="delete"
      onClick={(e) => {
        props.showConfirmationDialog({
          dialogTitle: "Delete Booking",
          dialogContent: "Are you sure you want to delete this booking?",
          confirmationButtonText: "Delete",
          onConfirm: handleDeleteBooking,
        });
      }}
    >
      <DeleteIcon /> // material-ui
    </Fab>
  );
};

export default withConfirmationDialog(MyComponent)

Additional Info

The main guide I used to build this can be found here. When running npm start it compiles fine, and the error is never displayed in the terminal. I only see in my browser the 'Invalid hook call' message, and a stack trace pointing to my first use of useState(false inside my HOC.

Any help would be greatly appreciated!

0

1 Answer 1

2

the issue here is your HOC is calling hooks outside of function component ComponentWithConfirmationDialog. all hooks must be called inside a component, not outside. your HOC function is not a Component itself.

in order to fix that you need to move all that is above ComponentWithConfirmationDialog to inside it like:

const WithConfirmationDialog = <P extends ExtraProps>(Component: React.ComponentType<P>) => {

  const ComponentWithConfirmationDialog = (props: P) => {
    const [open, setOpen] = useState(false)
    const [title, setTitle] = useState('')
    const [content, setContent] = useState('')
    const [confirmationButtonText, setConfirmationButtonText] = useState('')
    const [onConfirm, setOnConfirm] = useState<ConfirmationCallback>()

    const handleShow = (params: RequiredParameters) => {
        setTitle(params.dialogTitle)
        setContent(params.dialogContent)
        setConfirmationButtonText(params.confirmationButtonText)
        setOnConfirm(params.onConfirm)
        setOpen(true)
    }

    const handleConfirm = () => {
        if (onConfirm) {
            onConfirm()
        }
        setOpen(false)
    }

    const handleClose = () => {
        setOpen(false)
    }

    return (
        <>
            <Dialog
                open={open}
                onClose={handleClose}
            >
                <DialogTitle>{title}</DialogTitle>
                <DialogContent>
                    <DialogContentText>{content} </DialogContentText>
                </DialogContent>
                <DialogActions>
                    <Button onClick={handleConfirm} color="primary">
                        {confirmationButtonText}
                    </Button>
                    <Button onClick={handleClose} color="primary">
                        Cancel
                    </Button>
                </DialogActions>
            </Dialog>
            <Component {...props} showConfirmationDialog={handleShow} />
        </>
    )
 }
    return ComponentWithConfirmationDialog
}

export default WithConfirmationDialog
Sign up to request clarification or add additional context in comments.

3 Comments

This fixed it. Much appreciated @buzatto!
You're the man! I spent at least 4 hours scratching my head over this.
@AbdulMateen glad I could help :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.