async fetch pushing data twice into array - reactjs

I'm trying to make a Rick & Morty API call with fetch and an async arrow function, but I found that the function is pushing the elements received twice into my array.
I already tried to make the call with and without useEffect (I'm using React with TypeScript) but I got no results and I don't understand why the function is being called twice.
Anyone available to explain to me why this is happening?
data.ts:
import { PlanetInterface, ResidentsInterface } from "./data-interfaces";
export const planetsList: PlanetInterface[] = [];
export const residentsList: ResidentsInterface[] = [];
export const getPlanetById = async (planets: number[]) => {
for (let planet of planets) {
const response = await fetch(
`https://rickandmortyapi.com/api/location/${planet}`
);
const planetData: PlanetInterface = await response.json();
planetsList.push(planetData);
}
console.log(planetsList);
};
// export const getResidentsByPlanet = async (residents: string[]) => {
// for (let resident of residents) {
// const response = await fetch(resident);
// const residentData = await response.json();
// residentsList.push(residentData);
// }
// console.log(residentsList);
// };
app.tsx:
import { useEffect } from "react";
import { getPlanetById } from "./api/data";
import "./App.css";
function App() {
useEffect(() => {
getPlanetById([1, 2]);
}, []);
// getPlanetById([1, 2]);
return <main className="container"></main>;
}
export default App;
Expected output: Array of 2 objects (planets with ID 1 and 2)
Received output: Array of 4 objects (planet with ID 1 twice and planet with ID 2 also twice)
If anyone can help me understand why this is happening and how I can fix it, I would be very grateful.

The design of that getPlanetById might be not suit for React since the call of it create a side effect and there is no way to clean it up, you should wrap it into a hook or do a manually clean up, here is an example:
useEffect(() => {
getPlanetById([1, 2]);
return () => { planetsList.length = 0 }
}, []);

I guess you are using <React.StrictMode />
If you remove that, the function is called once as you expect.
Here is the document about strict mode
https://en.reactjs.org/docs/strict-mode.html

Related

What's wrong with my custom hook in React?

Hi Stack Overflow Community!
I am learning react and now I am practicing custom hooks. I get the example data from here:
https://jsonplaceholder.typicode.com/posts
I've got two components.
App.js
import React from "react";
import useComments from "./hooks/useComments"
const App = () => {
const Comments = useComments();
const renderedItems = Comments.map((comment) => {
return <li key={comment.id}>{comment.title}</li>;
});
return (
<div>
<h1>Comments</h1>
<ul>{renderedItems}</ul>
</div>
);
}
export default App;
useComments.js
import {useState, useEffect} from "react";
const useComments = () => {
const [Comments, setComments] = useState([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/posts", {
"mode": "cors",
"credentials": "omit"
}).then(res => res.json()).then(data => setComments(data))
}, []);
return [Comments];
};
export default useComments;
My output looks like this and i don't know why. There are no warnings or errors.
This line makes Comments be an array:
const [Comments, setComments] = useState([]);
...and then you're wrapping it in an additional array:
return [Comments];
But when you use it, you're treating it as a single dimensional array.
const Comments = useComments();
const renderedItems = Comments.map...
So you'll just need to line those two up. If you want two levels of array-ness (perhaps because you plan to add more to your hook, so that it's returning more things than just Comments), then the component will need to remove one of them. This can be done with destructuring, as in:
const [Comments] = useComments();
Alternatively, if you don't need that complexity, you can change your hook to not add the extra array, and return this:
return Comments;

Re-rendering of Google Maps Elevations API responses within React functional component

