reactjstypescriptmaterial-uireact-hook-form

Typescript which is correct type for control parameter?


I created a typescript react component FormInput for my form. This component uses the MUI framework and react-hook-form. The problem is that I can't set the correct type in my component for param "control". I temporarily consolidated it with the use of type any.

Input type into props control is type Control<TrekEditDto, object>.

In documentation react-hook-form is that param control?: Control<FieldValues, object> | undefined

I think that the solution is to use generic.

This is my react input component:

import { Controller } from 'react-hook-form';
import { TextField, Typography } from '@material-ui/core';
import React from 'react';

type FormInputProps = {
  control: any;
  name: string;
  label: string;
  multiline?: boolean;
  defaultValue?: string;
  fullWidth?: boolean;
  disabled?: boolean;
  error?: { message: string };
};

const FormInput = ({ control, name, label, multiline, defaultValue, fullWidth, disabled, error }: FormInputProps) => {
  return (
    <>
      <Controller
        control={control}
        name={name}
        defaultValue={defaultValue}
        render={(...inputProps) => {
          return (
            <>
              <TextField
                {...inputProps}
                fullWidth={fullWidth}
                label={label}
                multiline={multiline}
                disabled={disabled}
                error={error != null}
              />
              <Typography variant="inherit" color="textSecondary">
                {error?.message}
              </Typography>
            </>
          );
        }}
      />
    </>
  );
};

export default FormInput;

This part is calling my component:

<FormInput
   control={control}
   name="translation.cz.name"
   label={t('trek.create.formField.name.cs')}
/>

It is full file contain calling my component.

import React, {useEffect, useState} from "react";
import TrekService from "../services/TrekService";
import {
    Box,
    Button,
    Container, DialogActions, DialogContent, DialogContentText,
    FormLabel,
    Grid,
    Paper,
    Snackbar,
    Typography
} from "@material-ui/core";
import {FormRadio} from "../components/FormRadio";
import {http} from "../../../common/axios-api";
import {useForm} from "react-hook-form";
import {useTranslation} from "react-i18next";
import {DropzoneArea} from "material-ui-dropzone";
import AlertDialog from "../../../components/AlertDialog/AlertDialog";
import {Alert} from "@material-ui/lab";
import {useHistory} from "react-router-dom";
import {Permissions} from "../../../auth/hooks/Permissions";
import ROLES from "../../../auth/Roles";
import {TYPE_OF_STATUS} from "../commons/TrekConstants";
import {FormAutocomplete} from "../components/FormAutocomplete";
import {FormDifficulty} from "../components/FormDifficulty";
import {FormExperiences} from "../components/FormExperiences";
import {TrekEditDto} from "../../../models/Trek";
import {Codebook} from "../../../models/Codebook";
import FormInput from "../components/FormInput";
import FormInputMultiline from "../components/FormInputMultiline";

