Реагируйте на организацию и дизайн кода хуков - PullRequest
0 голосов
/ 25 апреля 2020

Я создаю довольно простое приложение CRUD со следующей структурой компонентов: MainContent содержит все другие компоненты и имеет настраиваемый хук (useItems), который делает все необходимое и возвращает результат в MainContent, который распределяет результаты по дочерним компонентам. Меня беспокоит, если это правильный подход, MainContent - это центральное место, а ловушка useItems - это то, что может оказаться хууууууге, поскольку новые действия добавляются позже.

К сожалению, я не могу поделиться своим репо публично, но я вставил упрощенную версию основных компонентов и хуков, так что вопрос не только теоретический. Что было бы лучше для организации и дизайна кода?

Кроме того, что свидетельствует о том, что что-то не так, я испытываю много повторных визуализаций приложения (например, когда я нажмите, чтобы открыть AddEditModal, моя таблица будет перерисована).

Я довольно новичок в разработке, особенно в области хуков, поэтому помощь будет весьма полезна.


Основной централизованный компонент:

export const MainContent: React.FC = () => {
    const [currentPage, setCurrentPage] = useState<number>(DEFAULT_PAGE);
    const [pageSize, setPageSize] = useState<number>(DEFAULT_PAGE_SIZE);
    const [tableAction, setTableAction] = useState<TableRowAction>(TABLE_ROW_ACTION_NONE);
    const {
        loading,
        checkedAll,
        items,
        totalPages,
        totalRecords,
        allSearchParams,
        filterItems,
        saveItems,
        updateItems,
        updateAllSearchParams,
        updateCheckedAll
    } = useItems();
    const [addEditModalData, setAddEditModalData] = useState({ showModal: false, item: EMPTY_ITEM });
    const [searchData, setSearchData] = useState<ItemFilterOptions>(EMPTY_FILTER_OPTIONS);

    useEffect(() => {
        const fetchData = async () => {
            // API call to get filter dropdown options
        };
        fetchData();
    }, []);

    // a bunch of functions that take params from components and call functions in useItems hook

    return (
        <>
            <Navigation addNew={() => setAddEditModalData({ showModal: true, item: EMPTY_ITEM })} />
            <>
                <Filter
                    loading={loading}
                    pageSize={pageSize}
                    currentPage={currentPage}
                    searchData={searchData}
                    onFilter={allSearchParams => filterItems(allSearchParams)}
                    setPageSize={setPageSizeFromFilter}
                    setCurrentPage={setCurrentPageFromFilter}
                />
                <TableContent
                    loading={loading}
                    pageSize={pageSize}
                    currentPage={currentPage}
                    totalPages={totalPages}
                    totalCount={totalRecords}
                    onChangePage={onChangePage}
                    onChangePageSize={onChangePageSize}
                    table={
                        <Table
                            loading={loading}
                            disabledAction={false /*TODO*/}
                            checkedAll={checkedAll}
                            onCheckedAll={onCheckedAll}
                            items={items}
                            onActionFinish={() => {/*TODO*/}}
                            tableAction={tableAction}
                            onRowChecked={onRowChecked}
                            onEdit={item => setAddEditModalData({ showModal: true, item })}
                            onDelete={() => {/*TODO*/}}
                            onAnotherItemAdd={() => {/*TODO*/}}
                        /> 
                />
                {addEditModalData.showModal && <AddEditModal 
                    item={addEditModalData.item}
                    saving={loading}
                    searchData={searchData}
                    searchThings1={filterService().searchThings1()}
                    onClose={() => setAddEditModalData({ ...addEditModalData, showModal: false })}
                    onSave={(item, selectedThings) => saveItems(item, selectedThings)}
                />}
            <>
        <>
    )
};

Фильтрующий компонент ( так что вы можете увидеть используемые здесь хуки):

export const Filter: React.FC<Props> = (props: Props) => {
    const isFirstRender = useRef(true);
    const [selectedValues, setSelectedValues] = useState<ItemFilterSelectedValues>(EMPTY_SEARCH_PARAMS);

    useEffect(() => {
        const parseUrlData = async () => {
            const parsedUrl: ItemUrlFilter = parseUrlParams(window.top.location.search.substr(1));
            if (parsedUrl !== null) {
                const urlParams = mapSearchFromUrl(parsedUrl, props.searchData);
                props.setPageSize(urlParams.pageSize);
                props.setCurrentPage(urlParams.currentPage);
                setSelectedValues(urlParams.searchParams);
            } else {
                isFirstRender.current = false;
            }
        };
        if (isFirstRender.current && props.searchData !== EMPTY_FILTER_OPTIONS) {
            parseUrlData();
        }
    }, [props.searchData, props]);

    const applyFilter = useCallback(() => {
        const allSearchParams: AllSearchParams = mapToSearchParams(props.currentPage, props.pageSize, selectedValues);
        props.onFilter(allSearchParams);
    }, [props, selectedValues]);

    useEffect(() => {
        if (isFirstRender.current && selectedValues !== EMPTY_SEARCH_PARAMS) {
            isFirstRender.current = false;
            applyFilter();
        }
    }, [selectedValues, applyFilter]);

    return (
        <FilterComponent
            loading={props.loading}
            onFilter={applyFilter}
            onClear={() => setSelectedValues(EMPTY_SEARCH_PARAMS)}
        >
            <AsyncSelect
                defaultValue={selectedValues.selectedThing1}
                options={filterService().searchThings1}
                onChange={thing1 => setSelectedValues({ ...selectedValues, selectedThings1: thing1 })}
            />
            <MultiSelect
                selectedOptions={selectedValues.selectedThing2}
                options={props.searchData.thing2Options}
                onChange={thing2 => setSelectedValues({ ...selectedValues, selectedThings2: thing2 })}
            />
            // more items here
        </FilterComponent>
    );
};

пользовательский хук useItems (этот огромный хук, который, вероятно, будет видеть дополнительные действия, добавленные к нему):

export const useItems = () => {
    const [checkedAll, setCheckedAll] = useState<boolean>(false);
    const [items, setItems] = useState<ItemInternal[]>([]);
    const [totalPages, setTotalPages] = useState<number>(DEFAULT_TOTAL_PAGES);
    const [totalRecords, setTotalRecords] = useState<number>(DEFAULT_TOTAL_RECORDS);
    const [allSearchParams, setAllSearchParams] = useState<AllSearchParams>(EMPTY_ALL_SEARCH_PARAMS);
    const [saveRequest, setSaveRequest] = useState<ItemInternal[]>([]);
    const [ filterResponse, filterLoading, setFilterUrl, setFilterParams ] = useApiRequest();
    const [ saveRequestResponse, saveRequestLoading, setSaveRequestUrl, setSaveRequestParams ] = useApiRequest();
    // more useApiRequests will be added here

    useEffect(() => {
        const setData = async () => {
            const { items, totalPages, totalRecords } = filterResponse.data;
            setItems(items.map((item: Item) => mapToInternal(item)));
            setTotalPages(totalPages);
            setTotalRecords(totalRecords);
        };
        if (filterResponse.data) {
            setData();
        }
    }, [filterResponse]);

    useEffect(() => {
        const setData = async () => {
            const ids: number[] = saveRequestResponse.data;
            const updated: ItemInternal[] = ids.map(id => ({ ...saveRequest[ids.indexOf(id)], id }));
            setItems(items => [ ...updated, ...items ]);
            setSaveRequest([]);
        };
        if (saveRequestResponse.data) {
            setData();
        }
    }, [saveRequestResponse]); // TODO: fix dependency array

    return {
        loading: filterLoading, // TODO fix loading later
        checkedAll,
        items,
        totalPages,
        totalRecords,
        allSearchParams,
        filterItems: (allSearchParams: AllSearchParams) => {
            setAllSearchParams(allSearchParams);
            setFilterUrl(`./api/item/filter?page=${allSearchParams.currentPage}&pageSize=${allSearchParams.pageSize}`);
            setFilterParams(mapSearchParams(allSearchParams.searchParams));
            serializeToUrlParams(mapSearchParamsToUrl(allSearchParams.searchParams, allSearchParams.currentPage, allSearchParams.pageSize));
        },
        saveItems: (item: ItemInternal, selectedGateways: Option[]) => {
            setSaveRequest(mapToInternals(item, selectedGateways));
            setSaveRequestUrl('./api/item');
            setSaveRequestParams(mapToSaveRequest(item, selectedGateways));
        },
        updateItems: (items: ItemInternal[]) => setItems(items),
        updateAllSearchParams: (allSearchParams: AllSearchParams) => setAllSearchParams(allSearchParams),
        updateCheckedAll: (checkedAll: boolean) => setCheckedAll(checkedAll),
        // more actions will be added here
    };
};

пользовательский useApiRequest крючок:

export const useApiRequest = (): [AxiosResponse, boolean, Function, Function] => {
    const { dispatch } = useContext(ErrorContext);
    const [loading, setLoading] = useState<boolean>(false);
    const [response, setResponse] = useState<AxiosResponse>({} as AxiosResponse);
    const [url, setUrl] = useState<string>('');
    const [params, setParams] = useState<any>();

    useEffect(() => {
        const fetchData = async () => {
            const ax = axios.create({});
            ax.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
            ax.interceptors.response.use(response => response, error => {
                if (error.response.status === 401) {
                    dispatch({ type: 'setErrorState', errorState: { isAuthenticated: false, isAuthorized: false, hasErrors: false } });
                }
                if (error.response.status === 403) {
                    dispatch({ type: 'setErrorState', errorState: { isAuthenticated: true, isAuthorized: false, hasErrors: false } });
                }
                if (error.response.status >= 500) {
                    dispatch({ type: 'setErrorState', errorState: { isAuthenticated: true, isAuthorized: true, hasErrors: true } });
                }

                return error;
            });

            setLoading(true);
            const result = await ax.post(url, params);
            setResponse(result);
            setLoading(false);
            setUrl('');
        };
        if (url !== '') {
            fetchData();
        }
    }, [dispatch, url, params]);

    return [ response, loading, setUrl, setParams ];
}; 
...