Preface: I'm fairly new to React (Coming over from Angular). I know things a similar but different.
I have referenced the following SO threads to no avail in my situation:
React not displaying data after successful fetch
Objects are not valid as a React child. If you meant to render a collection of children, use an array instead
Currently, I'm trying to get my data to display from an API I developed. I'm used to the Angular approach which would call for a ngFor in the template for most data showcase situations.
I'm having trouble wrapping my mind around what I have to do here in order to display my data. The data is expected to be an array of objects which I would then parse to display.
I also receive the following error: Error: Objects are not valid as a React child (found: object with keys {data}). If you meant to render a collection of children, use an array instead.
I've searched high and low for a solution but sadly, nothing I've seen has worked for me. (All of the answers on SO are using the class-based version of React, of which I am not).
You can see my data output in the following screenshot:
I am also including my custom hook code and the component that is supposed to render the data:
CUSTOM DATA FETCH HOOK
interface Drone{
id: number;
name: string;
model: string;
price: number;
}
export function useGetData(urlpath:string) {
const [droneData, setData] = useState<any>()
async function handleDataFetch(path:string){
const result = await fetch(`https://drone-collections-api-jc.herokuapp.com${path}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'x-access-token': 'Bearer API-TOKEN'
}
})
const response = await result.json();
setData(response)
}
useEffect( () => {
handleDataFetch(urlpath)
})
return droneData
}
THE DRONE COMPONENT
import { useGetData } from '../../custom-hooks'
export const Drones = () => {
let data = useGetData('/drones')
console.log(data)
// const DisplayDrone = ( ) => {
// return (
// Array.prototype.map( data => {
// <div>{ data.name }</div>
// })
// )
// }
return (
<div>
<h1>Hello Drones</h1>
</div>
)
}
Also, for more context, the current code can be found at this repo: https://github.com/carter3689/testing-drone-frontend
Please, help me understand what I'm missing. Many Thanks!
There are several locations that needed to be fixed
In fetchData.tsx
export function useGetData(urlpath: string) {
const [droneData, setData] = useState<any>([]);
async function handleDataFetch(path: string) {
const result = await fetch(`https://jsonplaceholder.typicode.com/posts`, {
...
});
const response = await result.json();
setData(response);
}
useEffect(() => {
handleDataFetch(urlpath);
}, []);
Explanation:
you need a "blank" array for looping through. I guess that the error causes by the fact that at the start, before the data is fetched, there is nothing to loop through. It's same as doing undefined.map(), which is obviously fail.
You need a dependencies array for useEffect. Right now your code will do an infinite loop since everytime it get data, it update the state, thus re-run the useEffect and repeat. Add dependencies array limit when that useEffect will run
In Drones.tsx
return (
<div>
{data.map(item => <div>{item.name}</div>}
</div>
)
Not much to say here. I don't use Angular so I'm not sure why you use Array.prototype.map, but in React you can loop through your variable directly. I also have a CodeSandbox link for your project (I use public API)
Related
So at the moment I am having to put my request / api logic directly into my components because what I need to do a lot of the time is set state based on the response I get from the back end.
Below is a function that I have on my settings page that I use to save the settings to recoil after the user hits save on the form:
const setUserConfig = useSetRecoilState(userAtoms.userConfig);
const submitSettings = async (values: UserConfigInterface) => {
try {
const { data: {data} } = await updateUser(values);
setUserConfig({
...data
});
} catch (error) {
console.log('settings form error: ', error);
}
}
This works perfectly...I just dont want the function in my component as most of my components are getting way bigger than they need to be.
I have tried making a separate file to do this but I can only use the recoil hooks (in this instance useSetRecoilState) inside of components and it just complains when I try and do this outside of a react component.
I have tried implementing this with recoils selector and selectorFamily functions but it gets kind of complicated. Here is how I have tried it inside of a file that has atoms / selectors only:
export const languageProgress = atom<LanguageProgress>({
key: "LanguageProgress",
default: {
level: 1,
xp: 0,
max_xp: 0
}
})
export const languageProgressUpdate = selectorFamily<LanguageProgress>({
key: "LanguageProgress",
get: () => async () => {
try {
const { data: { data } } = await getLanguageProgress();
return data;
} catch (error) {
console.log('get language progress error');
}
},
set: (params:object) => async ({set}) => {
try {
const { data: { data } } = await updateLanguageProgress(params);
set(languageProgress, {
level: data.level,
xp: data.xp,
max_xp: data.max_xp
});
} catch (error) {
console.log('language progress update error: ', error);
}
}
});
What I want to do here is get the values I need from the back end and display it in the front which I can do in the selector function get but now I have 2 points of truth for this...my languageProgress atom will initially be incorrect as its not getting anything from the database so I have to use useGetRevoilValue on the languageProgressUpdate selector I have made but then when I want to update I am updating the atom and not the actual value.
I cannot find a good example anywhere that does what I am trying to here (very suprisingly as I would have thought it is quite a common way to do things...get data from back end and set it in state.) and I can't figure out a way to do it without doing it in the component (as in the first example). Ideally I would like something like the first example but outside of a component because that solution is super simple and works for me.
So I dont know if this is the best answer but it does work and ultimately what I wanted to do was seperate the logic from the screen component.
The answer in my situation is a bit long winded but this is what I used to solve the problem: https://medium.com/geekculture/crud-with-recoiljs-and-remote-api-e36581b77168
Essentially the answer is to put all the logic into a hook and get state from the api and set it there.
get data from back end and set it in state
You may be looking for useRecoilValueLoadable:
"This hook is intended to be used for reading the value of asynchronous selectors. This hook will subscribe the component to the given state."
Here's a quick demonstration of how I've previously used it. To quickly summarise: you pass useRecoilValueLoadable a selector (that you've defined somewhere outside the logic of the component), that selector grabs the data from your API, and that all gets fed back via useRecoilValueLoadable as an array of 1) the current state of the value returned, and 2) the content of that API call.
Note: in this example I'm passing an array of values to the selector each of which makes a separate API call.
App.js
const { state, contents } = useRecoilValueLoadable(myQuery(arr));
if (state.hasValue && contents.length) {
// `map` over the contents
}
selector.js
import { selectorFamily } from 'recoil';
export const myQuery = selectorFamily({
key: 'myQuery',
get: arr => async () => {
const promises = arr.map(async item => {
try {
const response = await fetch(`/endpoint/${item.id}`);
if (response.ok) return response.json();
throw Error('API request not fulfilled');
} catch (err) {
console.log(err);
}
});
const items = await Promise.all(promises);
return items;
}
});
Many articles writing about how to return pending promise and work with React suspense but it's not working in real world.
They don't consider if the component got visited second time, and it won't refetch the data from the server.
e.g. => https://dev.to/darkmavis1980/a-practical-example-of-suspense-in-react-18-3lln?signin=true
The below example would only work for the first time we visit the component but not re-fetch data for the following times.
Any idea to let it work to prevent not doing re-fetching?
Component
const dataFetchWithWrapPromise = (url) => {
return wrapPromise(window.fetch(url, {
}));
}
const resource = dataFetchWithWrapPromise('http://localhost:3000/data');
function Articles() {
const data = resource.read();
React.useEffect(() => {
return () => {
resource.reset();
}
}, []);
return (
<>
<h1>Data</h1>
<pre>
{JSON.stringify(data, null, 4)}
</pre>
</>
);
}
export default Articles;
function wrapPromise(promise) {
let status = 'pending';
let response;
const suspender = promise.then(
async res => {
status = 'success';
response = await res.json();
},
err => {
status = 'error';
response = err;
},
);
const handler = {
pending: () => {
throw suspender;
},
error: () => {
throw response;
},
success: () => {
console.log(response)
return response
},
default: () => {
throw suspender;
},
};
const read = () => {
const result = handler[status] ? handler[status]() : handler.default();
return result;
};
const reset = () => {
if(status!=='pending') {
status = 'pending';
response = undefined;
}
}
return { read, reset };
}
export default wrapPromise;
Ok, so I think I got you covered. It so happens that I liked <Suspense> ever since I heard of it. I stumbled with it in my learning of asynchronous JavaScript because I was coding wj-config. This preface is just to let you know that I'm no React master, but it so happens that I ended up creating a React example for wj-config v2.0.0, which is currently in BETA 2. This example does what you want.
So no more chit-chat. The code of interest is here.
It is a table component that loads person data from Mockaroo. The web page (parent) has two controls to specify the number of rows wanted as well as the minimum birth date wanted. Whenever the value of any of those controls change, the person data is re-fetched. The table itself uses <Suspense> in two places.
The component module starts by defining the fetching functions needed for person and country data. Then it declares some variables that are captured in scopes later on. The starting promise is required for the first render. Its resolver is exposed through startingResolver, and the starting promise is wrapped as per the <Suspense> mechanics that you clearly know.
Focus your attention now to the PersonsTable function. It sets up a useEffect call to re-trigger the data fetching operations based on changes of props. As I'm not a super master in ReactJS, maybe there's a better way than props. I just know props will trigger the effect automatically, so I used them.
On the first render, the starting promise is thrown, but it will never be fulfilled since it is a bogus promise. The code inside useEffect makes this promise resolve at the same time the fetching promise resolves. Then, using the fetching promise, the readPersons function is defined.
NOTE: I'm not a native English speaker. Pardon my horrible "persons" instead of "people" mistake. :-( I'll correct whenever I have time.
Anyway, with this set up, you'll have completed your goal. The linked code sample goes beyond this by having an inner <Suspense> that waits for country data, but I suppose an explanation is not needed since I believe the question is now covered.
Hope this helps!
For the initial render, I have object date, which is an empty array. I then try to get data from an influxDB, but the get result isn't reflected by React with a re-render. The get function is calling in useEffect (you can see this in screenshots). I use typescript, and to avoid getting an error on the initial load (that data is an empty array of objects and it doesn't have a value property) I use the typescript syntax, but it still doesn't display the correct value. It doesn't display anything at all.
What could be the problem? In the last photo, we can see another way to display data without a question mark from typescript, but it doesn't work correctly either, even if the length of the array is greater than 0, it still doesn't display data[0].value.
Initial data:
Data after DB get:
Get the first element in array:
Browser result (before ':' we should see data[0].value):
Alternate way (when data isn't empty we should see the value of the first object in array):
I also show we code
import React from 'react';
import './App.css';
import { FluxTableMetaData } from '#influxdata/influxdb-client';
const { InfluxDB } = require('#influxdata/influxdb-client');
export const App = () => {
debugger
const authData = {
token: 'Emyw1rqUDthYRLpmmBc6O1_yt9rGTT57O50zoKiXUoScAjL6G-MgUN6G_U9THilr86BfIPHMYt6_KSDNHhc9Jg==',
org: 'testOrg',
bucket: 'test-bucket',
};
const client = new InfluxDB({
url: 'http://localhost:8086',
token: authData.token,
});
const queryApi = client.getQueryApi(authData.org);
const query = `from(bucket: "${authData.bucket}") |> range(start: -1d)`;
const data: any[] = [];
React.useEffect(() => {
queryApi.queryRows(query, {
next(row: string[], tableMeta: FluxTableMetaData) {
debugger;
const o = tableMeta.toObject(row);
const item = {
time: o._time,
measurement: o._measurement,
field: o._field,
value: o._value,
};
return data.push(item);
},
error(error: Error) {
return error;
},
complete() {
console.log(data)
return data;
},
})
},[]);
debugger;
return (
<div>
<div>{data.length !== data[0].value}:</div>
<div>hello</div>
</div>
);
};
another way:
<div>
<div>{data[0]?.value}:</div>
<div>hello</div>
</div>
The main issue in your code is, You have defined data as a const variable, and not as a state. Thus, in useEffect, even if your data gets changed, it will not reflect on data[0].value as it is a const variable and react doesn't render updated values of variables. It updates/renders only if it's a state.
In short, Convert your const data to be a stateand use setState like below for your code to work!
const [data, setData] = React.useState([]);
...
setData([...data , item]);
I suggest you use the React States for that in the following way
var [nameOfVariableWhichWillChange, changeFunction] = React.useState("");
now whenever whichever function wants to change the value of that function just use changeFunction(newValueOfVariable)
the plus point of using React state is wherever you might have used that variable on change of That variable each instance will change on its own...
Do let me know does that solve your problem, or you need something else
React doesn't re-render the webpage even if the data has changed. You need to store your data inside a React.useState and call setState to trigger a re-render.
const [data, setData] = useState([])
React.useEffect(() => {
...
next(row: string[], tableMeta: FluxTableMetaData) {
...
setData([...data, item])
},
...
Read about useState here for more information: https://reactjs.org/docs/hooks-state.html
We have written a custom data fetching hook useInternalApi which is similar to the useDataApi hook at the very bottom of this fairly decent tutorial on data fetching with react hooks. Our app fetches a lot of sports data, and in particular, we are trying to figure out the right data-fetching pattern for our use case, which is fairly simple:
Fetch general info for a specific entity (an NCAA conference, for example)
Use info returned with that entity (an array of team IDs for teams in the specific conference), and fetch info on each team in the array.
For this, our code would then look something like this:
import `useInternalApi` from '../path-to-hooks/useInternalApi';
// import React... and other stuff
function ComponentThatWantsTeamInfo({ conferenceId }) {
// use data fetching hook
const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })
// once conferenceInfo loads, then load info from all teams in the conference
if (conferenceInfo && conferenceInfo.teamsArray) {
const [teamInfos, isLoading2, isError2] = useInternalApi('teamInfo', { teamIds: conferenceInfo.teamIds })
}
}
In the example above, conferenceId is an integer, teamIds is an array of integers, and the combination of the 2 parameters to the useInternalApi function create a unique endpoint url to fetch data from. The two main problems with this currently are:
Our useInternalApi hook is called in an if statement, which is not allowed per #1 rule of hooks.
useInternalApi is currently built to only make a single fetch, to a specific endpoint. Currently, it cannot handle an array of teamIds like above.
What is the correct data-fetching pattern for this? Ideally, teamInfos would be an object where each key is the teamId for one of the teams in the conference. In particular, is it better to:
Create a new internal hook that can handle an array of teamIds, will make the 10 - 20 fetches (or as many as needed based on the length of the teamsArray), and will use Promise.all() to return the results all-together.
Keep the useInternalApi hook as is, and simply call it 10 - 20 times, once for each team.
Edit
I'm not sure if the underlying code to useInternalApi is needed to answer this question. I try to avoid creating very long posts, but in this instance perhaps that code is important:
const useInternalApi = (endpoint, config) => {
// Set Data-Fetching State
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
// Use in lieu of useEffect
useDeepCompareEffect(() => {
// Token/Source should be created before "fetchData"
let source = axios.CancelToken.source();
let isMounted = true;
// Create Function that makes Axios requests
const fetchData = async () => {
// Set States + Try To Fetch
setIsError(false);
setIsLoading(true);
try {
const url = createUrl(endpoint, config);
const result = await axios.get(url, { cancelToken: source.token });
if (isMounted) {
setData(result.data);
}
} catch (error) {
if (isMounted) {
setIsError(true);
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
};
// Call Function
fetchData();
// Cancel Request / Prevent State Updates (Memory Leaks) in cleanup function
return () => {
isMounted = false; // set to false to prevent state updates / memory leaks
source.cancel(); // and cancel the http request as well because why not
};
}, [endpoint, config]);
// Return as length-3 array
return [data, isLoading, isError];
};
In my opinion, if you need to use a hook conditionally, you should use that hook inside of a separate component and then conditionally render that component.
My understanding, correct me if I'm wrong, is that the initial API call returns an array of ids and you need to fetch the data for each team based on that id?
Here is how I'd do something of that sorts.
import `useInternalApi` from '../path-to-hooks/useInternalApi';
// import React... and other stuff
function ComponentThatDisplaysASpecificTeam(props){
const teamId = props.teamId;
const [teamInfo] = useInternalApi('teamInfo', { teamId });
if(! teamInfo){
return <p>Loading...</p>
}
return <p>do something with teamInfo...</p>
}
function ComponentThatWantsTeamInfo({ conferenceId }) {
// use data fetching hook
const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })
if (! conferenceInfo || ! conferenceInfo.teamsArray) {
return <p>this is either a loading or an error, you probably know better than me.</p>
}
// Let the data for each team be handled by its own component. This also lets you not have to use Promise.all
return (
<div>
{conferenceInfo.teamIds.map(teamId => (
<ComponentThatDisplaysASpecificTeam teamId={teamId} />
))}
</div>
)
}
I'm really stuck in trying to render some data being passed down as props. I'll include some code and definitions below, but if you feel that I need to include some further code snippets, please let me know (I'm really struggling to find what's causing the error, so I may have missed out the causal issue!).
I first take data from an API which is then used to populate a UserList component via useState (setUsers(data):
useEffect(() => {
async function getUserList() {
setLoading(true);
try {
const url =
"API URL";
const response = await fetch(url);
const data = await response.json();
setUsers(data);
} catch (error) {
throw new Error("User list unavailable");
}
setLoading(false);
}
getUserList();
}, []);
If a user is clicked in the UserList, this changes the selectedUser state of the parent Home component to be the specific user's unique_ID via:
onClick={() => setSelectedUser(unique_ID)}
If the selectedUser changes, the Home component also does a more updated data fetch from the API to get all information relevant to the specific user via their unique_ID:
useEffect(() => {
async function getSelectedUserData() {
try {
const url = `API URL/${selectedUser}`;
const response = await fetch(url);
const data = await response.json();
setSelectedUserData(data);
} catch (error) {
throw new Error("User data unavailable");
}
}
getSelectedUserData();
}, [selectedUser]);
The specific user data is then passed down as props to a child UserInformation component:
<UserInformation selectedUser={selectedUser} selectedUserData={selectedUserData} />
At this point, I can see all the data being passed around correctly in the browser React Developer Tools.
The UserInformation component then gets the data passed via props:
import React, { useEffect, useState } from "react";
function UserInformation({ selectedUser, selectedUserData }) {
const [currentUser, setCurrentUser] = useState({ selectedUserData });
useEffect(() => {
setCurrentUser({ selectedUserData });
}, [selectedUser, selectedUserData]);
return (
<div>
<p>{selectedUserData.User_Firstname}</p>
<p>{currentUser.User_Firstname}</p>
</div>
);
}
export default UserInformation;
And here is where I get stuck - I can't seem to render any of the data I pass down as props to the UserInformation component, even though I've tried a few different methods (hence the <p>{selectedUserData.User_Firstname}</p> and <p>{currentUser.User_Firstname}</p> to demonstrate).
I'd really appreciate any help you can give me with this - I must be making an error somewhere!
Thanks so much, and sorry for the super long post!
I managed to solve this (thanks to the help of Mohamed and Antonio above, as well as the reactiflux community).
import React from "react";
function UserInformation({ selectedUserData }) {
const currentUserRender = selectedUserData.map(
({ User_Firstname, User_Lastname }) => (
<div key={unique_ID}>
<p>{User_Firstname}</p>
</div>
)
);
return (
<div>
{selectedUserData ? currentUserRender : null}
</div>
);
}
export default UserInformation;
As selectedUserData was returning an array instead of an object, I needed to map the data rather than call it with an object method such as {selectedUserData.User_Firstname}.
const currentUserRender = selectedUserData.map(
({ User_Firstname, User_Lastname }) => (
<div key={unique_ID}>
<p>{User_Firstname}</p>
</div>
)
);
The above snippet maps the selected data properties found inside selectedUserData ({ User_Firstname, User_Lastname }), with the whole map being called in the return via {selectedUserData ? currentUserRender : null}.
Hopefully my explanation of the above solution is clear for anyone reading, and a big thanks again to Mohamed and Antonio (as well as a few others in the reactiflux Discord community) for helping!
You're trying to set the current user to an object with key "selectedUserData".
So if you want to access it you've to access it by this key name so change this line currentUser.User_Firstname to currentUser.selectedUserData.User_Firstname