React: Cannot read properties of undefined (reading 'id') - reactjs

I have created react gallery which allows user to upload an image into firebase storage, then firestore reads its urls and displays in my app.
I basicaly loop through an array of images (named docs) created and exported in other file. Now I want to add possibility to delete the file so before I remove it from firebase storage I want to remove the document from firestore database.
Now, I want function deleteDocument to remove document by method deleteDoc provided by firebase.
When I fire it I get an error: Cannot read properties of undefined (reading 'id').
Seems like id is undefined but when I look into Dev Tools I clearly see there is id field (example: id: "REqWGEahJF2iqsdROM"). I have tried to get into component I want to delete by ref.current and ref.doc.id. I've been working on it for last few days and really I got no idea how to fix it :/
When I replace compRef.doc.id with compRef.current I got this error: Uncaught TypeError: n.indexOf is not a function.
import React from "react";
import { projectFirestore } from "../../firebase/config";
import { deleteDoc, doc } from 'firebase/firestore';
import useFirestore from "../../hooks/useFirestore";
const ImageGrid = ({ setSelectedImg }) => {
const compRef = React.useRef();
const { docs } = useFirestore('images');
const deleteDocument = () => {
console.log(compRef);
const docRef = doc(projectFirestore, 'images', compRef.current)
deleteDoc(docRef).then(() => {
console.log(`removed doc ${compRef.current}`)
})
}
return (
<div className="img-grid">
{ docs && docs.map(doc => (
<div className="img-wrap" key={doc.id}
ref={node => compRef.current = node}
onClick={() => setSelectedImg(doc.url)}>
<img src={doc.url} alt="uploaded img" />
<button className="delete"
onClick={deleteDocument}
>Delete</button>
<div>
))}
</div>)
}
Does anyone have an idea how to solve this?
EDIT:
answer to console.log object compRef
current: div.img-wrap
__reactFiber$ars7zzthpl: FiberNode {tag: 5, key: null, elementType:
'div', type: 'div', stateNode: div.img-wrap, …}
__reactProps$ars7zzthpl:
children: (2) [{…}, {…}]
className: "img-wrap"
onClick: () => setSelectedImg(doc.url)
...
id: ""
I think you dont need more properties of that object.
Now I see id field is empty, so is key. Still cannot solve this :(
When I console log docs array (compRef is reference to docs) then I see:
id: "REqWGEahJFQh2iqsdROM"

Related

How to preset prop (logged in user) for React component unit test?

I'm rather new to testing React application, thank you for your time in advance for responding to a newbie question.
So I've been following tutorial on Full Stack Open and came across this challenge about writing tests for React. There is this component Blog which takes some props from App > Blog List > Blog, including one called 'user' which is the returned object from the login function storing username and token etc.
In the Blog's JSX there is a 'remove' button which is shown only to logged in users, controlled by its style determined by a function comparing the username of the original poster of the blog and that of the currently logged in user.
Right now I'm not writing test for username comparison function at all, but it just gets in the way because I can't seem to set a value for 'user' to be passed into the Blog component, and this error was returned during the test:
display: blog.user.username === user.username ? '' : 'none'
^
TypeError: Cannot read properties of undefined (reading 'username')
And here are the codes of the Blog component and the test at current state:
import { useState } from 'react'
const Blog = ({ blog, addLike, deleteBlog, user }) => {
const [showDetails, setShowDetails] = useState(false)
const showWhenDetailsTrue = { display: showDetails ? '' : 'none' }
const toggleDetails = () => {
setShowDetails(!showDetails)
}
const postedBySelf = async () => {
const style = await {
display: blog.user.username === user.username ? '' : 'none',
}
return style
}
return (
<div style={blogStyle}>
<div>
{blog.title} {blog.author}{' '}
<button onClick={toggleDetails}>{showDetails ? 'hide' : 'view'}</button>
</div>
<div style={showWhenDetailsTrue} className="defaultHidden">
<div>{blog.url}</div>
<div>
likes {blog.likes}
<button onClick={() => addLike(blog.id)}>like</button>
</div>
<div>{blog.author}</div>
<button onClick={() => deleteBlog(blog)} style={postedBySelf()}>
remove
</button>
</div>
</div>
)
}
export default Blog
The test file:
import React from 'react'
import '#testing-library/jest-dom/extend-expect'
import { render, screen } from '#testing-library/react'
import Blog from './Blog'
test('renders title and author, but not url or number of likes by default', async () => {
const blog = {
title: 'Blog title',
author: 'Blog author',
url: 'Blog url',
user: {
username: 'mockuser',
},
}
await render(<Blog blog={blog} user={{ username: 'mockuser' }} />)
screen.getByText('Blog title', { exact: false })
screen.getAllByText('Blog author', { exact: false })
const { container } = render(<Blog blog={blog} />)
const div = container.querySelector('.defaultHidden')
expect(div).toHaveStyle('display: none')
})
When the postedBySelf function and associated content are commented out the test is passed. My question is, how can I mock the 'user' object and pass it into the component during the test? I don't understand why it is undefined even if I explicitly declared its value.
Thanks again for your time and appreciate your advice.
Finally spotted my mistake, had to pass in the user in the second rendering of the Blog too.
I wasn't quite sure if I'm missing critical knowledge on this topic but this tutorial explains things very well and helped me spotted the issue in a way. Strongly recommended: https://www.youtube.com/watch?v=OVNjsIto9xM

How to add items to a cart in React

I made an app with React which is displaying 9 products on the screen. I made a button to add them to a cart and I want to make the app functionally just for demo purpose. I used hooks and I don't know what I did wrong.Here are the errors that I'm receiving:
Uncaught TypeError: Cannot read properties of undefined (reading 'map')
at Products (Products.js:8:1)
react-dom.development.js:18523 The above error occurred in the <Products> component:
at Products (http://localhost:3000/main.f01d7322f0afd7419d5f.hot-update.js:30:5)
at Routes (http://localhost:3000/static/js/bundle.js:40456:5)
at Router (http://localhost:3000/static/js/bundle.js:40389:15)
at BrowserRouter (http://localhost:3000/static/js/bundle.js:39198:5)
at App (http://localhost:3000/static/js/bundle.js:44:84)
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
Here is the full project: https://github.com/burNN11/Project
import productItems from './data/Data'
const Products = ({handleAddProduct, productItems}) => {
console.log(productItems)
return (
<div className='produse'>
{productItems.map((item)=> (
<div className='produs' key={item.id}>
<div>
<img className='imagine-produs'
src={item.image}
alt = {item.name}
Edit
I cloned your project, applied my fix and got it to work. The offending code is in App.js, in the handleAddProduct() handler:
const handleAddProduct = product => {
const ProductExist = cartItems.find(item => item.id === product.id);
if (ProductExist) {
setCartItems(
cartItems.map(item =>
item.id === product.id
? { ...ProductExist, quantity: ProductExist.quantity + 1 }
: item
)
);
} else {
setCartItems([...cartItems, {
...product,
quantity: ProductExist.quantity + 1 // <-- error is here
}]);
}
};
In the else block, ProductExist is undefined, as the only way to get into the else block is for it to be undefined. But you try to use ProductExist.quantity + 1. It should be just 1, like this:
const handleAddProduct = product => {
const ProductExist = cartItems.find(item => item.id === product.id);
if (ProductExist) {
setCartItems(
cartItems.map(item =>
item.id === product.id
? { ...ProductExist, quantity: ProductExist.quantity + 1 }
: item
)
);
} else {
setCartItems([...cartItems, {
...product,
quantity: 1 // <-- Change here
}]);
}
};
With this change, the cart feature is working on my machine, on localhost. There is no UI update to show the new items in the cart, but I checked the console and the items are being added correctly, without errors.
Hope this helped.
Original answer
In components/Products.js, you define the <Products/> component as:
import productItems from './data/Data'
const Products = ({handleAddProduct, productItems}) => {
console.log(productItems)
...
You override the productItems import by defining it as a prop. This is because of how JavaScript handles scope. The definition in the function parameters takes precedence.
You should change the lines above to remove the prop, like this:
import productItems from './data/Data'
const Products = ({handleAddProduct}) => {
console.log(productItems)
...
You don't pass the productItems prop in App.js, hence the prop is undefined when you try to map over it.

Why is my data that is coming from apollo server not showing up when I refresh the page?

I am building a simple application using React, Apollo and React Router. This application allows you to create recipes, as well as edit and delete them (your standard CRUD website).
I thought about how I would present my problem, and I figured the best way was visually.
Here is the home page (localhost:3000):
When you click on the title of a recipe, this is what you see (localhost:3000/recipe/15):
If you click the 'create recipe' button on the home page, this is what you see (localhost:3000/create-recipe):
If you click on the delete button on a recipe on the home page, this is what you see (localhost:3000):
If you click on the edit button on a recipe on the home page, this is what you see (localhost:3000/recipe/15/update):
This update form is where the problem begins. As you can see, the form has been filled with the old values of the recipe. Everything is going to plan. But, when I refresh the page, this is what you see:
It's all blank. I am 67% sure this is something to do with the way React renders components or the way I am querying my apollo server. I don't fully understand the process React goes through to render a component.
Here is the code for the UpdateRecipe page (what you've probably been waiting for):
import React, { useState } from "react";
import { Button } from "#chakra-ui/react";
import {
useUpdateRecipeMutation,
useRecipeQuery,
useIngredientsQuery,
useStepsQuery,
} from "../../types/graphql";
import { useNavigate, useParams } from "react-router-dom";
import { SimpleFormControl } from "../../shared/SimpleFormControl";
import { MultiFormControl } from "../../shared/MultiFormControl";
interface UpdateRecipeProps {}
export const UpdateRecipe: React.FC<UpdateRecipeProps> = ({}) => {
let { id: recipeId } = useParams() as { id: string };
const intRecipeId = parseInt(recipeId);
const { data: recipeData } = useRecipeQuery({
variables: { id: intRecipeId },
});
const { data: ingredientsData } = useIngredientsQuery({
variables: { recipeId: intRecipeId },
});
const { data: stepsData } = useStepsQuery({
variables: { recipeId: intRecipeId },
});
const originalTitle = recipeData?.recipe.recipe?.title || "";
const originalDescription = recipeData?.recipe.recipe?.description || "";
const originalIngredients =
ingredientsData?.ingredients?.ingredients?.map((ing) => ing.text) || [];
const originalSteps = stepsData?.steps?.steps?.map((stp) => stp.text) || [];
const [updateRecipe] = useUpdateRecipeMutation();
const navigate = useNavigate();
const [formValues, setFormValues] = useState({
title: originalTitle,
description: originalDescription,
ingredients: originalIngredients,
steps: originalSteps,
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
}}
>
<SimpleFormControl
label="Title"
name="title"
type="text"
placeholder="Triple Chocolate Cake"
value={formValues.title}
onChange={(e) => {
setFormValues({ ...formValues, title: e.target.value });
}}
/>
<SimpleFormControl
label="Description"
name="description"
type="text"
placeholder="A delicious combination of cake and chocolate that's bound to mesmerize your tastebuds!"
value={formValues.description}
onChange={(e) => {
setFormValues({ ...formValues, description: e.target.value });
}}
/>
<MultiFormControl
label="Ingredients"
name="ingredients"
type="text"
placeholder="Eggs"
values={formValues.ingredients}
onAdd={(newValue) => {
setFormValues({
...formValues,
ingredients: [...formValues.ingredients, newValue],
});
}}
onDelete={(_, index) => {
setFormValues({
...formValues,
ingredients: formValues.ingredients.filter(
(__, idx) => idx !== index
),
});
}}
/>
<MultiFormControl
ordered
label="Steps"
name="steps"
type="text"
placeholder="Pour batter into cake tray"
color="orange.100"
values={formValues.steps}
onAdd={(newValue) => {
setFormValues({
...formValues,
steps: [...formValues.steps, newValue],
});
}}
onDelete={(_, index) => {
setFormValues({
...formValues,
steps: formValues.steps.filter((__, idx) => idx !== index),
});
}}
/>
<Button type="submit">Update Recipe</Button>
</form>
);
};
I'll try to explain it as best as I can.
First I get the id parameter from the url. With this id, I grab the corresponding recipe, its ingredients and its steps.
Next I put the title of the recipe, the description of the recipe, the ingredients of the recipe and the steps into four variables: originalTitle, originalDescription, originalIngredients and originalSteps, respectively.
Next I set up some state with useState(), called formValues. It looks like this:
{
title: originalTitle,
description: originalDescription,
ingredients: originalIngredients,
steps: originalSteps,
}
Finally, I return a form which contains 4 component:
The first component is a SimpleFormControl and it is for the title. Notice how I set the value prop of this component to formValues.title.
The second component is also a SimpleFormControl and it is for the description, which has a value prop set to formValues.description.
The third component is a MultiFormControl and it's for the ingredients. This component has its value props set to formValues.ingredients.
The fourth component is also aMultiFormControl and it's for the steps. This component has its value props set to formValues.steps.
Let me know if you need to see the code for these two components.
Note:
When I come to the UpdateRecipe page via the home page, it works perfectly. As soon as I refresh the UpdateRecipe page, the originalTitle, originalDescripion, originalIngredients and originalSteps are either empty strings or empty arrays. This is due to the || operator attached to each variable.
Thanks in advance for any feedback and help.
Let me know if you need anything.
The problem is that you are using one hook useRecipeQuery that will return data at some point in the future and you have a second hook useState for your form that relies on this data. This means that when React will render this component the useRecipeQuery will return no data (since it's still fetching) so the useState hook used for your form is initialized with empty data. Once useRecipeQuery is done fetching it will reevaluate this code, but that doesn't have any effect on the useState hook for your form, since it's already initialized and has internally cached its state. The reason why it's working for you in one scenario, but not in the other, is that in one scenario your useRecipeQuery immediately returns the data available from cache, whereas in the other it needs to do the actual fetch to get it.
What is the solution?
Assume you don't have the data available for your form to properly render when you first load this component. So initialize your form with some acceptable empty state.
Use useEffect to wire your hooks, so that when useRecipeQuery finishes loading its data, it'll update your form state accordingly.
const { loading, data: recipeData } = useRecipeQuery({
variables: { id: intRecipeId },
});
const [formValues, setFormValues] = useState({
title: "",
description: "",
ingredients: [],
steps: [],
});
useEffect(() => {
if (!loading && recipeData ) {
setFormValues({
title: recipeData?.recipe.recipe?.title,
description: recipeData?.recipe.recipe?.description,
ingredients: ingredientsData?.ingredients?.ingredients?.map((ing) => ing.text),
steps: stepsData?.steps?.steps?.map((stp) => stp.text),
});
}
}, [loading, recipeData ]);

Gatsby fetching Wordpress Custom Post Types and create pages

I read the documentation and tried several tutorials, but I am stuck on fetching custom post types with GatsbyJS.
I tried several approaches, but none of them are working as expected. I always receive a 404.
This is a part of the snippet I am using, which works fine with pages and posts, but not with a custom post type.
The projects pages should be created under a project subfolder/path. Like: example.com/project/my-first-project
The part of the gatsby-node.js looks like that:
const createSinglePages = async ({ posts, gatsbyUtilities }) =>
Promise.all(
posts.map(({ previous, post, next }) =>
// createPage is an action passed to createPages
// See https://www.gatsbyjs.com/docs/actions#createPage for more info
gatsbyUtilities.actions.createPage({
// Use the WordPress uri as the Gatsby page path
// This is a good idea so that internal links and menus work 👍
path: post.uri,
// use the blog post template as the page component
component: path.resolve(
`./src/templates/${post.__typename.replace(`Wp`, ``)}.js`
),
// `context` is available in the template as a prop and
// as a variable in GraphQL.
context: {
// we need to add the post id here
// so our blog post template knows which blog post
// the current page is (when you open it in a browser)
id: post.id,
// We also use the next and previous id's to query them and add links!
previousPostId: previous ? previous.id : null,
nextPostId: next ? next.id : null,
},
})
)
);
The src/template/project.js file looks like this:
import React from "react";
import { Link, graphql } from "gatsby";
import Image from "gatsby-image";
import parse from "html-react-parser";
import Layout from "../components/Layout";
import Seo from "../components/Seo";
const ProjectTemplate = ({ data: { post } }) => {
const featuredImage = {
fluid: post.featuredImage?.node?.localFile?.childImageSharp?.fluid,
alt: post.featuredImage?.node?.alt || ``,
};
return (
<Layout>
<Seo title={post.title} description={post.excerpt} />
<article
className="blog-post"
itemScope
itemType="http://schema.org/Article"
>
<header>
<h1 itemProp="headline">{parse(post.title)}</h1>
<p>{post.date}</p>
{/* if we have a featured image for this post let's display it */}
{featuredImage?.fluid && (
<Image
fluid={featuredImage.fluid}
alt={featuredImage.alt}
style={{ marginBottom: 50 }}
/>
)}
</header>
{!!post.content && (
<section itemProp="articleBody">{parse(post.content)}</section>
)}
</article>
</Layout>
);
};
export default ProjectTemplate;
export const pageQuery = graphql`
query ProjectById(
# these variables are passed in via createPage.pageContext in gatsby-node.js
$id: String!
) {
# selecting the current post by id
post: wpProject(id: { eq: $id }) {
id
content
title
date(formatString: "MMMM DD, YYYY")
featuredImage {
node {
altText
localFile {
childImageSharp {
fluid(maxWidth: 1000, quality: 100) {
...GatsbyImageSharpFluid_tracedSVG
}
}
}
}
}
}
}
`;
Is the Gatsby API creating a subfolder automatically, or do I need to define that somewhere for each post type?
Any help appreciated!
You define the "subfolder" under the path field in:
gatsbyUtilities.actions.createPage({
path: post.uri,
component: path.resolve(
`./src/templates/${post.__typename.replace(`Wp`, ``)}.js`
),
context: {
id: post.id,
previousPostId: previous ? previous.id : null,
nextPostId: next ? next.id : null,
},
})
You just need to do something like:
path: `projects/${post.id}`
Check the slashes and trailing slashes here.
You cna replace projects for your dynamic project type if you fetch that information for a more automatic approach (assuming it's post.__typename).
In order to use Custom Post Types with WPGraphQL, you must configure the Post Type to show_in_graphql using the following field:
show_in_graphql : true
While Registering a new Custom Post Type
This is an example of registering a new "docs" post_type and enabling GraphQL Support.
add_action( 'init', function() {
register_post_type( 'docs', [
'show_ui' => true,
'labels' => [
//#see https://developer.wordpress.org/themes/functionality/internationalization/
'menu_name' => __( 'Docs', 'your-textdomain' ),
],
'show_in_graphql' => true,
'hierarchical' => true,
'graphql_single_name' => 'document',
'graphql_plural_name' => 'documents',
] );
} );

Component gives type error if it includes image element

The Project
I have a project (React, Typescript, React useContext) that calls an api to fetch information about the episodes of a series, then display the information as cards.
Current status
Last time I ran the project, it worked, I deployed it to Heroku, it worked. One month later, after no changes, it doesn´t work either on my local or on heroku, they throw the same error.
Errors:
Uncaught TypeError: Cannot read property 'medium' of null
at EpisodesList.tsx:21
EpisodesList.tsx:21 Uncaught (in promise) TypeError: Cannot read property 'medium' of null
at EpisodesList.tsx:21
The Episodelists component
const EpisodesList = (props: any): JSX.Element => {
const { episodes, toggleFavAction, favourites, store } = props;
const { state, dispatch } = store;
return episodes.map((episode: Episode) => {
return (
<div key={episode.id} className="episode-box">
<section>
<img
src={episode.image.medium}
/>
<div>{episode.name}</div>
<div>
Season: {episode.season} Number: {episode.number}
</div>
</section>
</div>
);
});
};
export default EpisodesList;
The Home page that uses the component
const Home = () => {
const { state, dispatch } = React.useContext(Store);
useEffect(() => {
state.episodes.length === 0 && fetchDataAction(dispatch);
});
const props: EpisodeProps = {
episodes: state.episodes,
store: { state, dispatch },
toggleFavAction,
favourites: state.favourites,
};
return (
<section className="episode-layout">
{console.log("props in home return is:", props)}
<EpisodesList {...props} />
</section>
);
};
Console log + what I tried
Maybe the issue is related to this --> the console.log in the return part shows:
props in home return is: {episodes: Array(0), store: {…}}
props in home return is: {episodes: Array(42), store: {…}}
The weird thing is, if I remove the image element from the Episodelist component, it works without errors, all the data is there (I can see it in the console.log, even the image.medium).
Any ideas why I am suddenly getting these errors and how I can reinsert my image element?
All you have to do is use conditional rendering for the image.
<img src={episode.image && episode.image.medium} /> or
<img src={episode.image ? episode.image.medium : "some default image"} />
This happens because on initial render the "image" property of "episode" is null and you are trying to access it like that "episode.null.medium" so you need to add a condition that will try to access the "medium" property only when "episode.image" is not null.

Resources