Why arent these functions within the main functional component?

Viewed 58

I don't understand why these formatter functions are placed outside the main component called TemplateTable.

If you need to use the actionformatter for example, you cannot pass a dispatch function do it correct?

I had an issue trying to set the actionformatter onClick for the delete button to a dispatch(deleteTemplate()).

When I run the code while the actionformatter is outside the main component TemplateTable, I get dispatch is undefined. When I defined dispatch within the component I obviously get the cannot use react hooks ouside function component problem.

I can fix this whole issue by just including the actionformatter inside the block of templateTable. I just feel like im shortcutting and was wondering if anyone had any input on this

import React, { createRef, Fragment, useState, useEffect} from 'react';
import {
  Button,
  Card,
  CardBody,
  Col,
  CustomInput,
  DropdownItem,
  DropdownMenu,
  DropdownToggle,
  InputGroup,
  Media,
  Modal,
  ModalBody,
  Row,
  UncontrolledDropdown
} from 'reactstrap';
import { connect, useDispatch } from 'react-redux';
import FalconCardHeader from '../common/FalconCardHeader';
import ButtonIcon from '../common/ButtonIcon';
import paginationFactory, { PaginationProvider } from 'react-bootstrap-table2-paginator';
import BootstrapTable from 'react-bootstrap-table-next';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Link } from 'react-router-dom';
import Flex from '../common/Flex';
import Avatar from '../common/Avatar';
import { getPaginationArray } from '../../helpers/utils';
import CreateTemplate from '../templates/CreateTemplate';
import customers from '../../data/e-commerce/customers';
import { listTemplates, deleteTemplate } from '../../actions/index';


const nameFormatter = (dataField, { template }) => {
  return (
    <Link to="/pages/customer-details">
      <Media tag={Flex} align="center">
        <Media body className="ml-2">
          <h5 className="mb-0 fs--1">{template}</h5>
        </Media>
      </Media>
    </Link>
  );
};

const bodyFormatter = (dataField, { avatar, body }) => {
  return (
    <Link to="/pages/customer-details">
      <Media tag={Flex} align="center">
        
        <Media body className="ml-2">
          <h5 className="mb-0 fs--1">{body}</h5>
        </Media>
      </Media>
    </Link>
  );
};

const emailFormatter = email => <a href={`mailto:${email}`}>{email}</a>;
const phoneFormatter = phone => <a href={`tel:${phone}`}>{phone}</a>;

const actionFormatter = (dataField, { _id }) => ( 
  // Control your row with this id
  
  <UncontrolledDropdown>
    <DropdownToggle color="link" size="sm" className="text-600 btn-reveal mr-3">
      <FontAwesomeIcon icon="ellipsis-h" className="fs--1" />
    </DropdownToggle>
    <DropdownMenu right className="border py-2">
      <DropdownItem onClick={() => console.log('Edit: ', _id)}>Edit</DropdownItem>
      <DropdownItem onClick={} className="text-danger">
        Delete
      </DropdownItem>
    </DropdownMenu>
  </UncontrolledDropdown>
  );

const columns = [
  {
    dataField: 'name',

    text: 'Name',
    headerClasses: 'border-0',
    classes: 'border-0 py-2 align-middle',
    formatter: nameFormatter,
    sort: true
  },
  {
    dataField: 'content',
    headerClasses: 'border-0',
    text: 'Content',
    classes: 'border-0 py-2 align-middle',
    formatter: bodyFormatter,
    sort: true
  },
  {
    dataField: 'joined',
    headerClasses: 'border-0',
    text: 'Last modified',
    classes: 'border-0 py-2 align-middle',
    sort: true,
    align: 'right',
    headerAlign: 'right'
  },
  {
    dataField: '',
    headerClasses: 'border-0',
    text: 'Actions',
    classes: 'border-0 py-2 align-middle',
    formatter: actionFormatter,
    align: 'right'
  }
];

