star wars api, how do I get values from api? - reactjs

I'm using the https://swapi.dev/api/people/ and I'm having an interesting issue with an api I haven't encountered before. Here is an example response of the first value form the api using https://swapi.dev/api/people/
[
{
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "http://swapi.dev/api/planets/1/",
"films": [
"http://swapi.dev/api/films/1/",
"http://swapi.dev/api/films/2/",
"http://swapi.dev/api/films/3/",
"http://swapi.dev/api/films/6/"
],
"species": [],
"vehicles": [
"http://swapi.dev/api/vehicles/14/",
"http://swapi.dev/api/vehicles/30/"
],
"starships": [
"http://swapi.dev/api/starships/12/",
"http://swapi.dev/api/starships/22/"
],
"created": "2014-12-09T13:50:51.644000Z",
"edited": "2014-12-20T21:17:56.891000Z",
"url": "http://swapi.dev/api/people/1/"
},
]
The value I want to get is the first value of the films array, which I can achieve however its just a url. I'd need to somehow grab that url, make an api call with it and then get the first film. An example response after calling the api for the first value of films looks like this:
{
"title": "The Empire Strikes Back",
"episode_id": 5,
"opening_crawl": "It is a dark time for the\r\nRebellion. Although the Death\r\nStar has been destroyed,\r\nImperial troops have driven the\r\nRebel forces from their hidden\r\nbase and pursued them across\r\nthe galaxy.\r\n\r\nEvading the dreaded Imperial\r\nStarfleet, a group of freedom\r\nfighters led by Luke Skywalker\r\nhas established a new secret\r\nbase on the remote ice world\r\nof Hoth.\r\n\r\nThe evil lord Darth Vader,\r\nobsessed with finding young\r\nSkywalker, has dispatched\r\nthousands of remote probes into\r\nthe far reaches of space....",
"director": "Irvin Kershner",
"producer": "Gary Kurtz, Rick McCallum",
"release_date": "1980-05-17",
}
I've never used an api that has urls as values, any ideas on how this might work?
Here is my current code:
Page.jsx
import React, {useState, useEffect} from 'react';
import '../css/Page.css';
import axios from 'axios';
const Page = () => {
const [data, setData] = useState([]);
const fetchData = async () => {
try {
const res = await axios.get(
"https://swapi.dev/api/people/"
);
setData(res?.data.results);
} catch (error) {
console.log(error);
}
}
useEffect(() => {
fetchData();
}, [])
const mapData = () => {
if(data && data.length > 0) {
return (
<>
{data.map(item => (
<div className="itemContainer" key={item.name}>
<div className="image"><img src="" alt=""/></div>
<div className="content">
<span className="title">Name: </span><span className="name">{item.name}</span>
<br />
<span className="title">Birthyear: </span><span className="name">{item.birth_year}</span>
<br />
<span className="title">Homeworld: </span><span className="name">{item.homeworld}</span>
</div>
</div>
))}
</>
)
}
}
return (
<div className="container">
{mapData()}
</div>
);
}
export default Page;

Related

Undefined error when trying to map child objects in React

