Ryan
Ryan

Reputation: 1050

React higher order component not working in typescript

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!

Upvotes: 3

Views: 1555

Answers (1)

buzatto
buzatto

Reputation: 10382

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

Upvotes: 2

Related Questions