Abstraction for cleaner CRUD apps in React

Viewed 81

I have a new app I'm working on that has a lot of CRUD functionality. A list/grid and then a modal with multiple tabs of content and a save button in the modal header. I'm wondering about patterns to separate state logic from the JSX component so it's easier to read/maintain.

I read about "lifting state", but that makes this item component unruly with the callbacks (and it'll get worse for sure as more detail is added). Is it possible to put the handlers and the state itself in a custom hook and then pull them in where needed, instead of having it at "the closest common ancestor"? For example,

const {company, setCompany, updateCompany, getCompanyByCode, createCompany, onNameChangeHandler, onTitleChangeHandler, etc.} from "./useCompany" 

Would all users of this hook have the same view of the data and be able to see and effect updates? I've read about putting data fetching logic in hooks, but what about change handlers for what could be 20 or more fields? I think it's the clutter of the callbacks that bothers me the most.

Here is a shortened version of a component that renders the tabs and data for a specific domain item. Is this a use case for context and/or custom hooks?

import {
  getCompanyByCode,
  updateCompany,
  createCompany,
  companyImageUpload,
} from "../../api/companyApi";

const initialState: CompanyItemDetailModel = {
  code: "",
  name: { en: "", es: "" },
  title: { en: "", es: "" },
  description: { en: "", es: "" },
  displayOrder: 0,
  enabled: false,
};

export interface CompanyItemProps {
  onDetailsDialogCloseHandler: DetailsCancelHandler;
  companyCode: string;
  onSaveHandler: () => void;
}
const CompanyItem = (props: CompanyItemProps) => {
  const { CompanyCode, onDetailsDialogCloseHandler, onSaveHandler } = props;
  const classes = useStyles();
  const [tabValue, setTabValue] = useState<Number>(0);
  const [isLoading, setIsLoading] = useState(true);
  const nameRef = useRef({ en: "", es: "" });
  const [Company, setCompany] = useState<CompanyItemDetailModel>(initialState);
  const [saveGraphics, setSaveGraphics] = useState(false);

  useEffect(() => {
    async function getCompany(code: string) {
      try {
        const payload = await getCompanyByCode(code);
        nameRef.current.en = payload.name.en;
        setCompany(payload);
      } catch (e) {
        console.log(e);
      } finally {
        setIsLoading(false);
      }
    }

    if (CompanyCode.length > 0) {
      getCompany(CompanyCode);
    }
  }, [CompanyCode]);

  function handleTabChange(e: React.ChangeEvent<{}>, newValue: number) {
    setTabValue(newValue);
  }

  function detailsDialogSaveHandler() {
    const CompanyToSave = {
      ...Company,
      name: JSON.stringify({ ...Company.name }),
      description: JSON.stringify({ ...Company.description }),
      title: JSON.stringify({ ...Company.title }),
    };

    if (CompanyCode.length > 0) {
      updateCompany(CompanyToSave as any).then((e) => handleSaveComplete());
    } else {
      createCompany(CompanyToSave as any).then((e) => handleSaveComplete());
    }

    setSaveGraphics(true);
  }

  function handleSaveComplete() {
    onSaveHandler();
  }

  function detailsDialogCloseHandler(e: React.MouseEvent) {
    onDetailsDialogCloseHandler(e);
  }

  function handleNameTextChange(e: React.ChangeEvent<HTMLInputElement>) {
    setCompany((prevState) => ({
      ...prevState,
      name: {
        ...prevState.name,
        en: e.target.value,
      },
    }));
  }

  function handleCompanyCodeTextChange(e: React.ChangeEvent<HTMLInputElement>) {
    setCompany((prevState) => ({
      ...prevState,
      code: e.target.value,
    }));
  }

  function handleEnabledCheckboxChange(e: React.ChangeEvent<HTMLInputElement>) {
    setCompany((prevState) => ({
      ...prevState,
      enabled: e.target.checked,
    }));
  }

function handleTitleTextChange(e: React.ChangeEvent<HTMLInputElement>) {
    setCompany((prevState) => ({
      ...prevState,
      title: {
        ...prevState.title,
        en: e.target.value,
      },
    }));
  }
return (
    <DetailsDialog>
      <div className={classes.root}>
        <Form>
          <ModalHeader
            name={nameRef.current}
            languageCode={"en"}
            onTabChange={handleTabChange}
            onCancelHandler={detailsDialogCloseHandler}
            onSaveHandler={detailsDialogSaveHandler}
            tabTypes={[
              DetailsTabTypes.Details,
              DetailsTabTypes.Images,
            ]}
          />
          <TabPanel value={tabValue} index={0} dialog={true}>
            <CompanyDetailsTab
              details={Company}
              isEditMode={CompanyCode.length > 1}
              languageCode={"en"}
              handleNameTextChange={handleNameTextChange}
              handleCompanyCodeTextChange={handleCompanyCodeTextChange}
              handleEnabledCheckboxChange={handleEnabledCheckboxChange}
              handleTitleTextChange={handleTitleTextChange}
              handleDescriptionTextChange={handleDescriptionTextChange}
            />
          </TabPanel>

          <TabPanel value={tabValue} index={1} dialog={true}>
            <ImageList />
          </TabPanel>
        </Form>
      </div>
    </DetailsDialog>
  );
};
export default CompanyItem;

0 Answers
Related