export default function EditTrek() {

    const {t} = useTranslation(['translation', 'common']);
    const history = useHistory();
    const [trek, setTrek] = useState<TrekEditDto>();
    const [codebook, setCodebook] = useState<Codebook>();
    const {handleSubmit, control, reset, setValue, watch, formState: {errors}} = useForm<TrekEditDto>({
        defaultValues: trek
    });
    const [trekImages, setTrekImages] = useState<File[]>([]);
    const [coverImage, setCoverImage] = useState<File[]>([]);
    const [trekGpx, setTrekGpx] = useState<File[]>([]);
    const [openSuccessDialog, setOpenSuccessDialog] = useState(false);
    const [openErrorMessage, setOpenErrorMessage] = useState(false);
    const watchTrekType = watch('trip_type_id');

    // max file size is 2MB
    const MAX_FILE_SIZE_FOR_UPLOAD = 2 * 1000 * 1000;

    // max number of file for upload image
    const MAX_NUMBER_FILES_FOR_UPLOAD = 6;

    // max number gpx files for upload
    const MAX_NUMBER_GPX_FILES_FOR_UPLOAD = 1;

    useEffect(() => {
        async function fetchData() {
            try {
                await loadCoodBook();
                const trekId = (new URLSearchParams(window.location.search)).get('trekId');
                if (trekId == null) {
                    throw new Error(`Couldnt load data becouse trek id = ${trekId}`);
                }
                await TrekService.findById(trekId)
                    .then(response => {
                            setTrek(response.data);
                            reset(response.data);
                        }
                    )
            } catch (e) {
                console.error(e)
            }
        }

        fetchData();
    }, []);

    const loadCoodBook = () => {
        http.get(`web/codebook`)
            .then(response => {
                setCodebook(response.data);
            })
            .catch(reason => {
                console.error('Nastala chyba pri nacitani codebook: ', reason)
            })
    }

    const publish = (data: TrekEditDto) => {
        sendForm(data, TYPE_OF_STATUS.PUBLISHED);
    };

    const save = (data: TrekEditDto) => {
        sendForm(data, TYPE_OF_STATUS.TO_CHECK);
    };

    const sendForm = async (data: TrekEditDto, status: number) => {
        //convert data into correct format
        const sendData = JSON.parse(JSON.stringify(data));
        sendData.state_id = data.state_id?.id;
        sendData['status'] = status;
        if (data.danger != null) {
            sendData.danger = data.danger.map(danger => danger.id);
        }
        if (data.equipment != null) {
            sendData.equipment = data.equipment.map(equip => equip.id);
        }
        if (data.pois != null) {
            sendData.poi = data.pois.map(poi => poi.id);
        }

        // workaround for backend - removed start_point and destination_point
        delete sendData.start_point;
        delete sendData.destination_point;

        if (watchTrekType !== '2') {
            delete sendData.check_point;
        }

        try {
            const trekId = sendData?.trekId;
            await TrekService.updateTrek(trekId, sendData);

            // delete previous images
            await trek?.images.map(img => {
                const nameOfImage = img.substring(img.lastIndexOf('/') + 1, img.length);
                TrekService.deleteTrekImage(trekId, nameOfImage);
            });

            // delete previous cover image
            if (trek?.coverImage != undefined) {
                const nameOfImage = trek?.coverImage.substring(trek?.coverImage.lastIndexOf('/') + 1, trek?.coverImage.length);
                await TrekService.deleteCoverTrekImage(trekId, nameOfImage);
            }

            trekImages.length > 0 && trekId && trekImages.map(image => uploadTrekImagesFiles(trekId, image));
            coverImage.length > 0 && trekId && await uploadTrekCoverImagesFiles(trekId, coverImage[0]);
            trekGpx.length > 0 && trekId && await uploadTrekGpxFiles(trekId);
            handleOpenSuccessDialog();
        } catch (error) {
            console.error('update trek error = ', error);
            setOpenErrorMessage(true);
        }
    }

    const uploadTrekGpxFiles = (trekId: number) => {
        const formData = new FormData();
        formData.append('file', trekGpx[0])
        return TrekService.uploadTrekGpx(trekId, formData);
    }

    const uploadTrekImagesFiles = (trekId: number, image: File) => {
        const formData = new FormData();
        formData.append('file', image);
        return TrekService.uploadTrekImage(trekId, formData);
    }

    const uploadTrekCoverImagesFiles = (trekId: number, image: File) => {
        const formData = new FormData();
        formData.append('file', image);
        return TrekService.uploadTrekCoverImage(trekId, formData);
    }

    const handleCloseErrorMessage = () => {
        setOpenErrorMessage(false);
    }

    const handleCloseSuccessDialog = () => {
        setOpenSuccessDialog(false);
    }

    const handleOpenSuccessDialog = () => {
        setOpenSuccessDialog(true);
    }

    return (
        codebook && trek && Object.entries(trek).length > 0 && (
            <Container>
                <Paper>
                    <Box component="form" px={8} py={8}>
                        <Typography variant="h4"> {t('trek.edit.formTitle')}</Typography>
                        <Grid container spacing={3}>
                            <Grid item xs={12} sm={12}>
                                <FormInput
                                    control={control}
                                    name="translation.cz.name"
                                    label={t('trek.create.formField.name.cs')}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormInput
                                    control={control}
                                    name="translation.en.name"
                                    label={t('trek.create.formField.name.en')}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormInput
                                    control={control}
                                    name="translation.cz.title"
                                    label={t('trek.create.formField.title.cs')}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormInput
                                    control={control}
                                    name="translation.en.title"
                                    label={t('trek.create.formField.title.en')}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormInputMultiline
                                    control={control}
                                    name="translation.cz.description"
                                    label={t('trek.create.formField.description.cs')}
                                    maxRows={6}
                                    minRows={6}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormInputMultiline
                                    control={control}
                                    name="translation.en.description"
                                    label={t('trek.create.formField.description.en')}
                                    maxRows={6}
                                    minRows={6}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormInputMultiline
                                    control={control}
                                    name="note"
                                    label={t('trek.create.formField.note')}
                                    maxRows={6}
                                    minRows={1}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormInput
                                    control={control}
                                    name="duration"
                                    label={t('trek.create.formField.duration')}
                                    required={true}
                                    error={errors.duration}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormInput
                                    control={control}
                                    name="distance"
                                    label={t('trek.create.formField.distance')}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormInput
                                    control={control}
                                    name="elevation_difference"
                                    label={t('trek.create.formField.elevationDifference')}
                                />
                            </Grid>
                            <FormDifficulty control={control} setValue={setValue} menuItems={codebook?.difficulty}/>
                            <FormExperiences control={control} setValue={setValue}/>
                            <Grid item xs={12} sm={12}>
                                <FormInput
                                    control={control}
                                    name="highest_point"
                                    label={t('trek.create.formField.highestPoint')}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormInput
                                    control={control}
                                    name="link"
                                    label={t('trek.create.formField.link')}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormRadio name='trip_type_id'
                                           control={control}
                                           label={t('trek.create.formField.typeOfTrek')}
                                           menuItems={codebook?.tripType}
                                           setValue={setValue}
                                />
                            </Grid>
                            {watchTrekType === '2' && (
                                <>
                                    <Grid item xs={12} sm={12}>
                                        <FormInput
                                            control={control}
                                            name="check_point.lat"
                                            label={t('trek.create.formField.latitude')}
                                        />
                                    </Grid>
                                    <Grid item xs={12} sm={12}>
                                        <FormInput
                                            control={control}
                                            name="check_point.long"
                                            label={t('trek.create.formField.longitude')}
                                        />
                                    </Grid>
                                </>
                            )}
                            <Grid item xs={12} sm={12}>
                                <FormAutocomplete label={t('trek.create.formField.state')}
                                                  control={control}
                                                  name="state_id"
                                                  options={codebook?.state}
                                                  multiple={false}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormAutocomplete label={t('trek.create.formField.danger')}
                                                  control={control}
                                                  name="danger"
                                                  options={codebook?.danger}
                                                  multiple={true}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormAutocomplete label={t('trek.create.formField.equipment')}
                                                  control={control}
                                                  name="equipment"
                                                  options={codebook?.equipment}
                                                  multiple={true}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormAutocomplete label={t('trek.create.formField.poi')}
                                                  control={control}
                                                  name="pois"
                                                  options={codebook?.poi}
                                                  multiple={true}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormRadio name='camping_id'
                                           control={control}
                                           label={t('trek.create.formField.camping')}
                                           menuItems={codebook?.camping}
                                           setValue={setValue}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormRadio name='dog_friendly_id'
                                           control={control}
                                           label={t('trek.create.formField.dogFriendly')}
                                           menuItems={codebook?.dogFriendly}
                                           setValue={setValue}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormLabel
                                    component="legend">{t('trek.create.formField.coverImage', {
                                    imageCount: 1,
                                    imageMaxSize: MAX_FILE_SIZE_FOR_UPLOAD / 1000000
                                })}</FormLabel>
                                <DropzoneArea
                                    name={'coverImage'}
                                    filesLimit={1}
                                    acceptedFiles={['image/*']}
                                    onChange={(files) => {
                                        setCoverImage(files)
                                    }}
                                    maxFileSize={MAX_FILE_SIZE_FOR_UPLOAD}
                                    dropzoneText={t('material-ui-dropzone.dropzonetext')}
                                    initialFiles={[trek?.coverImage]}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormLabel
                                    component="legend">{t('trek.create.formField.images', {
                                    imageCount: MAX_NUMBER_FILES_FOR_UPLOAD,
                                    imageMaxSize: MAX_FILE_SIZE_FOR_UPLOAD / 1000000
                                })}</FormLabel>
                                <DropzoneArea
                                    name={'trekImages'}
                                    filesLimit={MAX_NUMBER_FILES_FOR_UPLOAD}
                                    acceptedFiles={['image/*']}
                                    onChange={(files) => {
                                        setTrekImages(files)
                                    }}
                                    maxFileSize={MAX_FILE_SIZE_FOR_UPLOAD}
                                    dropzoneText={t('material-ui-dropzone.dropzonetext')}
                                    initialFiles={trek?.images}
                                />
                            </Grid>
                            <Grid item xs={12} sm={12}>
                                <FormLabel component="legend">{t('trek.create.formField.gpx')}</FormLabel>
                                <DropzoneArea
                                    name={'trekGpx'}
                                    filesLimit={MAX_NUMBER_GPX_FILES_FOR_UPLOAD}
                                    // acceptedFiles={['application/gpx+xml']}
                                    onChange={(files) => {
                                        setTrekGpx(files)
                                    }}
                                    dropzoneText={t('material-ui-dropzone.dropzonetext')}
                                />
                            </Grid>
                            <Grid
                                container
                                direction="row"
                                justifyContent="center"
                            >
                                <Button variant={"contained"}
                                        onClick={handleSubmit(save)}>{t('trek.create.form.button.submit.save')}</Button>

                                <Permissions roles={[ROLES.admin, ROLES.approver]}>
                                    <Button variant={"contained"}
                                            onClick={handleSubmit(publish)}>{t('trek.create.form.button.submit.publish')}</Button>
                                </Permissions>
                            </Grid>
                        </Grid>

                        <Snackbar open={openErrorMessage} autoHideDuration={6000} onClose={handleCloseErrorMessage}>
                            <Alert onClose={handleCloseErrorMessage} severity="error">
                                {t('trek.edit.message.error')}
                            </Alert>
                        </Snackbar>

                        <AlertDialog open={openSuccessDialog}>
                            <DialogContent>
                                <DialogContentText id="alert-dialog-description">
                                    {t('trek.edit.message.success')}
                                </DialogContentText>
                            </DialogContent>
                            <DialogActions>
                                <Button onClick={handleCloseSuccessDialog} color="primary">
                                    {t('trek.edit.message.button.edit')}
                                </Button>
                                <Button onClick={() => history.push('/adventurerClient/treks')} color="primary">
                                    {t('trek.edit.message.button.goToTrekList')}
                                </Button>
                            </DialogActions>
                        </AlertDialog>
                    </Box>
                </Paper>
            </Container>
        )
    )
}

Solution

  • I think the documentation is pretty clear that you have to use your form values as a type.

    type FormValues = {
      'translation.cz.name': string
    }
    
    type FormInputProps = {
      control: Control<FormValues>
      // ...
    }
    

    Though I hope I don't have to tell you that if this "translation.cz.name" string is replaceable, your form processing logic will find itself in trouble. The name attribute is not supposed to be exposed to the end user, so there is absolutely no need to localise it.