const SelectRowInput = ({ indeterminate, rowIndex, ...rest }) => (
  <div className="custom-control custom-checkbox">
    <input
      className="custom-control-input"
      {...rest}
      onChange={() => {}}
      ref={input => {
        if (input) input.indeterminate = indeterminate;
      }}
    />
    <label className="custom-control-label" />
  </div>
);

const selectRow = onSelect => ({
  mode: 'checkbox',
  columnClasses: 'py-2 align-middle',
  clickToSelect: false,
  selectionHeaderRenderer: ({ mode, ...rest }) => <SelectRowInput type="checkbox" {...rest} />,
  selectionRenderer: ({ mode, ...rest }) => <SelectRowInput type={mode} {...rest} />,
  headerColumnStyle: { border: 0, verticalAlign: 'middle' },
  selectColumnStyle: { border: 0, verticalAlign: 'middle' },
  onSelect: onSelect,
  onSelectAll: onSelect
});



const TemplateTable = ( props ) => {
  
  let table = createRef();
  // State
  const [isSelected, setIsSelected] = useState(false);
  const [showTemplateModal, setShowTemplateModal] = useState(false);
  const handleNextPage = ({ page, onPageChange }) => () => {
    onPageChange(page + 1);
  };


  const dispatch = useDispatch()
  const deleteHandler = (_id) => {
    dispatch(deleteHandler())
  }
  
 useEffect(() => {
  dispatch(listTemplates())
}, [])
  
  const handlePrevPage = ({ page, onPageChange }) => () => {
    onPageChange(page - 1);
  };

  const onSelect = () => {
    setImmediate(() => {
      setIsSelected(!!table.current.selectionContext.selected.length);
    });
  };

  const options = {
  
    custom: true,
    sizePerPage: 12,
    totalSize: props.templates.length
    
  };
  
  return (
    <Card className="mb-3">
      <FalconCardHeader light={false}>
      
        {isSelected ? (
          <InputGroup size="sm" className="input-group input-group-sm">
            <CustomInput type="select" id="bulk-select">
              <option>Bulk actions</option>
              <option value="Delete">Delete</option>
              <option value="Archive">Archive</option>
            </CustomInput>
            <Button color="falcon-default" size="sm" className="ml-2">
              Apply
            </Button>
          </InputGroup>
        ) : (
          <Fragment>
            <ButtonIcon onClick={(() => setShowTemplateModal(true))}icon="plus" transform="shrink-3 down-2" color="falcon-default" size="sm">
              New Template
            </ButtonIcon>
            <Modal isOpen={showTemplateModal} centered toggle={() => setShowTemplateModal(!showTemplateModal)}>
          <ModalBody className="p-0">
            <Card>
              <CardBody className="fs--1 font-weight-normal p-4"> 
                <CreateTemplate />
              </CardBody>
            </Card>
          </ModalBody>
        </Modal>
            <ButtonIcon icon="fa-download" transform="shrink-3 down-2" color="falcon-default" size="sm" className="mx-2">
              Download
            </ButtonIcon>
            <ButtonIcon icon="external-link-alt" transform="shrink-3 down-2" color="falcon-default" size="sm">
              Expand View
            </ButtonIcon>
          </Fragment>
        )}
      </FalconCardHeader>
      <CardBody className="p-0">
        <PaginationProvider pagination={paginationFactory(options)}>
          {({ paginationProps, paginationTableProps }) => {
            const lastIndex = paginationProps.page * paginationProps.sizePerPage;
            
            return (
              <Fragment>
                
                <div className="table-responsive">
                  <BootstrapTable
                    ref={table}
                    bootstrap4
                    keyField="_id"
                    data={props.templates}
                    columns={columns}
                    selectRow={selectRow(onSelect)}
                    bordered={false}
                    classes="table-dashboard table-striped table-sm fs--1 border-bottom border-200 mb-0 table-dashboard-th-nowrap"
                    rowClasses="btn-reveal-trigger border-top border-200"
                    headerClasses="bg-200 text-900 border-y border-200"
                    {...paginationTableProps}
                  />
                </div>
                <Row noGutters className="px-1 py-3 flex-center">
                  <Col xs="auto">
                    <Button
                      color="falcon-default"
                      size="sm"
                      onClick={handlePrevPage(paginationProps)}
                      disabled={paginationProps.page === 1}
                    >
                      <FontAwesomeIcon icon="chevron-left" />
                    </Button>
                    {getPaginationArray(paginationProps.totalSize, paginationProps.sizePerPage).map(pageNo => (
                      <Button
                        color={paginationProps.page === pageNo ? 'falcon-primary' : 'falcon-default'}
                        size="sm"
                        className="ml-2"
                        onClick={() => paginationProps.onPageChange(pageNo)}
                        key={pageNo}
                      >
                        {pageNo}
                      </Button>
                    ))}
                    <Button
                      color="falcon-default"
                      size="sm"
                      className="ml-2"
                      onClick={handleNextPage(paginationProps)}
                      disabled={lastIndex >= paginationProps.totalSize}
                    >
                      <FontAwesomeIcon icon="chevron-right" />
                    </Button>
                  </Col>
                </Row>
              </Fragment>
            );}
          }
        </PaginationProvider>
      </CardBody>
    </Card>
  );
};