I'm trying to get image url for nested JSON object.
Tried {post.image.url} but I get an error saying url undefined
I appreciate any help or guidance that could be offered. New to Javascript / React but after an hour of Googling and searching couldn't come up with a solution for this. Must be missing something simple :-P
Here's my code...
export const getAllPosts = async () => {
return await fetch(
`https://notion-api.splitbee.io/v1/table/${NOTION_BLOG_ID}`
).then((res) => res.json());
}
export async function getStaticProps() {
const posts = await getAllPosts()
return {
props: {
posts
},
};
}
function Blog({ posts }) {
return (
<div>
{posts.map((post) => (
<Link href="/blog/[slug]" as={`/blog/${post.slug}`}>
<div>
<div className='text-6xl'>{post.title}</div>
<img className='w-24' src={post.imgUrl}></img>
{/*{post.image.rawUrl}*/}
</div>
</Link>
))}
</div>
);
}
export default Blog
Here's the JSON...
[
{
"id": "90ee0723-aeb5-4d64-a970-332aa8f819f6",
"slug": "first-post",
"date": "2020-04-21",
"Related to Notion API Worker (Column)": [
"0976bfa6-392a-40b0-8415-94a006dba8d9"
],
"imgUrl": "https://www.notion.so/image/https:%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F689883de-2434-4be3-8179-a8ba62a7bc1e%2Fsnowmountain.jpg?table=block&id=90ee0723-aeb5-4d64-a970-332aa8f819f6&cache=v2",
"image": [
{
"name": "snowmountain.jpg",
"url": "https://www.notion.so/image/https:%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F689883de-2434-4be3-8179-a8ba62a7bc1e%2Fsnowmountain.jpg?table=block&id=90ee0723-aeb5-4d64-a970-332aa8f819f6&cache=v2",
"rawUrl": "https://s3-us-west-2.amazonaws.com/secure.notion-static.com/689883de-2434-4be3-8179-a8ba62a7bc1e/snowmountain.jpg"
}
],
"title": "My first blogpost bruce"
}
]
The image property of a post is an array as denoted by the bracket on the end of line 1:
1. "image": [
2. {
3. "name": "snowmountain.jpg",
4. "url": "https://www.notion.so/image/https:%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F689883de-2434-4be3-8179-a8ba62a7bc1e%2Fsnowmountain.jpg?table=block&id=90ee0723-aeb5-4d64-a970-332aa8f819f6&cache=v2",
5. "rawUrl": "https://s3-us-west-2.amazonaws.com/secure.notion-static.com/689883de-2434-4be3-8179-a8ba62a7bc1e/snowmountain.jpg"
6. }
7. ],
If you know that there will always be exactly one image you could access it directly by referencing the first array item.
function Blog({ posts }) {
return (
<div>
{posts.map((post) => (
<Link href="/blog/[slug]" as={`/blog/${post.slug}`}>
<div>
<div className='text-6xl'>{post.title}</div>
<img className='w-24' src={post.imgUrl}></img>
{post.image && post.image[0].rawUrl}
</div>
</Link>
))}
</div>
);
}
Note that this could err if the array is undefined / null or if it's empty, so you should probably safe-guard it by only displaying something if it exists.
There is no need to call then() method, if you use async\await.
As mdn says:
await can be put in front of any async promise-based function to pause
your code on that line until the promise fulfills, then return the
resulting value.
export const getAllPosts = async () => {
return await fetch(
`https://notion-api.splitbee.io/v1/table/${NOTION_BLOG_ID}`
);
}
export async function getStaticProps() {
const posts = await getAllPosts()
return {
props: {
posts
},
};
}

Mapping API response to JSX

I have a JSON response in the form:
[
{
"author": 2,
"title": "how to draw",
"slug": "how-to-draw",
"content": "second attempt",
"status": 0,
"date_created": "2020-11-28T20:25:41.650172Z",
"date_posted": "2020-11-28T20:25:41.650172Z",
"tags": "none"
},
{
"author": 1,
"title": "Admin test post",
"slug": "admin-test-post",
"content": "This is just a test of the content field!\r\n\r\nParagraph 2",
"status": 4,
"date_created": "2020-11-16T21:02:02.521705Z",
"date_posted": "2020-11-16T21:37:40.477318Z",
"tags": "a b c"
}
]
And I am struggling to map the response correctly to a series of divs in ReactJS. The idea would be to have a div for each post, each with the author, title, etc. of the post. I can use post[0].title to take the title of the first element of the array. Am I missing something obvious?
import React, {useState} from 'react';
import axios from 'axios';
const Django = () => {
const [posts, setPosts] = useState([]);
// proxy set to http://127.0.0.1:8000/
const apiLink = "/api/post/";
const fetchData = async () => {
const res = await axios.get(`${apiLink}`, { headers: { Accept: "application/json"} });
console.log(res.data);
setPosts([res.data]);
}
return (
<div>
<h1>Posts:</h1>
<button onClick={fetchData}>Load jokes</button>
{posts.map((post, key) => {
//console.log(joke);
return (
<div className="data" key={key}>
{post.title}
{key}
</div>
)})}
</div>
);
}
export default Django;
It is not exactly clear what your problem is, but I suppose you're confused about why you aren't getting any data by directly calling {post.title} in the map body. That's because you're putting the result from your fetch, which is already an array into another array: setPosts([res.data]);. Just change the state setter to setPosts(res.data); and everything in the map should work fine.

how do I Mock API and following the same approach as my static array?

