Returning a component from a map function - javascript

I want to create a function that maps throw a firestore collection and return a Component for each document in the collection I have tried to use the code below
<div className="posts">
{
db.collection("posts")
.get()
.then((snapshot) => {
snapshot.docs.map((doc)=>(
<PostContect img={doc.data().image} Admin={doc.data().admin} Date={"January 14, 2019"} CommentsNo={"2"} Title={doc.data().title} Body={doc.data().title} />
))})}
</div>
but this error shows:
Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.ode

JSX is not the place to make Ajax calls. Instead make the call inside componentDidMount/useEffect and set the data received inside state. As soon as state is updated with received data, React will automatically re-render the component and display the expected content. Try something as follows:
const [snapshots, setSnapshots] = useState();
useEffect(()=> {
db.collection("posts")
.get()
.then((snapshot) => {
setSnapshots(snapshot.docs)
}
)
}, []);
render
<div className="posts">
{snapshots && snapshots.map((doc)=>(
<PostContect img={doc.data().image} Admin={doc.data().admin} Date={"January 14, 2019"} CommentsNo={"2"} Title={doc.data().title} Body={doc.data().title} />
)
}
</div>

Since you haven't given a hint as to whether you're using hooks or not, here is how you should deal with asynchronous stuff and setting states in React (using hooks) :-
function MyComponent () {
const [snapshot,setSnapshot] = useState();
useEffect(()=>{
async function getPosts(){
let snapshot = await db.collection("posts").get();
setSnapshot(snapshot);
}
getPosts();
},[])
return
(<div className="posts">
snapshot?.docs?.map((doc)=>(
<PostContect img={doc.data().image} Admin={doc.data().admin} Date={"January 14, 2019"} CommentsNo={"2"} Title={doc.data().title} Body={doc.data().title} />)
</div>)
}
What you are doing is returning Promise object as the error states. That async stuff is taken care either inside a useEffect,event handler or a custom-hook. There could be more ways but this is a general point to start.

As the error message states Promise is not a valid React child. To solve that issue you can introduce a state which keeps the result of db.collection('posts').get() method. Then iterating through on that array with your .map() which will be a valid React child.
See a possible solution for your scenario:
// in the root of you component, introduce a state
const [posts, setPosts] = useState([])
// here in useEffect you can do your API call and update the state
useEffect(() => {
db.collection("posts")
.get()
.then((snapshot) => {
setPosts(snapshot.docs)
})
}, [])
// ... rest of your component's code
return (
<div className="posts">
{
posts && posts.map((doc, i) => (
<div key={i}>
doc.data().title
</div>
// here you can use <PostContect /> component
// either doc.data().title or doc.title to get the value
))
}
</div>
)
Suggested reads:
Using the Effect Hook
Using the State Hook

The code you wrote returns a promise, which can't be rendered by react. You can use conditional rendering to return something else while the data is being fetched.
Check this question for more info

Related

In React, how can I make a single api call, set the result to a state variable, and display it?

I am programming a react application where I need to make a single async api call, save the result to a state variable and display the result. I am using an axios instance where the response of the api call is a nested object. For example:
{
kanji:
{character: "", stroke:""},
quote: "string"
}
So far I have the following code where I am able to console.log the homeData object successfully. However I get the error: 'TypeError: homeData is undefined'.
const Home = () =>{
const [homeData, setHomeData] = useState({})
const getHomeData = async()=>{
instance.get("/data")
.then(res =>{
setHomeData(res.data)
})
.catch(error =>{
console.log(error)
})
}
useEffect(()=>{
getHomeData()
}, [])
console.log(homeData)
return(
<>
<p>{homeData}<p>
</>
)
}
This seems like a promise issue but I am not sure how to fix it. Thank you for your help in advance.
This is not a promise issue, it has to due with the order in which the lifecycle methods run. useEffect runs after the component renders. This means when the component first renders, the homeData is an empty object. homeData will be present in the second render. The following code first checks if homeData exists, then if it exists, it displays the kanji character. Note also, you cant just display a raw object into the dom so you will have to access the homeData by property for it to display
const Home = () =>{
const [homeData, setHomeData] = useState({})
const getHomeData = async()=>{
instance.get("/data")
.then(res =>{
setHomeData(res.data)
})
.catch(error =>{
console.log(error)
})
}
useEffect(()=>{
getHomeData()
}, [])
console.log(homeData)
return(
<>
<p>{homeData && homeData.kanji.character}<p>
</>
)
}

Saving api response to State using useState and Axios (React JS)

I'm having an issue when trying to save to State an axios API call. I've tried
useState set method not reflecting change immediately 's answer and many other and I can't get the state saved. This is not a duplicate, because I've tried what the accepted answer is and the one below and it still doesn't work.
Here's the (rather simple) component. Any help will be appreciated
export const Home = () => {
const [widgets, setWidgets] = useState([]);
useEffect(() => {
axios
.get('/call-to-api')
.then((response) => {
const data = response.data;
console.log(data); // returns correctly filled array
setWidgets(widgets, data);
console.log(widgets); // returns '[]'
});
}, []); // If I set 'widgets' here, my endpoint gets spammed
return (
<Fragment>
{/* {widgets.map((widget) => { // commented because it fails
<div>{widget.name}</div>;
})} */}
</Fragment>
);
};
Welcome to stackoverflow, first thing first the setting call is incorrect you must use spread operator to combine to array into one so change it to setWidgets([...widgets, ...data]); would be correct (I assume both widgets and data are Array)
second, react state won't change synchronously
.then((response) => {
const data = response.data;
console.log(data); // returns correctly filled array
setWidgets(widgets, data);
console.log(widgets); // <--- this will output the old state since the setWidgets above won't do it's work till the next re-render
so in order to listen to the state change you must use useEffect hook
useEffect(() => {
console.log("Changed Widgets: ", widgets)
}, [widgets])
this will console log anytime widget changes
the complete code will look like this
export const Home = () => {
const [widgets, setWidgets] = useState([]);
useEffect(() => {
axios
.get('/call-to-api')
.then((response) => {
const data = response.data;
setWidgets([...widgets, ...data])
});
}, []);
useEffect(() => {
console.log("Changed Widgets: ", widgets)
}, [widgets])
return (
<Fragment>
{/* {widgets.map((widget) => { // commented because it fails
<div>{widget.name}</div>;
})} */}
</Fragment>
);
};
Try:
setWidgets(data);
istead of
setWidgets(widgets, data);
Your widgets.map() probably fails because there isn't much to map over when the component is being rendered.
You should update it with a conditional like so, just for clarity:
widgets.length>0 ? widgets.map(...) : <div>No results</div>
And your call to setWidgets() should only take one argument, the data:
setWidgets(data)
or if you want to merge the arrays use a spread operator (but then you need to add widgets as the dependency to the useEffect dependency array.
setWidgets(...widgets, ...data)
You might also have to supply the setWidgets hook function to the useEffect dependency array.
Let me know if this helps..

React functional component test file unable to update state value and call its props function

i do have a functional parent component and it do have a state value , which have default value as false and it is calling another component which is visible only when the state value changes to true and the child component do have 2 function in it.
Illustrating with code
export const MainSearch = (props) => {
const [search, setSearch] = useState(false);
const closeSearch = () => {
setSearch(false);
ANALYTICS.trackEvent('popup_collpsed');
}
const toggleSearch = async () => {
await setSearch(true);
ANALYTICS.trackEvent('popup_expanded');
}
};
return (
<React.Fragment>
<searchBar toggleSearch={toggleSearch} />
{search &&
<childSearch
toggleSearch={toggleSearch}
closeSearch={closeSearch}
/>}
</React.Fragment>
)
}
And its test file with one test case
describe('MainSearch',()=>{
it('AdvancedSearch - Toggle popup_expanded and popup_collapsed ', async () => {
const component = shallow(<MainSearch {...props} />);
const searchBar = component.find('searchBar');
await searchBar.props().toggleSearch(); // calling function
expect(ANALYTICS.trackEvent).toHaveBeenCalledWith('popup_expanded');
const childSearchComponent = component.find('childSearch'); // not working ,since state value hides
expect(childSearchComponent).toBeDefined();
await advancedSearchComponent.props().closeSearch();// here getting null for .props()
expect(ANALYTICS.page.trackEvent).toHaveBeenCalledWith('popup_collapsed');
});
});
i know its possible with component.update for CLASS COMPONENTS, but here am using functional components and am getting error
if i remove the state value search , am getting my test case PASS, but cant remove that , its needed. so my test case need to make the state value true and call the function closeSearch and then check the analytics.
BUT am getting error Method “props” is meant to be run on 1 node. 0 found instead.
I guess state value if false and its not getting that particular node .
Can you guys please help me on same , since am stuck with it and can give more info if needed
Take a look at your toggleSearch function. You're awaiting setSearch, which isn't a promise. Remove the await Keyword and you should be fine!
If you would want to trigger your Analytics call only after the State has been set, you would need to hook in to Reacts useEffect Hook.
Then you could do something like this:
useEffect(() => {
if(search){
ANALYTICS.trackEvent('popup_expanded');
}
}, [search])
https://reactjs.org/docs/hooks-effect.html

How do I set initial state to data fetched with useQuery?

I'm working with React and Apollo and I'm trying to initialize state with data fetched using useQuery hook but I'm getting "Cannot read property 'map' of undefined" when loading the page.
const { loading, error, data } = useQuery(FETCH_BLOGS);
const [state, setState] = useState(undefined);
useEffect(() => {
if (loading === false && data) {
setState(data.blogs);
}
}, [loading, data]);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
In the JSX I'm calling {renderBlogs(state)} which maps over the array and is where the error is being thrown. If I pass in the initial data {renderBlogs(data.blogs)} it works but I need to store the data in state as it will be mutated.
When I console.log(state) it logs 2 lines:
The first is undefined.
The second is the array of blogs (as shown in the screenshot).
It appears that the page is trying to render the initial state (undefined) before before the state is set to the query data. Is this the case? I thought using useEffect would solve this but that doesn't seem to be the case. Any help is appreciated, thank you.
To quote the useEffect documentation:
The function passed to useEffect will run after the render is committed to the screen. Think of effects as an escape hatch from React’s purely functional world into the imperative world.
The problem is that useEffect will trigger after a render. This will result in the following chain of events. Assume loading is just set to false and there where no errors fetching the data.
When the component is now being rendered both the if (loading) and if (error) guard clauses will be skipped, because the data successfully loaded. However state is not yet updated because the useEffect callback will trigger after the current render. At this point when you call renderBlogs(state) state will still be undefined. This will trow the error you describe.
I might be missing some context, but from what is shown in the question there is no reason to use useEffect. Instead use data directly.
The example provided by Apollo for useQuery provides a good starting point:
import { gql, useQuery } from '#apollo/client';
const GET_GREETING = gql`
query GetGreeting($language: String!) {
greeting(language: $language) {
message
}
}
`;
function Hello() {
const { loading, error, data } = useQuery(GET_GREETING, {
variables: { language: 'english' },
});
if (loading) return <p>Loading ...</p>;
return <h1>Hello {data.greeting.message}!</h1>;
}
Applying the example to your scenario it might look very similar.
const { loading, error, data } = useQuery(FETCH_BLOGS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
return renderBlogs(data.blogs);
If you need the setter you might want to provide a bit more context to the question. However a common reason to have a setter might be if you want to apply some sort of filter. A simple solution would be to have a computed blogs state.
const filteredBlogs = data.blogs.filter(blog => blog.title.includes(search));
Here search would be the state of some <input value={search} onChange={handleSearchChange} /> element. To improve performance you could also opt to use useMemo here.
const filteredBlogs = useMemo(() => (
data.blogs.filter(blog => blog.title.includes(search))
), [data.blogs, search]);
If you really need to use useState for some reason, you could try the onCompleted option of useQuery instead of useEffect.
const [blogs, setBlogs] = useState();
const { loading, error } = useQuery(FETCH_BLOGS, {
onCompleted: data => setBlogs(data.blogs),
});

Rendering an array of objects React

Good afternoon,
I am trying to display data that is provided to my application by an instance of MongoDB before the initial render. I have yet been successful in doing so either running into errors or warnings.
This is the part of the render method I am working with.
<div className="right-column">
<div className="pipeline">
{this.props.tournamentList.map((x,i) => {
return <li key={i}>{x.name}</li>
})}
</div>
</div>
this.props.tournamentList has a value of an array of objects like so:
tournamentList:
Array[15]
0:
{…}
1:
{…}
2:
{…} ...
This list comes to my application through the componentWillMount lifecycle method, so before the initial render. To me I should be able to iterate through the array and make a dynamically generated list of tournaments provided by my database.
Yet with the code I provided I am getting this warning:
Uncaught (in promise) Error: Objects are not valid as a React child (found: object with keys {prelims, outRounds, notes}). If you meant to render a collection of children, use an array instead or wrap the object using createFragment(object) from the React add-ons. Check the render method ofBuildTournament.
I tried this approach, creating called displayTournaments and calling it inside the div with the class of "pipeline" but nothing happens, no errors no render:
displayTournaments(){
const { tournamentList } = this.props;
tournamentList.map((x,i) => {
return <li key={i}>{x.name}</li>
})
}
Clearly I am doing something wrong but I don't know what. Is this an instance where I should be using keyed fragments as suggested by the error message? Would anyone smarter than myself be willing to lend some insight?
Cheers.
Update:
Sorry, I misunderstood your question. Kyle is correct with the loading state.
In addition, using a library like lodash will allow you to map over objects in a more natural manner. The native javascript map method doesn't handle objects all that well.
https://www.npmjs.com/package/lodash
you use it much the same way. just
import _ from lodash
then
_.map(objectToMap, (x) => <Component key={x}>{x.thing}</Component>)
Here would be a simple solution that would have a loading state, error state, and success state.
The first thing to note is you will need to use Object.keys() to your object in order to map over the array of keys since you cannot map plain objects. You should also note that the map will return the key of each object so in order to target key values pairs you will need to use a syntax like this tournaments[key].name rather than just doing tournament.name as you are targeting an object with in an object and then grabbing the value.
Let me know if you need any more help with this
import React from 'react'
import Loader from '../Loader'
const resultList = ({ tournaments, isFetching = true }) => (
<div>
{
isFetching
? <div>
<Loader /><br />
<span>Loading…</span>
</div>
: <div>
{
Object.keys(tournaments).length
? <div>
{
tournaments.map((key) => (
<section id={tournaments[key].id} key={key}>
<p>{tournaments[key].name}</p>
</section>
))
}
</div>
: <div>
<p>There are no tournaments....</p>
</div>
}
</div>
}
</div>
);
export default resultList
You are going to need to have a loading state if you get your data in the componentWillMount or componentDidMount lifecycle hooks. The below example will illustrate how this is done.
class ComponentThatGetsAsyncData extends PureComponent {
constructor( props ) {
super( props );
this.state = {
tournamentList: [ ]
}
}
componentDidMount() {
// use any http library you choose
axios.get( "/some_url" )
.then( ( res ) => {
// this will trigger a re-render
this.setState({
tournamentList: res.data
});
})
.catch( ( err ) => {
// handle errors
});
}
render() {
const { tournamentList } = this.state;
// i'd use something other than index for key
// on your initial render your async data will not be available
// so you give a loading indicator and when your http call
// completes it will update state, triggering a re-render
return (
{
tournamentList ?
tournamentList.map((x,i) => {
return <li key={i}>{x.name}</li>
}) :
<div className="loading">Loading...</div>
}
);
}
}

Categories

Resources