Second fetch on API - React - reactjs

I'm trying to access the data of an api and I have a problem that I can't solve...
I have made a call to the api and it returns the results correctly:
export const getCharacters = async (category) => {
const url = `https://www.breakingbadapi.com/api/characters?name=${encodeURI(category)}`;
const resp = await fetch(url);
const data = await resp.json();
const characters = data.map(item => {
return {
id: item.char_id,
title: item.name,
url: item.img,
nickname: item.nickname,
bday: item.birthay,
occupation: item.occupation,
status: item.status
}
})
return characters;
}
I put the results into a card and then I add in the character card a link to another component where I want to show "more details":
import React from 'react'
import { Link } from 'react-router-dom'
import '../../index.css'
export const CharactersGridItem = ({id, title, url}) => {
return (
<div className="card-global">
<span className="col">
<span className="card" style={{width: '10rem'}}>
<span className="row">
<img src={url} className="card-img-top" alt={title} style={{ width: '270px', height: '250px'}}></img>
<h6 className="card-title">{title}</h6>
<Link
className="card-body"
to={`/Characters/${id}`}
>
Details...
</Link>
</span>
</span>
</span>
</div>
)
}
All this works correctly and takes me to the url of the character's id on which I click.
My problem is when I try to recover those other properties of the character that, curiously, I can see them in the console (before everything explodes).
Here is the hook with which I made that second "request" for data:
import { useState, useEffect } from "react";
export const useCharacter = (id) => {
const [character, setCharacter] = useState()
function getCharacter(id) {
return fetch(
`https://www.breakingbadapi.com/api/characters/${encodeURI(id)}`
);
}
useEffect(() => {
// Después de que el componente se monta, se ejecuta este callback
getCharacter(id).then((resp) => resp.json()).then(([body]) => {
//console.log(body)
setCharacter(body)
})
}, [])
//return character;
return character;
}
And this is where I bring the data:
import React from 'react'
import { useParams } from 'react-router-dom';
import { useCharacter } from '../../hooks/useCharacter';
export const CharactersDetail = (setCharacter) => {
const { id } = useParams()
const characters = useCharacter(id)
console.log(characters)
return (
<div className="container">
<div className="container-section">
<p>{characters.name}</p>
<p>{characters.nickname}</p>
</div>
</div>
)
}
When I click on a character, and go to the details page (CharacterDetails), if I don't put this code on the return...:
<p>{characters.name}</p>
<p>{characters.nickname}</p>
...everything works correctly and with console.log it prints the data json correctly (console). Even if I write the code and refresh the page (localhost:3000), it prints what I ask for.
But the moment I go back to the character page, click on another one (different id) and get to its corresponding detail page, everything explodes. An error tells me that it doesn't recognize the characters.name or characters.nickname.
Any ideas on why this might be happening?
Any help is greatly appreciated!!!

wcharacters will be undefined for the first render. Only after the data is received your components render again.
So you need to explicitly handle the case where useCharacter returns undefined.
For example:
export const CharactersDetail = (setCharacter) => {
const { id } = useParams();
const characters = useCharacter(id);
if (!characters) {
return <div>Loading...</div>;
}
return (
<div className="container">
<div className="container-section">
<p>{characters.name}</p>
<p>{characters.nickname}</p>
</div>
</div>
)
}

Related

useQuery - can not read properties of undefined

