我正试图在useEffect调用中使用钩子只运行一次(并加载一些数据(。我一直错误地认为我不能做到这一点(尽管我在另一个应用程序中做了完全相同的事情,不确定为什么1有效而另一个无效(,我知道我可能违反了Hooks规则。。。那么,我该怎么办呢?我的目标是将所有CRUD操作逻辑卸载到一个简单的钩子中。这是MenuItem,它是试图使用钩子获取数据的组件。
const MenuItem = () => {
const [ID, setID] = useState<number | null>(null);
const [menu, setMenu] = useState<Item[]>([]);
const { getMenu, retrievedData } = useMenu();
//gets menu items using menu-hook
useEffect(() => {
getMenu();
}, []);
//if menu is retrieved, setMenu to retrieved data
useEffect(() => {
if (retrievedData.length) setMenu(retrievedData);
}, []);
//onClick of menu item, displays menu item description
const itemHandler = (item: Item) => {
if (ID === null || ID !== item._id) {
setID(item._id);
} else {
setID(null);
}
};
return ...
};
这里是getMenu,它是处理逻辑和数据检索的自定义钩子。
const useMenu = () => {
const backendURL: string = 'https://localhost:3001/api/menu';
const [retrievedData, setRetrievedData] = useState<Item[]>([]);
const getMenu = async () => {
await axios
.get(backendURL)
.then((fetchedData) => {
setRetrievedData(fetchedData.data.menu);
})
.catch((error: Error) => {
console.log(error);
setRetrievedData([]);
});
};
return { getMenu, retrievedData };
};
export default useMenu;
最后是错误。
Invalid hook call. Hooks can only be called inside of the body of a function component.
我想补充一点,我也在使用Typescript,它现在没有抱怨。
您可以做一些事情来改进此代码,这可能在将来有所帮助。你是对的,你打破了钩子的规则,但没有必要!如果将fetch
移出挂钩(无需在每次渲染时重新定义它(,则不将其放在deps
数组中是有效的,因为它是一个常量。
我还会让你的useMenu
钩子为你处理加载/返回加载值的所有细节。
const fetchMenu = async () => {
const backendURL: string = 'https://localhost:3001/api/menu';
try {
const { data } = await axios.get(backendURL);
return data.menu;
} catch (error: AxiosError) {
console.log(error);
return [];
};
}
export const useMenu = () => {
const [items, setItems] = useState<Item[]>([]);
useEffect(() => {
fetchMenu.then(result => setItems(result);
}, []);
return items;
};
现在你可以消耗你的钩子:
const MenuItem = () => {
const [ID, setID] = useState<number | null>(null);
// Now this will automatically be an empty array while loading, and
// the actual menu items once loaded.
const menu = useMenu();
// --- 8< ---
return ...
};
其他几件事-
- 尽量避免默认导出,因为默认导出很糟糕
- 这里有很多包可以让你的生活更轻松!
react-query
是一个不错的选择,因为它将管理围绕外部数据的所有生命周期/状态管理 - 或者,看看react use,这是一个自定义钩子的集合,可以帮助处理像这样的许多常见情况。您可以使用useAsync钩子来简化上面的
useMenu
钩子:
const backendURL: string = 'https://localhost:3001/api/menu';
const useMenu = () => useAsync(async () => {
const { data } = await axios.get(backendURL);
return data.menu;
});
现在消耗掉这个钩子:
const MenuItem = () => {
const { value: menu, loading, error } = useMenu();
if (loading) {
return <LoadingIndicator />;
}
if (error) {
return <>The menu could not be loaded</>;
}
return ...
};
除了能够在钩子获取时显示加载指示符外,如果您的组件在异步函数完成加载之前卸载(上面的代码不处理(,useAsync
也不会向您发出内存泄漏警告。
在这个项目上工作了一段时间后,我还找到了另一个干净的解决方案,我相信它不会打破钩子的规则。这需要我设置一个自定义http挂钩,该挂钩使用sendRequest
函数来处理应用程序范围的请求。让我明确一点,这不是一个简单的解决方案,我确实在增加复杂性,但我相信这会有所帮助,因为我将在应用程序中提出多种不同类型的请求。这是sendRequest函数。注意使用回调挂钩以防止不必要的重发
const sendRequest = useCallback(
async (url: string, method = 'GET', body = null, headers = {}) => {
setIsLoading(true);
const httpAbortCtrl = new AbortController();
activeHttpRequests.current.push(httpAbortCtrl);
try {
const response = await fetch(url, {
method,
body,
headers,
signal: httpAbortCtrl.signal,
});
const responseData = await response.json();
activeHttpRequests.current = activeHttpRequests.current.filter(
(reqCtrl) => reqCtrl !== httpAbortCtrl
);
if (!response.ok) throw new Error(responseData.message);
setIsLoading(false);
return responseData;
} catch (error: any) {
setError(error);
setIsLoading(false);
throw error;
}
},
[]
);
这是新的useMenu挂钩,请注意,我不需要返回getMenu,因为每次在我的应用程序中使用sendRequest时,都会自动调用getMenu。
export const useMenu = () => {
const { sendRequest } = useHttpClient();
const [menu, setMenu] = useState<MenuItem[]>([]);
const [message, setMessage] = useState<string>('');
useEffect(() => {
const getMenu = async () => {
try {
const responseData = await sendRequest(`${config.api}/menu`);
setMenu(responseData.menu);
setMessage(responseData.message);
} catch (error) {}
};
getMenu();
}, [sendRequest]);
return { menu, message };
};
祝好运