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:
- You might have mismatching versions of React and the renderer (such as React DOM)
- You might be breaking the Rules of Hooks
- 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.