React Query clean response function - javascript

I have a helper function to clean up the response from a useQuery call to an api.
Here is the function:
export const cleanResponse = function (data) {
let x = [];
let y = [];
data.forEach((item) => {
x.push(item.title);
y.push(item.price);
});
return { titles: x, prices: y };
};
In my main components I'm using useQuery to get the data then applying the above function to clean it up and prepare it for plotting:
const {
data: products,
isLoading,
isError,
} = useQuery(['products'], () => {
return axios('https://dummyjson.com/products/').then((res) => res.data);
});
const cleanData = cleanResponse(products.products);
const titles = cleanData?.titles;
const prices = cleanData?.prices;
Then I'm rendering a simple bar chart passing the data as props:
<BarChart titles={titles} prices={prices} />
I'm getting the following error:
Uncaught TypeError: Cannot read properties of undefined (reading 'products')
What am I missing here? Should my cleanResponse function be async because it's accepting data from an api?

The error is due to initial state you can do as below to prevent error.
cleanResponse(products?.products || [])

while the query is still running, you component renders with data:undefined.
one way to solve this is to check for undefined before you do your transformation. Other approaches are doing the data transformation at a different time, e.g. in the query function after fetching:
const {
data,
isLoading,
isError,
} = useQuery(['products'], () => {
return axios('https://dummyjson.com/products/')
.then((res) => res.data)
.then((data) => cleanResponse(data.products))
});
That way, the cleaned data will be stored in the cache already and data returned from useQuery will be the cleaned data.
Other approaches include using the select option. You can read more about that in my blog post about react-query data transformations.

Related

Apollo-client useLazyQuery returning undefined on refetch

I have a subcomponent Viewer that uses a refetch function passed down to its parent Homescreen.
The lazyQuery in homescreen is structured as follows:
const [getById, {loading, error, data, refetch}] = useLazyQuery(GET_BY_ID);
This will get an object from my mongoDB by its id, and when I need to call it again and reload data into my custom activeObject variable, I use the follow function:
const refetchObjects= async () => {
const {loading, error, data } = await refetch();
if (error) { console.log(error);}
if (data) {
activeObject = data.getRegionById;
}
}
However, sometimes the return object of await refetch(); is undefined and I'm not sure why.

Setting state on array logs empty array multiple times

I am trying to fetch data from a backend server and hold it in an array. After I have done this I want to pass the array to another
component. Although, when I try and populate the array and pass it to my component, I get multiple empty arrays passed rather than an array with data.
I first initialise the state of the array using useState()
const [data, setData] = useState([]);
I then have a function that fetches data from the backend and attempts to populate data.
useEffect(() => {
const fetchData = () => {
fetch('/data')
.then((res) => res.json())
.then((data) => {
for (const property in data) {
setDailyCases([...dailyCases].push(`${data[property]}`));
}
});
}
fetchData();
},[])
When I pass this data to another component: <DataComp data={data}, I don't get the data I was expecting.
When I console.log(props.data) this is the output:
Which is strange beacuse If I console.log() while running the data loop all the data is visible:
How can I make sure the data array is updating correctly, and when passed I get one array of all the data?
Here is the DataComp component:
const DataComp = (props) => {
console.log(props.cases)
return (
<h1>Testing</h1>
)
}
export default DataComp
Using #Fardeen Panjwani answer my component is getting the correct data, although I am now getting more outputs to the console that expected?
You're never calling setData with the fetched data as parameter.
useEffect(() => {
const fetchData = () => {
fetch('/data')
.then((res) => res.json())
.then((_data) => { // using "_data" in order to avoid clash with the state-hook
setData(_data) // <= this line is responsible for populating the "data" value.
for (const property in _data) {
setDailyCases([...dailyCases].push(`${_data[property]}`));
}
});
}
fetchData();
},[])
In order to update data's value, you need to call the setData method.

React hooks: what is best practice for wrapping multiple instances of a hook with a single hook?

