Context
All of my components need to fetch data.
How I fetch
Therefore I use a custom hook which fetches the data using the useEffect hook and axios. The hook returns data or if loading or on error false. The data is an object with mostly an array of objects.
How I render
I render my data conditional with an ternary (?) or the use of the short circuit (&&) operator.
Question
How can I destructure my data dependent if my useFetch hook is returning false or the data in a way i can reuse the logic or an minimal implementation to the receiving component?
What I have tried
moving the destructuring assignment into an if statement like in return. Issue: "undefined" errors => data was not available yet
moving attempt 1 to function. Issue: function does not return variables (return statement does not work either)
//issue
fuction Receiver() {
const query = headerQuery();
const data = useFetch(query);
const loaded = data.data //either ```false``` or object with ```data```
/*
The following part should be in an easy condition or passed to an combined logic but I just dont get it
destructuring assignment varies from component to component
ex:
const {
site,
data: {
subMenu: {
description,
article_galleries,
image: {
caption,
image: [{url}],
},
},
},
} = data;
*/
return loaded?(
<RichLink
title={title}
text={teaserForText}
link={link}
key={id}
></RichLink>):<Loading />
(
//for context
import axios from "axios";
import {
useHistory
} from "react-router-dom";
import {
useEffect,
useState
} from "react";
function useFetch(query) {
const [data, setData] = useState(false);
const [site, setSite] = useState(""); // = title
const history = useHistory();
useEffect(() => {
axios({
url: "http://localhost:1337/graphql",
method: "post",
data: {
query: query,
},
})
.then((res) => {
const result = res.data.data;
setData(result);
if (result === null) {
history.push("/Error404");
}
setSite(Object.keys(result)[0]);
})
.catch((error) => {
console.log(error, "error");
history.push("/Error");
});
}, [query, history, setData, setSite]);
return {
data: data,
site: site
};
}
export default useFetch;
)
You can return the error, data and your loading states from your hook. Then the component implementing the hooks can destructure all of these and do things depending upon the result. Example:
const useAsync = () => {
// I prefer status to be idle, pending, resolved and rejected.
// where pending status is loading.
const [status, setStatus] = useState('idle')
const [data, setData] = useState([])
const [error, setError] = useState(null)
useEffect(() => {
setStatus('pending')
axios.get('/').then(resp => {
setStatus('resolved')
setData(resp.data)
}).catch(err => {
setStatus('rejected') // you can handle error boundary
setError(err)
})
}, []}
return {status, data, error}
}
Component implementing this hook
const App = () => {
const {data, status, error} = useAsync()
if(status === 'idle'){
// do something
}else if(status === 'pending'){
return <Loader />
}else if(status === 'resolved'){
return <YourComponent data ={data} />
}else{
return <div role='alert'>something went wrong {error.message}</div>
}
}
the hooks can be enhanced more with the use of dynamic api functions.
Related
I have a custom hook named "useFetch" which makes an AJAX request and stores the result in the state. I simply want to format the data received from the ajax using a function in my component but not sure how to do this since the function needs to be called only after the data is received.
An example is below:
import React, { Component, useState } from "react";
import useFetch from "../../../Hooks/useFetch";
const Main = () => {
const { data, isPending, error } = useFetch(
"http://127.0.0.1:8000/api/historic/1"
);
function formatData(data){
//Do some processing of the data after it's been received
}
//This doesn't work of course because it runs before the data has been received
const formatted_data=formatData(data);
return (
//Some display using the formatted data
);
};
export default Main;
This is the custom hook, useFetch, which is used in the above component. I'd prefer to not have to do the formatting in here because the formatting is specifically related to the above component and this custom hook is designed to have more universal utility.
import { useState, useEffect } from "react";
const useFetch = (url) => {
const [data, setData] = useState(null);
const [isPending, setisPending] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortCont = new AbortController();
fetch(url, { signal: abortCont.signal })
.then((res) => {
if (res.ok) {
return res.json();
} else {
throw Error("could not fetch data for that resource");
}
})
.then((data) => {
setData(data);
setisPending(false);
setError(null);
})
.catch((er) => {
if (er.name === "AbortError") {
console.log("fetch aborted");
} else {
setError(er.message);
setisPending(false);
}
});
return () => abortCont.abort();
}, [url]);
return { data, isPending, error };
};
export default useFetch;
You should wrap it with useEffect hook with data as it's deps.
const [formattedData, setFormattedData] = useState();
useEffect(() => {
if (!data) return;
const _formattedData = formatData(data);
setFormattedData(_formattedData);
}, [data]);
I'm fairly new to the context API and react hooks beyond useState and useEffect so please bare with me.
I'm trying to create a custom useGet hook that I can use to GET some data from the backend then store this using the context API, so that if I useGet again elsewhere in the app with the same context, it can first check to see if the data has been retrieved and save some time and resources having to do another GET request. I'm trying to write it to be used generally with various different data and context.
I've got most of it working up until I come to try and dispatch the data to useReducer state and then I get the error:
Hooks can only be called inside the body of a function component.
I know I'm probably breaking the rules of hooks with my call to dispatch, but I don't understand why only one of my calls throws the error, or how to fix it to do what I need. Any help would be greatly appreciated.
commandsContext.js
import React, { useReducer, useContext } from "react";
const CommandsState = React.createContext({});
const CommandsDispatch = React.createContext(null);
function CommandsContextProvider({ children }) {
const [state, dispatch] = useReducer({});
return (
<CommandsState.Provider value={state}>
<CommandsDispatch.Provider value={dispatch}>
{children}
</CommandsDispatch.Provider>
</CommandsState.Provider>
);
}
function useCommandsState() {
const context = useContext(CommandsState);
if (context === undefined) {
throw new Error("Must be within CommandsState.Provider");
}
return context;
}
function useCommandsDispatch() {
const context = useContext(CommandsDispatch);
if (context === undefined) {
throw new Error("Must be within CommandsDispatch.Provider");
}
return context;
}
export { CommandsContextProvider, useCommandsState, useCommandsDispatch };
useGet.js
import { API } from "aws-amplify";
import { useRef, useEffect, useReducer } from "react";
export default function useGet(url, useContextState, useContextDispatch) {
const stateRef = useRef(useContextState);
const dispatchRef = useRef(useContextDispatch);
const initialState = {
status: "idle",
error: null,
data: [],
};
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case "FETCHING":
return { ...initialState, status: "fetching" };
case "FETCHED":
return { ...initialState, status: "fetched", data: action.payload };
case "ERROR":
return { ...initialState, status: "error", error: action.payload };
default:
return state;
}
}, initialState);
useEffect(() => {
if (!url) return;
const getData = async () => {
dispatch({ type: "FETCHING" });
if (stateRef.current[url]) { // < Why doesn't this also cause an error
const data = stateRef.current[url];
dispatch({ type: "FETCHED", payload: data });
} else {
try {
const response = await API.get("talkbackBE", url);
dispatchRef.current({ url: response }); // < This causes the error
dispatch({ type: "FETCHED", payload: response });
} catch (error) {
dispatch({ type: "ERROR", payload: error.message });
}
}
};
getData();
}, [url]);
return state;
}
EDIT --
useCommandsState and useCommandsDispatch are imported to this component where I call useGet passing the down.
import {
useCommandsState,
useCommandsDispatch,
} from "../../contexts/commandsContext.js";
export default function General({ userId }) {
const commands = useGet(
"/commands?userId=" + userId,
useCommandsState,
useCommandsDispatch
);
Why am I only getting an error for the dispatchRef.current, and not the stateRef.current, When they both do exactly the same thing for the state/dispatch of useReducer?
How can I refactor this to solve my problem? To summarise, I need to be able to call useGet in two or more places for each context with the first time it's called the data being stored in the context passed.
Here are various links to things I have been reading, which have helped me to get this far.
How to combine custom hook for data fetching and context?
Updating useReducer 'state' using useEffect
Accessing context from useEffect
https://reactjs.org/warnings/invalid-hook-call-warning.html
I think your problem is because you are using useRef instead of state for storing state. If you useRef for storing state you need to manually tell react to update.
I personally would not use reducer and just stick to the hooks you are familiar with as they fulfill your current requirements. I also think they are the best tools for this simple task and are easier to follow.
Code
useGetFromApi.js
This is a generalized and reusable hook - can be used inside and outside of the context
export const useGetFromApi = (url) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!url) return;
const getData = async () => {
try {
setLoading(true);
setData(await API.get('talkbackBE', url));
} catch ({ message }) {
setError(message);
} finally {
setLoading(false); // always set loading to false
}
};
getData();
}, [url]);
return { data, error, loading };
};
dataProvider.js
export const DataContext = createContext(null);
export const DataProvider = ({ children, url}) => {
const { data, error, loading } = useGetFromApi(url);
return (
<DataContext.Provider value={{ data, error, loading }}>
{children}
</DataContext.Provider>
);
};
useGet.js
Don't need to check if context is undefined - React will let you know
export const useGet = () => useContext(DataContext);
Usage
Most parent wrapping component that needs access to data. This level doesn't have access to the data - only it's children do!
const PageorLayout = ({children}) => (
<DataProvider url="">{children}</DataProvider>
)
A page or component that is nested inside of the context
const NestedPageorComponent = () => {
const {data, error, loading } = useGet();
if(error) return 'error';
if(loading) return 'loading';
return <></>;
}
Hopefully this is helpful!
Note I wrote most of this on Stack in the editor so I was unable to test the code but it should provide a solid example
In my React functional component, I have the following code;
const user = useFetch('api/userinfo', {});
Essentially, this is a custom hook call and internally it has a fetch call to the API and sets the data (below is relevant code inside usefetch);
const [data, setData] = useState(initialData);
//....fetch call
setData(json); // once data is fetched
In my main component, since my grid depends on this data, how do I make the code wait to proceed to the Grid jsx till data is fetched? I was planning to use async..await. But not sure if it is possible to do that here with custom hooks?
With below code, seems like the hooks is getting invoked multiple times for some reason;
export default function useFetch(initialUrl, initialData) {
const [url] = useState(initialUrl);
const [loadingData, setLoadingData] = useState(true);
const [data, setData] = useState(initialData);
useEffect(() => {
setLoadingData(true);
fetch(url)
.then(response => {
if (response.status === 200) {
response.json().then(json => {
setData(json);
setLoadingData(false);
});
})
}, [url]);
return [loadingData, data];
}
A couple options for you:
Use another state variable (ie some boolean) and use that to keep track of whether or not the data comes back from the API. Then conditionally render some 'loading' element
Check to see if the data exists and conditionally render based on its existence.
Here's how you can do it with your custom hook:
// defining useFetch hook
const useFetch = (url) => {
// state to keep track of loading
const [loadingData, setLoadingData] = useState(false);
// state for data itself
const [data, setData] = useState(null);
// effect to fetch data
useEffect(() => {
const fetchData = async () => {
try {
// set data to loading
setLoadingData(true);
// request to load data, you can use fetch API too
const { data } = await axios.get(url);
// set data in state and loading to false
setLoadingData(false);
setData(data);
} catch (error) {
console.log("error", error);
}
};
fetchData();
}, [url]);
// return the data and loading state from this hook
return [loadingData, data];
};
Now, you can use this hook in your component like:
const MyComponent = (props) => {
const [isDataLoading, data] = useFetch('/api/some-url');
// now check if data is loading, if loading then return a loader/spinner
if (isDataLoading || !data) return <p>Data is loading...</p>
// otherwise render your actual component
return (
<div>
<h1>This is my component with data</h1>
</div>
);
}
import React, {useState, useEffect, Component} from 'react';
import {Grid, Paper, TextField} from '#material-ui/core'
import DataManager from './../data_manager/data_manager'
const dataManager = new DataManager();
const Tile = (props)=>{
// Initializing State Variables
const [state, setState] = useState({
data : {}
})
const { status, data, error, isFetching } = useQuery("data",async()=>{
const res = await fetch("localhost:8000");
return res.json()
}
if(status==="success"){
setState({data})
}else{
return(<p>Doing</p>)
}
}
This code results in an infinite loop where the rendering keeps going on in a loop.
I think it is because setState causes useQuery to execute again setting the state again and so on.
Any help is appreciated. I want to store the data I get from useQuery in a state variable.
TIA.
You don't need to, it does that for you.
A great video on React Query is https://www.youtube.com/watch?v=DocXo3gqGdI, where the part part is actually replacing a setup with explicit state handling by simply using useQuery.
Should you really want to set some state then a better way is to do it from the onSuccess callback. See https://react-query.tanstack.com/reference/useQuery.
You might want to use useEffect as for now you fetch on every render:
const Tile = (props) => {
const [state, setState] = useState({
data: {},
});
const { status, data, error, isFetching } = useQuery("data", async () => {
const res = await fetch("localhost:8000");
return res.json();
});
useEffect(() => {
if (status === 'success') {
setState({ data });
}
}, [status, data]);
return status === 'success' ? (
<div>Success and use data</div>
) : (
<div>Loading</div>
);
};
I've created a react function component for the context as follows:
const ItemContext = createContext()
const ItemProvider = (props) => {
const [item, setItem] = useState(null)
const findById = (args = {}) => {
fetch('http://....', { method: 'POST' })
.then((newItem) => {
setItem(newItem)
})
}
let value = {
actions: {
findById
},
state: {
item
}
}
return <ItemContext.Provider value={value}>
{props.children}
</ItemContext.Provider>
}
In this way, I have my context that handles all the API calls and stores the state for that item. (Similar to redux and others)
Then in my child component further down the line that uses the above context...
const smallComponent = () =>{
const {id } = useParams()
const itemContext = useContext(ItemContext)
useEffect(()=>{
itemContext.actions.findById(id)
},[id])
return <div>info here</div>
}
So the component should do an API call on change of id. But I'm getting this error in the console:
React Hook useEffect has a missing dependency: 'itemContext.actions'. Either include it or remove the dependency array react-hooks/exhaustive-deps
If I add it in the dependency array though, I get a never ending loop of API calls on my server. So I'm not sure what to do. Or if I'm going at this the wrong way. Thanks.
=== UPDATE ====
Here is a jsfiddle to try it out: https://jsfiddle.net/zx5t76w2/
(FYI I realized the warning is not in the console as it's not linting)
You could just utilize useCallback for your fetch method, which returns a memoized function:
const findById = useCallback((args = {}) => {
fetch("http://....", { method: "POST" }).then(newItem => {
setItem(newItem);
});
}, []);
...and put it in the useEffect:
...
const { actions, state } = useContext(ItemContext)
useEffect(() => {
actions.findById(id)
}, [id, actions.findById])
...
Working example: https://jsfiddle.net/6r5jx1h7/1/
Your problem is related to useEffect calling your custom hook again and again, because it's a normal function that React is not "saving" throughout the renders.
UPDATE
My initial answer fixed the infinite loop.
Your problem was also related to the way you use the context, as it recreates the domain objects of your context (actions, state, ..) again and again (See caveats in the official documentation).
Here is your example in Kent C. Dodds' wonderful way of splitting up context into state and dispatch, which I can't recommend enough. This will fix your infinite loop and provides a cleaner structure of the context usage. Note that I'm still using useCallback for the fetch function based on my original answer:
Complete Codesandbox https://codesandbox.io/s/fancy-sea-bw70b
App.js
import React, { useEffect, useCallback } from "react";
import "./styles.css";
import { useItemState, ItemProvider, useItemDispatch } from "./item-context";
const SmallComponent = () => {
const id = 5;
const { username } = useItemState();
const dispatch = useItemDispatch();
const fetchUsername = useCallback(async () => {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users/" + id
);
const user = await response.json();
dispatch({ type: "setUsername", usernameUpdated: user.name });
}, [dispatch]);
useEffect(() => {
fetchUsername();
}, [fetchUsername]);
return (
<div>
<h4>Username from fetch:</h4>
<p>{username || "not set"}</p>
</div>
);
};
export default function App() {
return (
<div className="App">
<ItemProvider>
<SmallComponent />
</ItemProvider>
</div>
);
}
item-context.js
import React from "react";
const ItemStateContext = React.createContext();
const ItemDispatchContext = React.createContext();
function itemReducer(state, action) {
switch (action.type) {
case "setUsername": {
return { ...state, username: action.usernameUpdated };
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
function ItemProvider({ children }) {
const [state, dispatch] = React.useReducer(itemReducer, {
username: "initial username"
});
return (
<ItemStateContext.Provider value={state}>
<ItemDispatchContext.Provider value={dispatch}>
{children}
</ItemDispatchContext.Provider>
</ItemStateContext.Provider>
);
}
function useItemState() {
const context = React.useContext(ItemStateContext);
if (context === undefined) {
throw new Error("useItemState must be used within a CountProvider");
}
return context;
}
function useItemDispatch() {
const context = React.useContext(ItemDispatchContext);
if (context === undefined) {
throw new Error("useItemDispatch must be used within a CountProvider");
}
return context;
}
export { ItemProvider, useItemState, useItemDispatch };
Both of these blog posts helped me a lot when I started using context with hooks initially:
https://kentcdodds.com/blog/application-state-management-with-react
https://kentcdodds.com/blog/how-to-use-react-context-effectively
OK, I didn't want to write an answer as Bennett basically gave you the fix, but I think it is missing the part in the component, so here you go:
const ItemProvider = ({ children }) => {
const [item, setItem] = useState(null)
const findById = useCallback((args = {}) => {
fetch('http://....', { method: 'POST' }).then((newItem) => setItem(newItem))
}, []);
return (
<ItemContext.Provider value={{ actions: { findById }, state: { item } }}>
{children}
</ItemContext.Provider>
)
}
const smallComponent = () => {
const { id } = useParams()
const { actions } = useContext(ItemContext)
useEffect(() => {
itemContext.actions.findById(id)
}, [actions.findById, id])
return <div>info here</div>
}
Extended from the comments, here's the working JSFiddle