I have a React hooks component, which uses an HTML div-alike table to render data, and the data is being fetched from the server. I need to test the component to see if the table has data by mocking the API call. Below is the current code. I want to remove the use of arr completely and make avoid getting {users} rendered as text/plain as you can you in the below image
const arr = [
{
"demo": [
{
"_id": "T0810",
"title": "Historian",
"tags": [
"demo"
],
"queries": [],
},
{
"_id": "T0817",
"title": "book",
"tags": [
"demo"
],
"queries": [],
},
],
"demo_2": [
{
"_id": "T0875",
"title": "Program",
"tags": [
"demo_2",
"Control"
],
"queries": [],
},
{
"_id": "T0807",
"title": "Interface",
"tags": [
"demo_2"
],
"queries": [],
}
]
}];
const keys = Object.keys(arr[0]);
export default function Demo () {
const [isModalOpen, setModalIsOpen] = useState(false);
const [users, setUsers] = useState([]);
const handleOnClick = async () => {
try {
const { data } = await axios.get('https://run.mocky.io/v3/0d7aa6e3-fc01-4a47-893d-7e1cc3013d4e');
setUsers(data);
// Now that the data has been fetched, open the modal
setModalIsOpen(true);
} catch (err) {
console.error("failed", err);
}
};
return (
<div className="container">
<>
{keys.map((key) => (
<div className="col" key={key}>
<div className="row">{key}</div>
{arr[0][key].map((item) => (
<div className="row" key={item.technique_id} onClick={() => handleOnClick(item)}>{item.technique}</div>
))}
</div>
))}
</>
{isModalOpen && <Modal onRequestClose={() => setModalIsOpen(false)} data={users}/>}
</div>
);
}
Second attempt...Assuming I understand the ask then a package like Nock can do the trick (https://github.com/nock/nock).
The approach is to allow Nock to mock the API and return the result locally without calling the server. In your case it will be something like:
const nock = require('nock');
const arr = [...]; // your original definition of arr
const scope = nock('https://run.mocky.io')
.get('/v3/0d7aa6e3-fc01-4a47-893d-7e1cc3013d4e')
.reply(200, arr);
// The rest of your code "as is"
...
This setup of Nock will intercept every HTTP call to https://run.mocky.io and will specifically return the content of arr in response to a GET /v3/0d7aa6e3-fc01-4a47-893d-7e1cc3013d4e.
Typically, I would not put this code in the main code.
Instead you can use it as part of a testing script that runs without dependency on an active server. E.g. testing during build.

Refactoring from promise to async await and use pagination

I'm trying to re factor this code to use async/await
fetchTopRatedMovies(pageNumber).then((newData) =>
setApiData({
...newData,
results: [...apiData.results, ...newData.results]
})
);
I;m trying to use it in a try catch block within a useEffect
This is what I have so far.
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(
`${baseURL}${option}?api_key=${API_KEY}&language=en&page=${pageNumber}&region=GB`
);
const data = await response.json();
setMovieData({
...movieData,
...data,
});
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
setLoading(true);
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [option, pageNumber]);
Problem is I think is this part
const data = await response.json();
setMovieData({
...movieData,
...data,
});
As my state const [movieData, setMovieData] = useState({ page: 0, results: [] });
Is not being updated with the old data only the new data that changes on pageNumber increase.
My main goal is to have a button that adds more data onto the already displayed data.
Full code so far:
export const Directory = () => {
const [option, setOption] = useState('popular');
const [pageNumber, setPageNumber] = useState(1);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [movieData, setMovieData] = useState({ page: 0, results: [] });
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(
`${baseURL}${option}?api_key=${API_KEY}&language=en&page=${pageNumber}&region=GB`
);
const data = await response.json();
setMovieData({
...movieData,
...data,
});
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
setLoading(true);
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [option, pageNumber]);
const { results, page, total_pages } = movieData;
const handleOptionChange = e => {
setOption(e.target.value);
setPageNumber(1);
};
const pageLimit = page === 0 || page < total_pages;
return (
<div>
<select value={option} onChange={e => handleOptionChange(e)}>
<option value='popular'>Popular</option>
<option value='top_rated'>Top Rated</option>
<option value='now_playing'>Now Playing</option>
</select>
<ul>
{results &&
results.map(movie => {
return <li key={movie.id}>{movie.title}</li>;
})}
</ul>
{results && (
<button
disabled={!pageLimit}
onClick={() => setPageNumber(pageNumber + 1)}>
More
</button>
)}
</div>
);
};
Currently only one page results data displays at a time. So for first render page 1. Then when I click the more button. It fetchs page 2 results. However it only displays one page at a time. I want the new results to be added to the already rendered data. So with each button press more results are displayed.
console.log of data = await response.json()
{
"page": 1,
"total_results": 10000,
"total_pages": 500,
"results": [
{
"popularity": 728.376,
"vote_count": 3070,
"video": false,
"poster_path": "/xBHvZcjRiWyobQ9kxBhO6B2dtRI.jpg",
"id": 419704,
"adult": false,
"backdrop_path": "/5BwqwxMEjeFtdknRV792Svo0K1v.jpg",
"original_language": "en",
"original_title": "Ad Astra",
"genre_ids": [
18,
878
],
"title": "Ad Astra",
"vote_average": 6,
"overview": "The near future, a time when both hope and hardships drive humanity to look to the stars and beyond. While a mysterious phenomenon menaces to destroy life on planet Earth, astronaut Roy McBride undertakes a mission across the immensity of space and its many perils to uncover the truth about a lost expedition that decades before boldly faced emptiness and silence in search of the unknown.",
"release_date": "2019-09-18"
},
{
"popularity": 220.799,
"id": 454626,
"video": false,
"vote_count": 2868,
"vote_average": 7.5,
"title": "Sonic the Hedgehog",
"release_date": "2020-02-14",
"original_language": "en",
"original_title": "Sonic the Hedgehog",
"genre_ids": [
28,
878,
35,
10751
],
"backdrop_path": "/stmYfCUGd8Iy6kAMBr6AmWqx8Bq.jpg",
"adult": false,
"overview": "Based on the global blockbuster videogame franchise from Sega, Sonic the Hedgehog tells the story of the world’s speediest hedgehog as he embraces his new home on Earth. In this live-action adventure comedy, Sonic and his new best friend team up to defend the planet from the evil genius Dr. Robotnik and his plans for world domination.",
"poster_path": "/aQvJ5WPzZgYVDrxLX4R6cLJCEaQ.jpg"
},
{
"popularity": 204.235,
"vote_count": 3202,
"video": false,
"poster_path": "/y95lQLnuNKdPAzw9F9Ab8kJ80c3.jpg",
"id": 38700,
"adult": false,
"backdrop_path": "/upUy2QhMZEmtypPW3PdieKLAHxh.jpg",
"original_language": "en",
"original_title": "Bad Boys for Life",
"genre_ids": [
28,
80,
53
],
"title": "Bad Boys for Life",
"vote_average": 7.2,
"overview": "Marcus and Mike are forced to confront new threats, career changes, and midlife crises as they join the newly created elite team AMMO of the Miami police department to take down the ruthless Armando Armas, the vicious leader of a Miami drug cartel.",
"release_date": "2020-01-17"
}
]
}
Your movieData structure is:
{ page: 0, results: [] }
When you do:
setMovieData({
...movieData,
...data,
})
It just replaces the previous data with the newer one. Because the keys of both new and old data are the same. What you need to do is something like this:
setMovieData({
results: [
...movieData.results, ...data.results
],
page: data.page
})
It will append new data after old data. Hope it helps.
Another mistake is with the following code:
const pageLimit = page === 0 || page < total_pages;
This needs to be managed directly with movieData state. Update your code as following:
return (
<div>
<select value={option} onChange={e => handleOptionChange(e)}>
<option value='popular'>Popular</option>
<option value='top_rated'>Top Rated</option>
<option value='now_playing'>Now Playing</option>
</select>
<ul>
{movieData.results &&
movieData.results.map(movie => {
return <li key={movie.id}>{movie.title}</li>;
})}
</ul>
{movieData.results && (
<button
disabled={!(movieData.page === 0 || movieData.page < total_pages)}
onClick={() => setPageNumber(pageNumber + 1)}>
More
</button>
)}
</div>
);