I am writing an app for vehicle tracking. With the help of Google Maps API, I am able to get directions and extract all the required info. The problem appeared with Elevations API responses. From DirectionRender class I am sending path and distance as props. GM Elevations request is done via elevator.getElevationAlongPath(option,PlotElevation). PlotElevation (elevations,status) is a callback function. However, no matter how I try to receive just one response from it (using useMemo, useEffect, I think I tried everything), still, there are problems with the re-rendering of responses. OVER_QUERY_LIMIT or endless re-render. Could someone help with that?
Thanks
const Elevation = React.memo(props =>{
const [path, setPath] = useState({...props.path})
const [distance, setDistance]=useState({...props.distance})
const [elevationArray, setElevationArray] = useState(null)
const [stop, setStop] = useState(false)
let pathElev = JSON.parse(JSON.stringify(path))
React.useEffect(() => {
setPath(props.path)
}, [props.path])
React.useEffect(() => {
setDistance(props.distance)
}, [props.distance])
let elevator = new window.google.maps.ElevationService;
let numberSamples = parseInt( distance/40)
let options = {
'path':path,
'samples':numberSamples
}
//The problem starts here
const PlotElevation = (elevations, status) => {
if (stop === false){
console.log('status',status)
console.log(JSON.parse(JSON.stringify(elevations)))
setElevationArray(elevations)
//setStop(true)
console.log(elevations[19].elevation)
return
}
}
const Memo = React.useMemo(
()=>{
elevator.getElevationAlongPath(
options,PlotElevation
)
},[elevator.getElevationAlongPath(
options,PlotElevation
)])
// elevator.getElevationAlongPath(
// {
// path: path,
// samples: 100
// }, elevations =>{
// setElevationArray({
// // We’ll probably want to massage the data shape later:
// // elevationArray: elevations
// })
// }
// )
return (
<div>
{Memo}
{console.log('path is received ', pathElev)}
{console.log('number of samples', numberSamples)}
{console.log('elevation check ',elevationArray)}
{/* {elevator.getElevationAlongPath(
options,PlotElevation)} */}
</div>
)
})
export default Elevation

How to link to a show view from an index using react hooks with firestore data