Initial query returns undefined, but all subsequent calls return valid data. If I try to map my response, an error is thrown:
can not read properties of undefined
and the whole app is down. How do i fix this?
import { useQuery } from "#apollo/client";
import { Component } from "react";
import { useState, useEffect } from "react";
import GET_PRODUCTS from '../../server/getProducts';
import './productList.sass';
class ProductList extends Component {
render() {
return (
<RenderProducts />
);
}
}
const RenderProducts = () => {
const { data } = useQuery(GET_PRODUCTS);
console.log(data.products.map(product => console.log(product)));
const products = data.products.map((product) => {
return (
<li className="productList__item">
<img className="productList__item-img" src={product.mainImage.url} alt={product.title} />
<div className="productList__item-descr">
<div className="productList__item-title">{product.title}</div>
<div className="productList__item-price">{product.price} $</div>
</div>
</li>
)
})
return <ul>{products}</ul>
}
export default ProductList;
If I try to map my response, an error is thrown:
and the whole app is down. How do i fix this?
You'll need to render something when the query is in a loading state. You can take advantage of the loading and error properties of useQuery hook. Here's a sample:
const RenderProducts = () => {
const { data, loading, error } = useQuery(GET_PRODUCTS);
if(loading) return <div>loading...</div>
if(error) return <div>cannot render products...something went wrong</div>
// if the query has finished loading products and there's no error,
// You can access data.products
// and write your logic
console.log(data.products.map(product => console.log(product)));
const products = data.products.map((product) => {
return (
<li className="productList__item">
<img className="productList__item-img" src={product.mainImage.url} alt={product.title} />
<div className="productList__item-descr">
<div className="productList__item-title">{product.title}</div>
<div className="productList__item-price">{product.price} $</div>
</div>
</li>
)
})
return <ul>{products}</ul>
}

Is there a way to split a i18n translation?