React, how to render static json data with different keys

Here's an example of the static json data.
I'm having trouble rendering the 'promoimage' from the array.
I'm not 100% sure how to go about this to solve it. I was playing arround and wanted to check if 'promoimage' exists, but nothing was returned?
Any advice how to achieve this?
[
{
"title": "some title",
"promoimage": "image.jpg",
"url": "#"
},
{
"title": "some title",
"image": "example.jpg",
"url": "#"
},
{
"title": "some other title",
"promoimage": "image.jpg",
"url": "#"
},
{
"title": "title",
"image": "example.jpg",
"url": "#"
},
]
My React component:
import products from '../product-data.json';
...
export const CustomSlider = () => {
// Here I'm using react-slick
const productList = products.map((product, i) => {
const uniqueItems = [];
if (uniqueItems.indexOf(product.imageone) === -1) {
uniqueItems.push(product.imageone);
}
/* This works
if (product.hasOwnProperty('promoimage')) {
return true
}
*/
return <Product key={i} {...product} />;
}
);
return (
<Slider>
{productList}
</Slider>
)
}
The code is sending all object keys to Product, as props. Particularly this part {...product} is expanded into this:
<Product
key={i}
title="some title"
promoimage="image.jpg"
url="#"
/>
This is called spreading.
Now, I suspect <Product> doesn't know what to do with promoimage, but knows what to do with image. We haven't sent any image so we have to fix that. We can do so by either modifying product so that it renders image || promoimage, or change our parsing to this:
const productList = products.map((product, i) => {
const uniqueItems = []
if (uniqueItems.indexOf(product.promoimage) === -1) {
uniqueItems.push(product.promoimage)
}
return (
<Product
key={i}
{...product}
image={product.image || product.promoimage}
/>
)
})

Resources