Я создаю довольно простое приложение 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 ];
};