OK, so I got this component that animating my titles. But know, I want to translate my application with i18n, but problem is, I was using .split() function to make an array of words of my titles, I know that .split() is taking only string, and all I tested return me a JSX Element. So I can't split my pages title.
Is there another way to do it, to keep my translation ?
Here is an exemple of my pages with the component title and what I tried (I also tried with Translation from react-i18next, but same result)
About.tsx
import { useEffect, useState } from "react";
import AnimatedLetters from "../AnimatedLetters/AnimatedLetters"
import { Div } from "../Layout/Layout.elements";
import { useTranslation, Trans } from "react-i18next";
const About = () => {
const [letterClass, setLetterClass] = useState<string>('text-animate');
const { t, i18n } = useTranslation();
useEffect(() => {
setTimeout(() => {
setLetterClass('text-animate-hover')
}, 3000)
}, [])
const getTranslation = (value: string) => {
return <Trans t={t}>{value}</Trans>;
}
return (
<Div>
<div className="container about-page">
<div className="text-zone">
<h1>
<AnimatedLetters
strArray={getTranslation('About.Title').split("")}
idx={15}
letterClass={letterClass}
/>
</h1>
</div>
</Div>
)
}
export default About
Before decide to translate, I was making like that :
<AnimatedLetters
strArray={"About us".split("")}
idx={15}
letterClass={letterClass}
/>
AnimatedLetters.tsx
import { FunctionComponent } from "react"
import { Span } from "./AnimatedLetters.elements"
type Props = {
letterClass: string,
strArray: any[],
idx: number
}
const AnimatedLetters: FunctionComponent<Props> = ({ letterClass, strArray, idx }) => {
return (
<Span>
{
strArray.map((char: string, i: number) => (
<span key={char + i} className={`${letterClass} _${i + idx}`} >
{char}
</span>
))
}
</Span>
)
}
export default AnimatedLetters
OK I found it! I put the solution here in the case of someone else needs it!
In fact there is two ways, don't forget that I needed an array to transmet to my component, so the first was to put directly an array into my translations json files, like:
common.json
{
"Title": ["A","b","o","u","t","","u","s"]
}
But i did not thought that was very clean.
So the second way was to create a method that tooks the key of the json file, to return it directly, like this :
const getTranslate = (value: string) => {
return (`${t(value)}`)
}
Then I stringify it to can use .split() to make an array
const getTranslate = (value: string) => {
return JSON.stringify(`${t(value)}`).split("")
}
The translate and the array worked nicely, but it returned with double quotes. The last thing was to erase it, with a replace and a little regex, and now : everything works like a charm 😁
All the component looks like it now :
About.tsx
import { useEffect, useState } from "react";
import AnimatedLetters from "../AnimatedLetters/AnimatedLetters"
import { Div } from "../Layout/Layout.elements";
import { useTranslation } from 'react-i18next';
const About = () => {
const [letterClass, setLetterClass] = useState('text-animate');
const { t } = useTranslation();
useEffect(() => {
setTimeout(() => {
setLetterClass('text-animate-hover')
}, 3000)
}, [])
const getTranslate = (value: string) => {
return JSON.stringify(`${t(value)}`).replace(/\"/g, "").split("")
}
return (
<Div>
<div className="container about-page">
<div className="text-zone">
<h1>
<AnimatedLetters
strArray={getTranslate('Title')} // <<--- Called here
idx={15}
letterClass={letterClass}
/>
</h1>
</div>
</div>
</Div>
)
}
export default About

Problem to get an image in a blogpost with NextJS and WpGraphQL

I have another question in the project I'm doing, using NextJS to create a site with a blog part in Wordpress, through WPGraphQL, and I need support in a specific part. Let's go...
I managed to pull the highlighted image with almost no problems, I broke my head a bit, but it worked. The result that's functioning is the post excerpt, the code and the query were as follows (in time: image merely to test, it was the first idea that came to my mind, the site is not about Pokemon):
Image with working image in the excerpt, codes below
NextJS code:
import { LastPosts, PostContainer } from "./Styled";
const RecentBlogPosts = ({lastPosts}) => {
const posts = lastPosts;
return (
<LastPosts>
<h1> ÚLTIMAS POSTAGENS </h1>
{posts?.map((post) => {
return (
<PostContainer key={post.slug}>
<img src={post.featuredImage?.node.sourceUrl} alt="" />
<Link href={`/post/${post.slug}`}>
<a>
<h3> { post.title } </h3>
<div dangerouslySetInnerHTML={{ __html: post.excerpt }} />
<button> Saiba mais </button>
</a>
</Link>
</PostContainer>
)
})}
</LastPosts>
)
};
export default RecentBlogPosts;
Query for this part:
export const RECENT_POSTS = `query RecentPosts {
posts(where: {orderby: {field: DATE, order: DESC}}, first: 2) {
nodes {
id
slug
title
excerpt
featuredImage {
node {
sourceUrl
}
}
}
}
}`;
But I tried to pull the same image in the full blogpsot and it wasn't working... It appears when I view the post from the generic WordPress admin template, but not at my NextJS site, which i See through localhost:3000/post/[here would be the post title in slug] that I'm using. The rest is normal, text with all fonts and specifications with styled components, as well as tags, they work without any problem. The following is the same schema: image with result, code and query that I am using, this time for the part where I'm having problems:
Image with blogpost not working, codes below
NextJS code:
import fetcher from "../../lib/fetcher";
import { GET_ALL_POSTS_WITH_SLUG, POST_BY_SLUG } from "../../lib/wordpress/api";
import { useRouter } from "next/router";
import { Reset } from "../../constants/StyledConstants";
import Header from "../../components/Header/Header";
import { BlogArticle, BlogPostContent, TagLinks, TagWrapper } from "./StyledPost";
import Footer from "../../components/Footer/Footer";
const post = ({ postData }) => {
const blogPost = postData.data.post;
console.log(postData);
const tags = postData.data.post.tags.nodes;
const router = useRouter;
if(!router.isFallback && !blogPost?.slug) {
return <div>erro</div>
}
return (
<>
<Reset />
<Header />
<BlogPostContent>
{router.isFallback ? (
<div> Carregando...... </div>
) : (
<div>
<h1> { blogPost.title } </h1>
<img src={post.featuredImage?.node.sourceUrl} alt="imagem não aparece" />
<BlogArticle dangerouslySetInnerHTML={{ __html: blogPost.content }} />
<TagWrapper>
{tags.map((tag) => <TagLinks href={`/tags/${tag.slug}`} key={tag.slug}> { tag.name } </TagLinks>)}
</TagWrapper>
</div>
)}
</BlogPostContent>
<Footer />
</>
)
}
export default post;
export async function getStaticPaths() {
const response = await fetcher(GET_ALL_POSTS_WITH_SLUG);
const allposts = await response.data.posts.nodes;
return {
paths: allposts.map((post) => `/post/${post.slug}`) || [],
fallback: false
};
}
export async function getStaticProps({ params }) {
const variables = {
id: params.slug,
idType: "SLUG"
};
const data = await fetcher(POST_BY_SLUG, { variables })
return {
props: {
postData: data
},
};
}
Query being used:
export const POST_BY_SLUG = `query PostBySlug($id: ID!, $idType: PostIdType!) {
post(id: $id, idType: $idType) {
title
slug
date
content
featuredImage {
node {
sourceUrl
}
}
tags {
nodes {
name
slug
}
}
}
}`;
I tried to use {post.featuredImage?.node.sourceUrl} because, as far as I understand, following the same basis I did for the excerpt in the blogspot, it should work, but I guess I was wrong... I tried to think of other ways to do it to get to the image, without success... Could someone help to point out where I am wrong please? Thank you very much in advance!!

How do I unit test an object of arrays in React Testing Library?

I have a parent component CafeList.js that makes a service call to firebase and returns cafe data as arrays within an object. I pass this entire object as props to Cafe.js where it is mapped over, and properties are destructured out and rendered (eg, the cafe name: name).
I want to write a test to check that the name element is being rendering in the Cafe.js component, but I'm not sure how to access props that are in the form of an array or object. I'm new to RTL so am a bit lost - any suggestions?
CafeList.jsx
import React, { useState,useEffect } from 'react'
import db from '../fbConfig'
import Cafe from './Cafe'
const CafeList = () => {
const [cafes,setCafe] = useState([])
useEffect(() => {
let cafeArray = []
db.collection('cafes')
.get()
.then(snapshot => {
snapshot.forEach(cafe => {
cafeArray.push(cafe)
})
setCafe(cafeArray)
})
},[])
const [...cafeData] = cafes.map((cafe) => {
const { name, photoURL } = cafe.data()
return { name:name,photoURL:photoURL, id:cafe.id}
})
return(
<div className="cafe-container-container">
<h2 className = 'main-subheading'>Reviews</h2>
<Cafe cafes = {cafeData}/>
</div>
)
}
export default CafeList
Cafe.jsx
import React from 'react'
import {Link} from 'react-router-dom'
const Cafe = ({ cafes }) => {
return (
<div className="cafe-grid">
{
cafes.map((cafe) => {
return (
<Link
to={`/cafe-reviews/${cafe.id}`}
style={{ textDecoration: "none", color: "#686262" }}
>
<div className="cafe-container">
<h3>{cafe.name}</h3>
<img src={cafe.photoURL}></img>
</div>
</Link>
)
})
}
</div>
)
}
export default Cafe
cafe.test.js
import { render, screen } from '#testing-library/react'
import Cafe from '../components/Cafe'
test('is cafe name rendering', () =>{
render(<Cafe cafe = {[]}/>)
const nameElement = screen.getByText(//the cafe name as passed by props)
expect(nameElement).toBeInTheDocument
})

Display Search Results from API React/Redux

I have used Redux for state management in my application - with React Hooks. I am able to hit the API and get the response back like below screenshots from my action creator logs:
Here's the code of my component where I'm trying to display the results:
import { useState, useEffect } from "react";
import {Link} from 'react-router-dom';
import styled from "styled-components";
import {GoSearch} from 'react-icons/go';
import LoaderSpinner from "../components/LoaderSpinner";
import MovieItem from "../components/MovieItem";
import { RootStateOrAny, useDispatch, useSelector } from "react-redux";
import {fetchAllShows} from '../actions/movies';
import {searchMovieByTitle} from '../actions/search';
const Home = () => {
const [loading, setLoading] = useState(true);
const [searchString, setSearchString] = useState('');
const [isFromSearchResults, setIsFromSearchResults] = useState(false);
const dispatch = useDispatch();
const movies = useSelector((state: RootStateOrAny) => state.shows)
const searchResults = useSelector((state: RootStateOrAny) => state.shows);
useEffect(()=> {
setLoading(true);
dispatch(fetchAllShows());
setIsFromSearchResults(false);
}, [dispatch])
const handleSearchChange = (e: any) => {
e.preventDefault();
setSearchString(e.target.value);
}
const findMovieByTitle = () => {
dispatch(searchMovieByTitle(searchString));
setIsFromSearchResults(true);
setSearchString('');
}
console.log(isFromSearchResults);
var start, max, paginatedArr=[], pageSize = 25;
for(start = 0; max = movies.length, start < max; start += pageSize) {
paginatedArr = movies.slice(start, start + pageSize);
}
return <HomeContainer>
<div className="search-bar">
<input
type="text"
placeholder="Search for a movie"
value={searchString}
onChange={handleSearchChange}
/>
<div className="search" onClick={findMovieByTitle}>
<GoSearch />
</div>
</div>
<div className="grid">
{
isFromSearchResults
? <div>
{
searchResults.map((result: any, index: number) => {
console.log(result);
// console.log(result.show);
return <Link
key={index}
to={{pathname:`/movies/${result.show.id}`,
state: {movie: result.show}}}
>
<MovieItem show={result.show} />
</Link>
})
// errors out in this return statement. It says the result (from array mapped out above is null) whereas the action creator is able to print out the full search queries
}
</div>
: movies.length == 0
? <div className="loader">
<LoaderSpinner
isLoading={loading}
loadingText="Fetching Movies..."
/>
</div>
// : movies.map((movie:any, index:number) => {
: paginatedArr.map((movie:any, index:number) => {
return <Link
to={{pathname:`/movies/${movie.id}`,
state: {movie: movie}}} key={index}
>
<MovieItem show={movie} />
</Link>
})
}
</div>
</HomeContainer>
}
export default Home;
Here's the code for my action creator to make the search API call:
import {
SEARCH_MOVIE_BY_TITLE,
} from './types';
import ShowsService from '../services/ShowsService';
export const searchMovieByTitle = (title: string) => async (dispatch: any) => {
try {
let response = await ShowsService.searchMovieByTitle(title);
console.log(typeof(response.data));
// console.log(response.data);
const promise = response.data.map((items: any) => {
// console.log(items);
return items;
})
const searchArr = await Promise.all(promise);
console.log(searchArr);
dispatch({type: SEARCH_MOVIE_BY_TITLE, payload: searchArr});
} catch (err) {
console.log(err);
}
}
The problem now lies in trying to parse the search results array and display it as a list of <MyComponent /> with movie passed as props. It shows undefined for each of the items passed. How do I resolve this?
Sample error log attached below:
I looked into your code and it seems your logs were only in the searchMovieByTitle action. It seems there are no checks for the availability of data in the rendered view. Usually when you perform fetching actions, you also check if the data has been fetched successfully before starting to use it.
In your code, this could be done like this:
<div className="grid">
{isFromSearchResults && searchResults?.length ? ( // added check for searchResult here, to make sure it has data
<div>
{
searchResults.map((result: any, index: number) => {
console.log(result);
console.log(result.show?.id); // it is suggested to also check if the id exists in all the result.show objects
return (
<Link
key={index}
to={{
pathname: `/movies/${result.show?.id}`, // optionally add the "?." before id in order to avoid crushes if the id doesn't exist, though this depends on your backend logic; if they must always have an id, no need to add the "?."
state: { movie: result.show },
}}
>
<MovieItem show={result.show} />
</Link>
);
})
// errors out in this return statement. It says the result (from array mapped out above is null) whereas the action creator is able to print out the full search queries
}
</div>
) : movies.length == 0 ? (
<div className="loader">
<LoaderSpinner isLoading={loading} loadingText="Fetching Movies..." />
</div>
) : (
// : movies.map((movie:any, index:number) => {
paginatedArr.map((movie: any, index: number) => {
return (
<Link
to={{ pathname: `/movies/${movie.id}`, state: { movie: movie } }}
key={index}
>
<MovieItem show={movie} />
</Link>
);
})
)}
</div>
If you look closely, I've added a check for the length of the array, making sure the data is in there before starting to map it.
Also added a check for the id, to see if all your result.show objects have it.

Resources