React open pop up from react-table

I am building an app with react.js, it has a table built using the react table library.

Within this table I would like to have a button (or whatever) that will open a pop up showing a png. The png is determined by the cell content so may or may not be the same in all pop ups.

It appears I need to use a very simple modal so I decided to build it using the react bootstrap library as follows

import React, { useState } from "react";
import Modal from 'react-bootstrap/Modal';
import Button from 'react-bootstrap/Button';

const PopUp = ({ link }) => {
  const [show, setShow] = useState(false);

  const handleClose = () => setShow(false);
  const handleShow = () => setShow(true);

  return (
    <>
      <Button variant="primary" onClick={handleShow} size="sm">
        Pathway
      </Button>

      <Modal
        show={show}
        onHide={handleClose}
        dialogClassName={'IMG_path'}
        size="lg"
      >
        <Modal.Header closeButton>
          <Modal.Title>IMG title</Modal.Title>
        </Modal.Header>
        <Modal.Body closeButton>
          <img src={"./img/" + link}
               id="id"
               alt="description"
               className="img-fluid"/>
        </Modal.Body>
      </Modal>
    </>
  );
};

export default PopUp;

Then I tested it next to the react table, I mean within the same div, and it worked perfectly fine.

Then I added to the table using the following code

import React from "react";
import { useTable, usePagination, useSortBy } from 'react-table';

import PopUp from "./PopUp";

const MakePopup = ({ values }) =>  values && <PopUp link={values} /> ;