I have a react hook useDbReadTable for reading data from a database that accepts initial data of tablename and query. It returns an object that includes an isLoading status in addition to the data from the database.
I want to wrap this hook in a new hook that accepts initial data of an array of { tablename, query }, and returns an object with the data from the database for each table, but with the isLoading statuses consolidated into a single boolean based on logic in my new hook.
The idea is, the caller of the new hook can ask for data from a number of tables, but only has to check one status value.
My thought was to have the new hook look something like,
EDIT: Updated code (I had pasted the wrong version)
export const useDbRead = tableReads => {
let myState = {};
for (let i = 0; i < tableReads.length; ++i) {
const { tablename, query = {} } = tableReads[i];
const [{ isLoading, isDbError, dbError, data }] = useDbReadTable(tablename, query);
myState = { ...myState, [tablename]: { isLoading, isDbError, dbError, data }};
}
const finalState = {
...myState,
isLoading: Object.values(myState).reduce((acc, t) => acc || t.isLoading, false),
};
return [finalState];
};
However, eslint gives me this error on my useDbReadTable call:
React Hook "useDbReadTable" may be executed more than once. Possibly because it is called in a loop. React Hooks must be called in the exact same order in every component render. react-hooks/rules-of-hooks
And Rules for Hooks says,
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls. (If you’re curious, we’ll explain this in depth below.)
After reading the rule and the explanation, it seems the only issue is making sure the hooks are called in the same order on all re-renders. As long as I ensure the list of tables I pass in to my new hook never changes, shouldn't my new hook work fine (as my initial tests indicate)? Or am I missing something?
More importantly, is there a better idea how to implement this, that doesn't violate the Rules of Hooks?
Edit2: in case its helpful, here's useDbReadTable. Note that it includes more functionality than I mention in my question, since I wanted to keep the question as simple as possible. My question is whether my useDbRead is a good solution, or is there a good way to do it without violating the Rules of Hooks?
export const useDbReadTable = (initialTableName, initialQuery = {}, initialData = []) => {
const dbChangeFlag = useSelector(({appState}) => appState.dbChangeFlag);
const [tableName, setTableName] = useState(initialTableName);
const [query, setQuery] = useState(initialQuery);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isDbError: false,
dbError: {},
data: initialData,
});
useEffect(() => {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: dataFetch.FETCH_INIT });
try {
const result = Array.isArray(query) ?
await db[tableName].batchGet(query) // query is an array of Ids
:
await db[tableName].find(query);
if (!didCancel) {
dispatch({ type: dataFetch.FETCH_SUCCESS, payload: result });
}
} catch (error) {
if (!didCancel) {
dispatch({ type: dataFetch.FETCH_FAILURE, payload: error });
}
}
};
fetchData().then(); // .then() gets rid of eslint warning
return () => {
didCancel = true;
};
}, [query, tableName, dbChangeFlag]);
return [state, setQuery, setTableName];
};
You can probably avoid using the useDbReadSingle by making useDbRead itself array aware. Something like:
export const useDbRead = tableReads => {
const [loading, setLoading] = useState(true);
useEffect(() => {
const doIt = async () => {
// you would also need to handle the error case, but you get the idea
const data = await Promise.all(
tableReads.map(tr => {
return mydbfn(tr);
})
);
setLoading(false);
};
doIt();
}, [tableReads]);
return { loading, data };
};
When you need to use it for single table read, just call this with a array that has single element.
const {loading, data: [d]} = useDbRead([mytableread])

Using a read-once variable loaded async through react-hook