const mapStateToProps = (state) => {
  return {
    templates: state.templates,
    auth: state.auth,
    deleteTemplate: state.deleteTemplate
  }
}

export default connect(mapStateToProps, { listTemplates })(TemplateTable);

1 Answers

The short answer is that as-is there isn't any dependency on anything from a consuming component, so it makes complete sense to externalize these declarations from the TemplateTable component.

While you could just move code back into the component to close over any dependencies in the enclosure of the functional component body I think we can do better.

I suggest currying the dispatch function to any of the specific formatters that need it, i.e.

const actionFormatter = ({ dispatch }) => (dataField, { _id }) => ( 
  // Control your row with this id
  
  <UncontrolledDropdown>
    <DropdownToggle color="link" size="sm" className="text-600 btn-reveal mr-3">
      <FontAwesomeIcon icon="ellipsis-h" className="fs--1" />
    </DropdownToggle>
    <DropdownMenu right className="border py-2">
      <DropdownItem onClick={() => console.log('Edit: ', _id)}>Edit</DropdownItem>
      <DropdownItem
        onClick={() => dispatch(deleteTemplate())}
        className="text-danger"
      >
        Delete
      </DropdownItem>
    </DropdownMenu>
  </UncontrolledDropdown>
);

And turn columns into a factory function in order to receive and pass on configurations, i.e.

const columns = ({ dispatch }) => ([ // <-- consume config & destructure
  {
    dataField: 'name',

    text: 'Name',
    headerClasses: 'border-0',
    classes: 'border-0 py-2 align-middle',
    formatter: nameFormatter,
    sort: true
  },
  {
    dataField: 'content',
    headerClasses: 'border-0',
    text: 'Content',
    classes: 'border-0 py-2 align-middle',
    formatter: bodyFormatter,
    sort: true
  },
  {
    dataField: 'joined',
    headerClasses: 'border-0',
    text: 'Last modified',
    classes: 'border-0 py-2 align-middle',
    sort: true,
    align: 'right',
    headerAlign: 'right'
  },
  {
    dataField: '',
    headerClasses: 'border-0',
    text: 'Actions',
    classes: 'border-0 py-2 align-middle',
    formatter: actionFormatter({ dispatch }), // <-- pass dispatch
    align: 'right'
  }
]);

Create the columns configuration object and pass to the table.

const config = { dispatch };

...

<BootstrapTable
  ref={table}
  bootstrap4
  keyField="_id"
  data={props.templates}
  columns={columns(config)} // <-- pass config to factory
  selectRow={selectRow(onSelect)}
  bordered={false}
  classes="table-dashboard table-striped table-sm fs--1 border-bottom border-200 mb-0 table-dashboard-th-nowrap"
  rowClasses="btn-reveal-trigger border-top border-200"
  headerClasses="bg-200 text-900 border-y border-200"
  {...paginationTableProps}
/>
Related