function Table({ columns, data }) {
  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    prepareRow,
    page, 
    canPreviousPage,
    canNextPage,
    pageOptions,
    pageCount,
    gotoPage,
    nextPage,
    previousPage,
    setPageSize,
    state: { pageIndex, pageSize },
  } = useTable(
    {
      columns,
      data,
      autoResetPage: true,
      initialState: { pageIndex: 0 },
    },
    // hook for sorting
    useSortBy,
    // hook for pagination
    usePagination
  )

  return (
    <>
      <table {...getTableProps()} className={"table table-striped table-bordered table-hover"}>
        <thead>
          {headerGroups.map(headerGroup => (
            <tr {...headerGroup.getHeaderGroupProps()}>
              {headerGroup.headers.map(column => (
                // Add the sorting props to control sorting. For this example
                // we can add them into the header props
                <th {...column.getHeaderProps(column.getSortByToggleProps())} scope={"col"}>
                  {column.render('Header')}
                  <span>
                    {column.isSorted
                      ? column.isSortedDesc
                        ? ' 🔽'
                        : ' 🔼'
                      : ''}
                  </span>
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody {...getTableBodyProps()}>
          {page.map((row, i) => {
            prepareRow(row)
            return (
              <tr {...row.getRowProps()}>
                {row.cells.map(cell => {
                  return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
                })}
              </tr>
            )
          })}
        </tbody>
      </table>
      {/*
        Pagination can be built however you'd like.
        This is just a very basic UI implementation:
      */}
      <div className="pagination">
        <select
          value={pageSize}
          onChange={e => {
            setPageSize(Number(e.target.value))
          }}
        >
          {[10, 20, 30, 40, 50].map(pageSize => (
            <option key={pageSize} value={pageSize}>
              Show {pageSize}
            </option>
          ))}
        </select>{' '}
        <button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
          {'<<'}
        </button>{' '}
        <button onClick={() => previousPage()} disabled={!canPreviousPage}>
          {'<'}
        </button>{' '}
        <span className='page-of'>
          Page{' '}
          <strong>
            {pageIndex + 1} of {pageOptions.length}
          </strong>{' '}
        </span>
        <button onClick={() => nextPage()} disabled={!canNextPage}>
          {'>'}
        </button>{' '}
        <button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
          {'>>'}
        </button>
      </div>
    </>
  )
}

function MyTable(props) {
  const columns = [
    {
      Header: 'AA',
      accessor: 'AA',
    },
    {
      Header: 'BB',
      accessor: 'BB',
      sortType: "basic"
    },
    ...
    ...
    ...
    {
      Header: 'Img',
      accessor: 'url',
      sortType: "basic",
      disableSortBy: true,
      Cell: ({ cell: {value} }) => <MakePopup values={value} />
    }
  ];

  const data = props.tableData;

  return (
      <Table columns={columns} data={data} />
  )
}

Now the cells have the button to call the modal but it never appears 🙁

Searching here I found this and this other answer indicating that the modal should be outside the table instead of next the button within each cell so I modified the code to this

import React, { useState } from "react";
import { useTable, usePagination, useSortBy } from 'react-table';
import Modal from 'react-bootstrap/Modal';
import Button from 'react-bootstrap/Button';

// PopUp 
const [show, setShow] = useState(false);
const handleClose = () => setShow(false);
const [link, setLink] = useState('.png');

const handleClick = ( link ) => {
  setShow(true);
  setLink(link);
};

function Table({ columns, data }) {

  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    prepareRow,
    page, 
    canPreviousPage,
    canNextPage,
    pageOptions,
    pageCount,
    gotoPage,
    nextPage,
    previousPage,
    setPageSize,
    state: { pageIndex, pageSize },
  } = useTable(
    {
      columns,
      data,
      autoResetPage: true,
      initialState: { pageIndex: 0 },
    },
    // hook for sorting
    useSortBy,
    // hook for pagination
    usePagination
  )

  return (
    <>
      <table {...getTableProps()} className={"table table-striped table-bordered table-hover"}>
        <thead>
          {headerGroups.map(headerGroup => (
            <tr {...headerGroup.getHeaderGroupProps()}>
              {headerGroup.headers.map(column => (
                // Add the sorting props to control sorting. For this example
                // we can add them into the header props
                <th {...column.getHeaderProps(column.getSortByToggleProps())} scope={"col"}>
                  {column.render('Header')}
                  {/* Add a sort direction indicator */}
                  <span>
                    {column.isSorted
                      ? column.isSortedDesc
                        ? ' 🔽'
                        : ' 🔼'
                      : ''}
                  </span>
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody {...getTableBodyProps()}>
          {page.map((row, i) => {
            prepareRow(row)
            return (
              <tr {...row.getRowProps()}>
                {row.cells.map(cell => {
                  return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
                })}
              </tr>
            )
          })}
        </tbody>
      </table>
      {/*
        Pagination can be built however you'd like.
        This is just a very basic UI implementation:
      */}
      <div className="pagination">
        <select
          value={pageSize}
          onChange={e => {
            setPageSize(Number(e.target.value))
          }}
        >
          {[10, 20, 30, 40, 50].map(pageSize => (
            <option key={pageSize} value={pageSize}>
              Show {pageSize}
            </option>
          ))}
        </select>{' '}
        <button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
          {'<<'}
        </button>{' '}
        <button onClick={() => previousPage()} disabled={!canPreviousPage}>
          {'<'}
        </button>{' '}
        <span className='page-of'>
          Page{' '}
          <strong>
            {pageIndex + 1} of {pageOptions.length}
          </strong>{' '}
        </span>
        <button onClick={() => nextPage()} disabled={!canNextPage}>
          {'>'}
        </button>{' '}
        <button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
          {'>>'}
        </button>
      </div>
    </>
  )
}

function MyTable(props) {
  const columns = [
    {
      Header: 'AA',
      accessor: 'AA',
    },
    {
      Header: 'BB',
      accessor: 'BB',
      sortType: "basic"
    },
    ...
    ...
    ...
    {
      Header: 'Img',
      accessor: 'url',
      sortType: "basic",
      disableSortBy: true,
      Cell: ({ cell: { png } }) => png && (
        <Button
          variant="primary"
          onClick={ () => handleClick(png) }
          size="sm"
        >
          Pathway
        </Button>
      )
    }
  ];

  const data = props.genesData;

  return (
    <>
      <Table columns={columns} data={data} />
      <Modal
        show={show}
        onHide={handleClose}
        dialogClassName={'IMG_path'}
        size="lg"
      >
        <Modal.Header closeButton>
          <Modal.Title>IMG title</Modal.Title>
        </Modal.Header>
        <Modal.Body closeButton>
          <img src={"./img/" + link}
               id="id"
               alt="description"
               className="img-fluid"/>
        </Modal.Body>
      </Modal>
    </>
  )
}

export default MyTable;

Now I get the following compilation error and the app does not start

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:

  1. You might have mismatching versions of React and the renderer (such as React DOM)
  2. You might be breaking the Rules of Hooks
  3. You might have more than one copy of React in the same app See react-invalid-hook-call for tips about how to debug and fix this problem.

which points to this part of the code

   4 | import Button from 'react-bootstrap/Button';
   5 | 
   6 | // PopUp states
>  7 | const [show, setShow] = useState(false);
   8 | const handleClose = () => setShow(false);
   9 | // const handleShow = () => setShow(true);
  10 | const [link, setLink] = useState('.png');

Now I am unsure of how to address this. I have been considering using a state management library such as redux but I think it will not solve the problem. I am using modals from bootstrap but I would use what-ever-it-is since I think the modals may be a little bit to much for the functionality I need.

Thanks

=========================

Edit based on answer

As recommended by @Afzal Zubair I moved the hooks inside the function as follows :

import React, { useState } from "react";
import { useTable, usePagination, useSortBy } from 'react-table';
import Modal from 'react-bootstrap/Modal';
import Button from 'react-bootstrap/Button';

function Table({ columns, data }) {

  // PopUp states
  const [show, setShow] = useState(false);
  const handleClose = () => setShow(false);
  // const handleShow = () => setShow(true);
  const [link, setLink] = useState('.png');

  const handleClick = ( link ) => {
    setShow(true);
    setLink(link);
  };

  const {
    getTableProps,
    getTableBodyProps,
  ...
  ...
  ...
  ...
  return (
    <>
      <Table columns={columns} data={data} />
      <Modal
        show={show}
        onHide={handleClose}
        dialogClassName={'IMG_path'}
        size="lg"
      >
        <Modal.Header closeButton>
          <Modal.Title>KEGG pathway</Modal.Title>
        </Modal.Header>
        <Modal.Body closeButton>
          <img src={"./img/" + link}
               alt="plant-pathogen_interaction"
               className="img-fluid"/>
        </Modal.Body>
      </Modal>
    </>
  )
}

Now I get a different compilation error

Failed to compile.

./src/components/table/ReactTable_PopUp.jsx Line 193:27: ‘handleClick’ is not defined no-undef Line 208:15: ‘show’ is not defined no-undef Line 209:17: ‘handleClose’ is not defined no-undef Line 217:32: ‘link’ is not defined no-undef

Search for the keywords to learn more about each error.

referring to the following code

 190     Cell: ({ cell: { png } }) => png && (
 191       <Button
 192         variant="primary"
 193         onClick={ () => handleClick(png) }
 194         size="sm"
 195       >
 196         Pathway
 197       </Button>
 198     )

and

 204  return (
 205    <>
 206      <Table columns={columns} data={data} />
 207      <Modal
 208        show={show}
 209        onHide={handleClose}

and

 216        <Modal.Body closeButton>
 217          <img src={"./img/" + link}
 218               alt="plant-pathogen_interaction"
 219               className="img-fluid"/>
 220        </Modal.Body>

Thanks

====================================================================

SOLVED

@Afzal Zubair is right, the hooks needed to be moved to the Functional element, function MyTable in my code

function MyTable(props) {

  // PopUp states
  const [show, setShow] = useState(false);
  const handleClose = () => setShow(false);
  const [link, setLink] = useState('.png');

  const handleClick = ( link ) => {
    setShow(true);
    setLink(link);
  };  

  const columns = [
    {
      Header: 'AA',
      accessor: 'AA',
    },
    {
      Header: 'BB',
      accessor: 'BB',
      sortType: "basic"
    },
    ...
    ...

Now it works!

Answer

It seems like you are using useState hooks outside react component.

   4 | import Button from 'react-bootstrap/Button';
   5 | 
   6 | // PopUp states
>  7 | const [show, setShow] = useState(false);
   8 | const handleClose = () => setShow(false);
   9 | // const handleShow = () => setShow(true);
  10 | const [link, setLink] = useState('.png');

You should use the modal outside the Table components, but you cannot use react hooks outside the react components.

I suggest using these hooks inside the react component, Functional component to be precise. You can refer to this document for better understanding, link.

Leave a Reply

Your email address will not be published. Required fields are marked *