I am trying to figure out how to define a link to reference that can use a firebase document id to link to a show view for that document. I can render an index. I cannot find a way to define a link to the document.
I've followed this tutorial - which is good to get the CRUD steps other than the show view. I can find other tutorials that do this with class components and the closest I've been able to find using hooks is this incomplete project repo.
I want to try and add a link in the index to show the document in a new view.
I have an index with:
const useBlogs = () => {
const [blogs, setBlogs] = useState([]); //useState() hook, sets initial state to an empty array
useEffect(() => {
const unsubscribe = Firebase
.firestore //access firestore
.collection("blog") //access "blogs" collection
.where("status", "==", true)
.orderBy("createdAt")
.get()
.then(function(querySnapshot) {
// .onSnapshot(snapshot => {
//You can "listen" to a document with the onSnapshot() method.
const listBlogs = querySnapshot.docs.map(doc => ({
//map each document into snapshot
id: doc.id, //id and data pushed into blogs array
...doc.data() //spread operator merges data to id.
}));
setBlogs(listBlogs); //blogs is equal to listBlogs
});
return
// () => unsubscribe();
}, []);
return blogs;
};
const BlogList = ({ editBlog }) => {
const listBlog = useBlogs();
return (
<div>
{listBlog.map(blog => (
<Card key={blog.id} hoverable={true} style={{marginTop: "20px", marginBottom: "20px"}}>
<Title level={4} >{blog.title} </Title>
<Tag color="geekblue" style={{ float: "right"}}>{blog.category} </Tag>
<Paragraph><Text>{blog.caption}
</Text></Paragraph>
<Link to={`/readblog/${blog.id}`}>Read</Link>
<Link to={`/blog/${blog.id}`}>Read</Link>
</Card>
))}
</div>
);
};
export default BlogList;
Then I have a route defined with:
export const BLOGINDEX = '/blog';
export const BLOGPOST = '/blog/:id';
export const NEWBLOG = '/newblog';
export const EDITBLOG = '/editblog';
export const VIEWBLOG = '/viewblog';
export const READBLOG = '/readblog/:id';
I can't find a tutorial that does this with hooks. Can anyone see how to link from an index to a document that I can show in a different page?
I did find this code sandbox. It looks like it is rendering a clean page in the updateCustomer page and using data from the index to do it - but the example is too clever for me to unpick without an explanation of what's happening (in particular, the updateCustomer file defines a setCustomer variable, by reference to useForm - but there is nothing in useForm with that definition. That variable is used in the key part of the file that tries to identify the data) - so I can't mimic the steps.
NEXT ATTEMPT
I found this blog post which suggests some changes for locating the relevant document.
I implemented these changes and while I can print the correct document.id on the read page, I cannot find a way to access the document properties (eg: blog.title).
import React, { useHook } from 'react';
import {
useParams
} from 'react-router-dom';
import Firebase from "../../../firebase";
import BlogList from './View';
function ReadBlogPost() {
let { slug } = useParams()
// ...
return (
<div>{slug}
</div>
)
};
export default ReadBlogPost;
NEXT ATTEMPT:
I tried to use the slug as the doc.id to get the post document as follows:
import React, { useHook, useEffect } from 'react';
import {
useParams
} from 'react-router-dom';
import Firebase from "../../../firebase";
import BlogList from './View';
function ReadBlogPost() {
let { slug } = useParams()
// ...
useEffect(() => {
const blog =
Firebase.firestore.collection("blog").doc(slug);
blog.get().then(function(doc) {
if (doc.exists) {
console.log("Document data:", doc.data());
doc.data();
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});
});
return (
<div>{blog.title}
</div>
)
};
export default ReadBlogPost;
It returns an error saying blog is not defined. I also tried to return {doc.title} but I get the same error. I can see all the data in the console.
I really can't make sense of coding documentation - I can't figure out the starting point to decipher the instructions so most things I learn are by trial and error but I've run out of places to look for inspiration to try something new.
NEXT ATTEMPT
My next attempt is to try and follow the lead in this tutorial.
function ReadBlogPost(blog) {
let { slug } = useParams()
// ...
useEffect(() => {
const blog =
Firebase.firestore.collection("blog").doc(slug);
blog.get().then(function(doc) {
if (doc.exists) {
doc.data()
console.log("Document data:", doc.data());
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});
},
[blog]
);
return (
<div><Title level={4} > {blog.title}
</Title>
<p>{console.log(blog)}</p>
</div>
)
};
export default ReadBlogPost;
When I try this, the only odd thing is that the console.log inside the useEffect method gives all the data accurately, but when I log it form inside the return method, I get a load of gibberish (shown in the picture below).
NEXT ATTEMPT
I found this tutorial, which uses realtime database instead of firestore, but I tried to copy the logic.
My read post page now has:
import React, { useHook, useEffect, useState } from 'react';
import {
useParams
} from 'react-router-dom';
import Firebase from "../../../firebase";
import BlogList from './View';
import { Card, Divider, Form, Icon, Input, Switch, Layout, Tabs, Typography, Tag, Button } from 'antd';
const { Paragraph, Text, Title } = Typography;
const ReadBlogPost = () => {
const [loading, setLoading] = useState(true);
const [currentPost, setCurrentPost] = useState();
let { slug } = useParams()
if (loading && !currentPost) {
Firebase
.firestore
.collection("blog")
.doc(slug)
.get()
.then(function(doc) {
if (doc.exists) {
setCurrentPost(...doc.data());
console.log("Document data:", doc.data());
}
}),
setLoading(false)
}
if (loading) {
return <h1>Loading...</h1>;
}
return (
<div><Title level={4} >
{currentPost.caption}
{console.log({currentPost})}
</Title>
</div>
)
};
export default ReadBlogPost;
Maybe this blog post is old, or maybe it's to do with it using .js where I have .jsx - which I think means I can't use if statements, but I can't get this to work either. The error says:
Line 21:9: Expected an assignment or function call and instead saw
an expression no-unused-expressions
It points to the line starting with Firebase.
I got rid of all the loading bits to try and make the data render. That gets rid of the above error message for now. However, I still can't return the values from currentPost.
It's really odd to me that inside the return statement, I cannot output {currentPost.title} - I get an error saying title is undefined, but when I try to output {currentPost} the error message says:
Error: Objects are not valid as a React child (found: object with keys
{caption, category, createdAt, post, status, title}). If you meant to
render a collection of children, use an array instead.
That makes no sense! I'd love to understand why I can log these values before the return statement, and inside the return statement, I can log them on the object but I cannot find how to log them as attributes.
First of all: is your useBlog() hook returning the expected data? If so, all you need to do is define your <Link/> components correctly.
<Link
// This will look like /readblog/3. Curly braces mean
// that this prop contains javascript that needs to be
// evaluated, thus allowing you to create dynamic urls.
to={`/readblog/${blog.id}`}
// Make sure to open in a new window
target="_blank"
>
Read
</Link>
Edit: If you want to pass the data to the new component you need to set up a store in order to avoid fetching the same resource twice (once when mounting the list and once when mounting the BlogPost itself)
// Define a context
const BlogListContext = React.createContext()
// In a top level component (eg. App.js) define a provider
const App = () => {
const [blogList, setBlogList] = useState([])
return (
<BlogListContext.Provider value={{blogList, setBlogList}}>
<SomeOtherComponent/>
</BlogListContext.Provider>
)
}
// In your BlogList component
const BlogList = ({ editBlog }) => {
const { setBlogList } = useContext(BlogListContext)
const listBlog = useBlogs()
// Update the blog list from the context each time the
// listBlog changes
useEffect(() => {
setBlogList(listBlog)
}, [listBlog])
return (
// your components and links here
)
}
// In your ReadBlog component
const ReadBlogComponent = ({ match }) => {
const { blogList } = useContext(BlogListContext)
// Find the blog by the id from params.
const blog = blogList.find(blog => blog.id === match.params.id) || {}
return (
// Your JSX
)
}
There are other options for passing data as well:
Through url params (not recommended).
Just pass the ID and let the component fetch its own data on mount.
I found an answer that works for each attribute other than the timestamp.
const [currentPost, setCurrentPost] = useState([]);
There is an empty array in the useState() initialised state.
In relation to the timestamps - I've been through this hell so many times with firestore timestamps - most recently here. The solution that worked in December 2019 no longer works. Back to tearing my hair out over that one...

How do I update an array using the useContext hook?

I've set a Context, using createContext, and I want it to update an array that will be used in different components. This array will receive the data fetched from an API (via Axios).
Here is the code:
Context.js
import React, { useState } from "react";
const HeroContext = React.createContext({});
const HeroProvider = props => {
const heroInformation = {
heroesContext: [],
feedHeroes: arrayFromAPI => {
setHeroesContext(...arrayFromAPI);
console.log();
}
};
const [heroesContext, setHeroesContext] = useState(heroInformation);
return (
<HeroContext.Provider value={heroesContext}>
{props.children}
</HeroContext.Provider>
);
};
export { HeroContext, HeroProvider };
See above that I created the context, but set nothing? Is it right? I've tried setting the same name for the array and function too (heroesContex and feedHeroes, respectively).
Component.js
import React, { useContext, useEffect } from "react";
import { HeroContext } from "../../context/HeroContext";
import defaultSearch from "../../services/api";
const HeroesList = () => {
const context = useContext(HeroContext);
console.log("Just the context", context);
useEffect(() => {
defaultSearch
.get()
.then(response => context.feedHeroes(response.data.data.results))
.then(console.log("Updated heroesContext: ", context.heroesContext));
}, []);
return (
//will return something
)
In the Component.js, I'm importing the defaultSearch, that is a call to the API that fetches the data I want to push to the array.
If you run the code right now, you'll see that it will console the context of one register in the Just the context. I didn't want it... My intention here was the fetch more registers. I have no idea why it is bringing just one register.
Anyway, doing all of this things I did above, it's not populating the array, and hence I can't use the array data in another component.
Does anyone know how to solve this? Where are my errors?
The issue is that you are declaring a piece of state to store an entire context object, but you are then setting that state equal to a single destructured array.
So you're initializing heroesContext to
const heroInformation = {
heroesContext: [],
feedHeroes: arrayFromAPI => {
setHeroesContext(...arrayFromAPI);
console.log();
}
};
But then replacing it with ...arrayFromAPI.
Also, you are not spreading the array properly. You need to spread it into a new array or else it will return the values separately: setHeroesContext([...arrayFromAPI]);
I would do something like this:
const HeroContext = React.createContext({});
const HeroProvider = props => {
const [heroes, setHeroes] = useState([]);
const heroContext = {
heroesContext: heroes,
feedHeroes: arrayFromAPI => {
setHeroes([...arrayFromAPI]);
}
};
return (
<HeroContext.Provider value={heroContext}>
{props.children}
</HeroContext.Provider>
);
};
export { HeroContext, HeroProvider };

Infinite loop with nested object

Why it's looping if i use { data: {}} when all fine with { data: 1} ?
Codesandbox example.
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [test, setTest] = useState(true);
const role = useRole({ data: {} }); // with object { data: 1 } all fine
useEffect(() => {
setTest(false);
}, []);
return 1;
}
export function useRole({ data }) {
const [role, roleSet] = useState(false);
useEffect(() => {
console.log("looping");
roleSet({});
}, [data]);
return role;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
I think what #TvG told is right on spot. Object comparison are done via references.
When you are creating the object the way you have done in the code will create new reference object everytime.
const role = useRole({ data: {} });
Even if you do it like this:
let defaultRole = { data: {} }
const role = useRole(defaultRole);
It will be creating the new object every time . the value of defaultRole will be recalculated in every render.
What can be done here is , React provides us useRef method which will not change on rerenders unless changed explicitly. Here is the link for you to read:
useRef docs
You can do something like this:
const { useEffect, useState, useRef } = React
function App() {
const [test, setTest] = useState(true);
console.log("running this")
let baseObj = {
data: {}
}
const roleDefaultValueRef = useRef(baseObj)
const role = useRole(roleDefaultValueRef.current);
useEffect(() => {
setTest(false);
}, []);
return 1
}
function useRole({ data }) {
const [role, roleSet] = useState(false);
useEffect(() => {
console.log("looping");
roleSet({});
debugger
}, [data]);
return role;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Folllow up question why object like data = {} will work in this case.
SO your useRole has an effect which is dependent on data. Let say you called useRole as useRole({})
now in your useRole you are spreading data value out of this passed object {}
so this line
export function useRole({ data }) {
// data will be evaluated as undefined and undefined will remain same on //consecutive rerenders and hence the effect will not run
}
This is the reason it is running when you are passing a blank object to useRole.
Hope this helps.
FOr understanding try to print the value of data in useRole, you will definitely understand it :)
1 === 1 //true
undefined === undefined //true
{} === {} // false
I assume that this has to do with how javascript works.
In react, the useEffect runs after every render by default. A way to customize that behavior is by providing a list as the second parameter.
That way react will check after a render against the provided values from the previous render and call the effect only when the values have changed.
This is where the problem comes in as you may verify with the example below:
if(1 === 1) {
console.log("1 === 1");
}
if({} === {}) {
console.log("{} === {}");
}
If you run this you may notice an output of only 1 === 1 here. That is because javascript is not treating two empty objects as equal.
Since you are providing { data: {} } and unpack the value of data in your useRole you have an empty object in your list.
I hope that helps.

Resources