Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React: Function components cannot be given refs when displaying a modal

The modal is rendered under the hashRouter in App.js. The modal is shown when its state isOpen is true.

I'm given this error when trying to display a Modal with react redux.

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Check the render method of `TrapFocus`.
    in ConnectFunction (at Modal.js:41)
    in TrapFocus (created by ForwardRef(Modal))
    in div (created by ForwardRef(Modal))
    in ForwardRef(Portal) (created by ForwardRef(Modal))
    in ForwardRef(Modal) (at Modal.js:35)
    in Modal (created by ConnectFunction)
    in ConnectFunction (created by WithStyles(undefined))
    in WithStyles(undefined) (at App.js:46)
    in ThemeProvider (at App.js:24)
    in App (created by WithStyles(App))
    in WithStyles(App) (at src/index.js:13)
    in Provider (at src/index.js:12)

This is my code:

Modal.js

import React, { Component } from "react";
import { compose } from "redux";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { Modal as MaterialModal } from "@material-ui/core";
import { withStyles } from "@material-ui/styles";
import { closeModal } from "../store/actions/actions-ui";
import BasicModal from "./Modals/BasicModal";

const ModalTypes = {
    BasicModal,
};

const styles = {
    modal: {
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
    },
};

class Modal extends Component {
    handleCloseModal = () => {
        const { dispatch, shouldCloseOnBackgroundTouch } = this.props;
        if (shouldCloseOnBackgroundTouch) dispatch(closeModal());
    };

    render() {
        const { modalType, data, isOpen, classes } = this.props;
        if (!modalType) {
            return null;
        }
        const ModalToRender = ModalTypes[modalType];
        return (
            <MaterialModal
                disableAutoFocus
                className={classes.modal}
                open={isOpen}
                onClose={this.handleCloseModal}
            >
                <ModalToRender {...data} />
            </MaterialModal>
        );
    }
}

Modal.propTypes = {
    dispatch: PropTypes.func.isRequired,
    isOpen: PropTypes.bool.isRequired,
    modalType: PropTypes.string,
    // eslint-disable-next-line react/forbid-prop-types
    data: PropTypes.object,
    shouldCloseOnBackgroundTouch: PropTypes.bool.isRequired,
    classes: PropTypes.objectOf(PropTypes.string).isRequired,
};

Modal.defaultProps = {
    data: {},
    modalType: "",
};

const mapStateToProps = (state) => ({
    isOpen: state.ui.modalState.isOpen,
    shouldCloseOnBackgroundTouch: state.ui.modalState.shouldCloseOnBackgroundTouch,
    modalType: state.ui.modalState.modalType,
    data: state.ui.modalState.data,
});

export default compose(
    withStyles(styles),
    connect(mapStateToProps)
)(Modal);

BasicModal.js

import React, { Component } from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import Button from "@material-ui/core/Button";
import Typography from "@material-ui/core/Typography";
import Paper from "@material-ui/core/Paper";
import If from "../__helpers__/If";
import { closeModal } from "../../store/actions/actions-ui";

class BasicModal extends Component {
    handleClose = () => {
        const { dispatch } = this.props;
        dispatch(closeModal());
    };

    render() {
        const { title, text, extraBtnText, extraBtnAction } = this.props;
        return (
            <Paper>
                <If truthy={title}>
                    <Typography gutterBottom variant="h4">
                        {title}
                    </Typography>
                </If>
                <If truthy={text}>
                    <Typography gutterBottom vairant="body2">
                        {text}
                    </Typography>
                </If>

                <Button onClick={this.handleClose}>Close</Button>
                <If truthy={extraBtnAction && extraBtnText}>
                    <Button
                        onClick={() => {
                            extraBtnAction();
                            this.handleClose();
                        }}
                    >
                        {extraBtnText}
                    </Button>
                </If>
            </Paper>
        );
    }
}

BasicModal.propTypes = {
    dispatch: PropTypes.func.isRequired,
    title: PropTypes.string.isRequired,
    text: PropTypes.string.isRequired,
    extraBtnText: PropTypes.string,
    extraBtnAction: PropTypes.func,
};

export default connect()(BasicModal);

I'm guessing that the issue lies with me trying to render a component like this:

<ModalToRender {...data} />

But I can't seem to understand what is wrong with it.

Any help is appreciated!

like image 237
Paikz Avatar asked Jul 18 '19 15:07

Paikz


1 Answers

You need to use the forwardRef option in connect() for the component (e.g. BasicModal) that is wrapped by Material-UI's Modal.

Example:

export default connect(null, null, null, {forwardRef: true})(BasicModal);

Refs provide access to the DOM node for a React element. Material-UI's TrapFocus (which is used by Modal) uses a ref on the child passed to Modal to manage focus aspects.

In your case, the child passed to Modal is the wrapper component returned by connect()(BasicModal). That wrapper component is, by default, a function component and function components cannot accept refs except by being wrapped by forwardRef.

The forwardRef option of connect causes it to wrap the function component using React.forwardRef so that it can successfully accept a ref which it passes along to the wrapped component (BasicModal in your case).

like image 140
Ryan Cogswell Avatar answered Oct 22 '22 07:10

Ryan Cogswell