I'm currently playing around with Reacts hooks but currently I'm stuck at mixing different use-cases.
The following scenario is what I am trying to get working. There should be one hook called useNationsAsync which is retrieving a list of available nations from the server.
Inside the hook I check if the list has already been loaded/stored to the localStorage in order to load it only once.
For the remote-call I use axios' get call together with the await keyword. So this "hook" has to be async. I've implemented it the following:
export async function getNationsAsync({ }: IUseNationsProps = {}): Promise<NationResource[]> {
const [storedNations, setStoredNations] = useLocalStorage<NationResource[]>("nations", null);
if (storedNations == null) {
const nationsResponse = await Axios.get<NationsGetResponse>("/v1/nations/");
setStoredNations(nationsResponse.data.nations);
}
return storedNations;
}
The useLocalStorage-hook is the one which can be found here (only typed for use with TypeScript).
In my final FunctionalComponent I only want to read the nations once so I thought using the useEffect hook with an empty array would be the place to be (as this is mainly the same as componentDidMount).
However, on runtime I get the following error on the first line of my getNationsAsync-hook:
Uncaught (in promise) Invariant Violation: Invalid hook call.
The usage in my FunctionalComponent is:
const [nations, setNations] = React.useState<NationResource[]>([]);
const fetchNations = async () => {
const loadedNations = await getNationsAsync();
setNations(loadedNations);
};
React.useEffect(() => {
fetchNations();
}, []);
I know that the issue is for calling useHook inside the method passed to useEffect which is forbidden.
The problem is, that I don't get the right concept on how to use the nations at a central point (a hook sharing the result, not the logic) but only load them once in the components which do need nations.
The hook you are creating manages the state of nations and returns it.
Instead of useState you are using useLocalStorage which, as far as I could read from the source, uses as initial state a localStorage value or the given value (null in your case) if there is no local one.
export const useNations = ():
| NationResource[]
| null => {
const [storedNations, setStoredNations] = useLocalStorage<
NationResource[] | null
>("nations", null);
useEffect(() => {
// If storedNations has a value don't continue.
if (storedNations) {
return;
}
const fetch = async () => {
// Check the types here, Im not sure what NationsGetResponse has.
const nationsResponse = await Axios.get<NationsGetResponse>(
"/v1/nations/"
);
setStoredNations(nationsResponse.data.nations);
};
fetch();
}, []);
return storedNations;
};
Then you can use this hook in any component:
export const MyComponent: React.ComponentType<Props> = props => {
const nations = useNations();
return // etc.
};
You can add async to custom hooks. I think this is what you're looking for.
export function getNations(props) {
const [storedNations, setStoredNations] = useLocalStorage("nations",[]);
async function getNations() {
const nationsResponse = await Axios.get("/v1/nations/");
setStoredNations(nationsResponse.data.nations);
}
useEffect(() => {
if (storedNations.length === 0) {
getNations()
}
}, [])
return storedNations;
}
You can call it like this
function App() {
const nations = getNations()
return (
// jsx here
)
}

Prevent Redux-Thunk from re-requesting data that already is in store

I'm trying to use redux thunk to make asynchronous actions and it is updating my store the way I want it to. I have data for my charts within this.props by using:
const mapStateToProps = (state, ownProps) => {
const key = state.key ? state.key : {}
const data = state.data[key] ? state.data[key] : {}
return {
data,
key
}
}
export default connect(mapStateToProps)(LineChart)
Where data is an object within the store and each time I make an XHR call to get another piece of data it goes into the store.
This is the the async and the sync action
export function addData (key, data, payload) {
return {
type: ADD_DATA,
key,
data,
payload
}
}
export function getData (key, payload) {
return function (dispatch, getState) {
services.getData(payload)
.then(response => {
dispatch(addData(key, response.data, payload))
})
.catch(error => {
console.error('axios error', error)
})
}
}
And the reducer:
const addData = (state, action) => {
const key = action.key
const data = action.data.results
const payload = action.payload
return {
...state,
payload,
key,
data: {
...state.data,
[key]: data
}
}
}
In the tutorial I have been following along with (code on github), this seems like enough that when a piece of data that already exists within the store, at say like, data['some-key'] redux will not request the data again. I'm not entirely sure on how it's prevented but in the course, it is. I however am definitely making network calls again for keys that already exist in my store
What is the way to prevent XHR for data that already exists in my store?
Redux itself does nothing about requesting data, or only requesting it if it's not cached. However, you can write logic for that. Here's a fake-ish example of what a typical "only request data if not cached" thunk might look like:
function fetchDataIfNeeded(someId) {
return (dispatch, getState) => {
const state = getState();
const items = selectItems(state);
if(!dataExists(items, someId)) {
fetchData(someId)
.then(response => {
dispatch(loadData(response.data));
});
}
}
}
I also have a gist with examples of common thunk patterns that you might find useful.

Categories