Losing Context Data on Page Refresh - javascript

I have a component that displays recipe data from an API, when the user clicks the button I am sending the recipe item ID up to Context with a recipeCtx.addRecipeID(id).
this button click also navigates the user to a page to display the recipe info.
const RecipeGridItem = ({title, image, id}) => {
const recipeCtx = useContext(RecipeContext);
const history = useHistory();
const recipeInfoSend = () => {
recipeCtx.addRecipeID(id);
history.push(`/info/${title.replace(/ /g,"-")}`);
}
return (
<div className={classes['grid-item']}>
<div className={classes['grid-item-inner']}>
<div className={classes['grid-front']}>
<img src={image} alt="" />
<h3>{title}</h3>
</div>
<div className={classes['grid-back']}>
{/* <p>{summary}</p> */}
<Button onClick={recipeInfoSend} btnText={'More Info'}/>
</div>
</div>
</div>
)
}
export default RecipeGridItem;
I then save the id state in my context provider
const [recipeId, setRecipeId] = useState('');
const addRecipeIDHandler = (id) => {
setRecipeId(id);
}
const recipeProviderValue = {
recipeId: recipeId,
};
Then I call the recipe ID down from context in my RecipeInfoPage.js file to make the necessary API call, this is the page the user is navigated to after clicking the button.
The data displays on the first page load, but if I refresh the page, the id value seems to get lost and all of my data from the api request is gone, leaving me with a blank page
import React, {useState, useContext, useEffect} from 'react';
import Container from '../Layout/Container';
import RecipeContext from '../../store/recipe-context';
const RecipeInfoPage = () => {
const recipeCtx = useContext(RecipeContext);
const recipeId = recipeCtx.recipeId;
const [recipeInfo, setRecipeInfo] = useState({});
const getRecipeInfo = () => {
fetch(`https://api.spoonacular.com/recipes/${recipeId}/information?apiKey=${process.env.REACT_APP_API_KEY}`)
.then(res => res.json())
.then(data => setRecipeInfo(data));
}
//Grid Recipes
useEffect(() => {
getRecipeInfo();
}, [recipeId]);
return (
<Container>
<h1>{recipeInfo.title}</h1>
<p dangerouslySetInnerHTML={{__html: recipeInfo.summary }}></p>
</Container>
)
}
export default RecipeInfoPage;

You should use params or query string instead of managing the id yourself.
history.push(`/info/${recipeId}`);
You will need to handle that route:
<Route path={'/info/:recipeId'}>
<YourComponent />
</Route>
And in YourComponent you can get the recipeId with useParams hook. And you can fetch the data with recipeId in useEffect

I am not very sure about this but isn't the command something along the lines of localStorage['myKey'] = 'somestring'; // only strings

Related

LocalStorage keeps changing my key-pair instead of pushing to the array

I want to add to the array a new object everytime I click at a card, but when I do so it changes the last key-pair to the new one and it doesnt add it. I chose this method of updating the state since I saw it is more popular than the one with the push.
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { AiOutlineHeart, AiFillHeart } from "react-icons/ai";
import styles from "./MovieCard.module.css";
const imagePrefixUrl = "http://image.tmdb.org/t/p/w500";
const MovieCard = (props) => {
const [items, setItems] = useState(
JSON.parse(localStorage.getItem("favorites")) || []
);
const [liked, setLiked] = useState(false);
const movie = props?.movie;
const addFavoriteHandler = (movie) => {
setItems((data) => [...data, movie]);
};
useEffect(() => {
localStorage.setItem("favorites", JSON.stringify(items));
}, [items]);
return (
<div className={styles.container}>
{liked ? (
<button className={styles.heartIcon} onClick={() => setLiked(false)}>
<AiFillHeart />
</button>
) : (
<button
className={styles.heartIcon}
onClick={() => addFavoriteHandler(movie)}
>
<AiOutlineHeart />
</button>
)}
<Link to={`/movie/${movie.id}`} title={movie?.title}>
<img src={`${imagePrefixUrl}${movie?.backdrop_path}`} />
<p>{movie?.title}</p>
</Link>
</div>
);
};
export default MovieCard;
I am assuming from the component name MovieCard, that your app would have multiple instances of this component under a parent component (assumed to be MovieCardList).
A solution to your issue would be to lift the state and addFavoriteHandler
const [items, setItems] = useState(
JSON.parse(localStorage.getItem("favorites")) || []
);
to the parent component MovieCardList and pass the handler addFavoriteHandler as a prop to each MovieCard.
This would ensure that you have a single point for updating your localStorage key favorites and it would not be overridden by new update.
The reason for the override issue you are experiencing is that each card has an instance of items and it does not fetch the latest value of favorites from the localStorage before updating it, meaning it would always override the favorites in localStorage as per the current code.

how do I render only one product intstead of all using .map?

I am making an ecommerce platform project but I need some help. on my main page I have the products, I used .map to iterate through all of them to render them all. I want it to be where if you click on a button on a product it will take you to that products page with more details. My only problem is I don't know how to render one product and not multiple. here is the code I am trying to work on
import React from 'react';
import { Card, CardMedia, CardContent, CardActions, Typography, IconButton, Button } from '#material-ui/core';
import useStyles from './styles';
const ProductPage = ({ products, onAddToCart }) => {
const classes = useStyles();
{products.map((product) => (
console.log(product)
))}
return (
<Card className={classes.root}>
</Card>
);
};
export default ProductPage;
basically it just maps through all of the products and console logs all of the products. But I only want it to console.log the product that I clicked on. How do I do that?
I don't really understand what you're looking at.
If you're looking at a single product, then you should only send the product you're concerned about and only render the product page when you click that product.
If you're simply trying to list all the products, you should return a Product component that has an onClick handler that handles your clicks.
Some something like this:
products.map(product => <Product onClick={console.log} {/* whatever other product props here*/} />
const Product = props => {
// fill in your product details here...
}
But if you're not doing this, I think you're conceptually confused. Either way, I think you should have a list of products that have a click handler and then maybe render a different page whenever you click a specific product.
To do this, you'll have to send the Product ID to the component. You can use React Router to send Product ID in URL as params.
In your Routes, Add this to send ID in the URL
<Route path="/products/:id">
<Product />
</Route>
Once you have the Product ID, You can create an API to fetch Product Details or You can filter the Products array.
On Products Page:
const [product, setProduct] = React.useState();
let { id } = useParams();
const getProductDetails = async (id) => {
let url = `${apiUrl}/api/product/getDetailsbyId/${id}`;
const response = await axios.get(url);
setProduct(response.data.data[0]);
};
const filterProduct = (id) => {
let product = props.products.filter(product => product.id === id);
setProduct(product);
};
useEffect(() => {
getProductDetails(id); // Or use filterProduct Function
}, []);
import React from 'react';
import { Card, CardMedia, CardContent, CardActions, Typography, IconButton, Button } from '#material-ui/core';
import useStyles from './styles';
const ProductPage = ({ products, onAddToCart }) => {
const [selectedProduct, setSelectedProduct] = useState(null)
const classes = useStyles();
return (
<>
{selectedProduct ? (
<Card className={classes.root}>
{selectedProduct?.name}
</Card>
):(
products.map((product) => (
<div onClick={() => setSelectedProduct(product)}>
{product?.name}
</div>
))
)
}
</>
);
};
export default ProductPage;
But better is on click a product jump into a new page by passing id & fetch to show details of product in that new page with that id

React useState conversion

I made a static webpage app that I have been slowly converting to React (MERN stack) to make it more dynamic/so I won't have to configure each and every HTML document. It's a product configurator that uses Google's model-viewer.
I'm fairly new to using a full-stack workflow but have found it pretty fun so far! I am having trouble however understanding on how to convert some of my vanilla JS to work within React. This particular script will change a source/3D model when a user clicks on a button. Below is a code snipit of what I have working currently on a static webpage.
import {useEffect, useState} from "react";
import {useSelector, useDispatch} from "react-redux";
// Actions
import {getProductDetails} from "../redux/actions/productActions";
const ProductScreen = ({match}) => {
const dispatch = useDispatch();
const [currentSrc, setCurrentSrc] = useState()
const [srcOptions, setSrcOptions] = useState()
const productDetails = useSelector((state) => state.getProductDetails);
const {loading, error, product} = productDetails;
useEffect(() => {
if (product && match.params.id !== product._id) {
dispatch(getProductDetails(match.params.id));
setCurrentSrc(product.src);
setSrcOptions(product.srcList);
}
}, [dispatch, match, product]);
return (
<div className="productcreen">
{loading ? (
<h2> Loading...</h2>) : error ? (
<h2>{error}</h2>) : (
<>
<div className='sizebuttons'>
{srcOptions.map((src) => (
<button onClick={() => setCurrentSrc(src)}>{src}{product.size}</button>
))}
{srcOptions.map((src) => (
<button onClick={() => setCurrentSrc(src)}>{src2}{product.size2}</button>
))}
{srcOptions.map((src) => (
<button onClick={() => setCurrentSrc(src)}>{src3}{product.size3}</button>
))}
</div>
<div className="productscreen__right">
<model-viewer id="model-viewer" src={currentSrc} alt={product.name} ar ar-modes="scene-viewer quick-look" ar-placement="floor" shadow-intensity="1" camera-controls min-camera-orbit={product.mincameraorbit} max-camera-orbit={product.maxcameraorbit} interaction-prompt="none">
<button slot="ar-button" className="ar-button">
View in your space
</button>
</model-viewer>
</div>
</> )} )};
Here is what the DB looks like:
The "product.size" is being pulled in from MongoDB, and I'm wondering if I could just swap models with: "product.src","product.src2","product.src3" (which is also defined in the DB already) I'm assuming I need to use useState in order to switch the source, but I am unsure. Any help would be greatly appreciated! If you'd like to see the static webpage of what I'm trying to accomplish, you can view it here if that helps at all.
Here is how the products are being exported in redux:
import * as actionTypes from '../constants/productConstants';
import axios from 'axios';
export const getProductDetails = (id) => async(dispatch) => {
try {dispatch({type: actionTypes.GET_PRODUCT_DETAILS_REQUEST});
const {data} = await axios.get(`/api/products/${id}`);
dispatch({
type: actionTypes.GET_PRODUCT_DETAILS_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: actionTypes.GET_PRODUCT_DETAILS_FAIL,
payload: error.response && error.response.data.message ?
error.response.data.message :
error.message,
});
}
};
You can use the useState hook from React to create the state. After you fetch your product from the DB you can set the initial value with setCurrentSrc or if it's coming from props, you can set the initial value like this: const [currentSrc, setCurrentSrc] = useState(props.product.src).
Then change the src of your model-viewer to use the state value so it will automatically rerender if the state value changes. Lastly, add onClick handlers to some buttons with the setCurrentSrc function to change the state.
const ProductViewer = (props) => {
const [currentSrc, setCurrentSrc] = useState()
const [srcOptions, setSrcOptions] = useState()
const dispatch = useDispatch()
const { loading, error, product } = useSelector(
(state) => state.getProductDetails
)
useEffect(() => {
if (product && match.params.id !== product._id) {
dispatch(getProductDetails(match.params.id))
}
}, [dispatch, match, product])
// update src and srcOptions when product changes
useEffect(() => {
setCurrentSrc(product.src)
setSrcOptions(product.srcList)
}, [product])
return (
<div className="productscreen__right">
<model-viewer
id="model-viewer"
src={currentSrc}
alt={product.name}
ar
ar-modes="scene-viewer quick-look"
ar-placement="floor"
shadow-intensity="1"
camera-controls
min-camera-orbit={product.mincameraorbit}
max-camera-orbit={product.maxcameraorbit}
interaction-prompt="none"
>
<button slot="ar-button" className="ar-button">
View in your space
</button>
{/* add your switch buttons somewhere... */}
{/* this assumes you have a srcList, but this could also be hardcoded */}
{srcOptions.map((src) => (
<buttton onClick={() => setCurrentSrc(src)}>{src}</buttton>
))}
</model-viewer>
</div>
)
}

Can't trigger a search function for movie API project because useState is in a different component

my problem is that I have two different components belonging to my App.js project. It's a movie database where I have a list of movies on the front page and I can search for other movies using the search bar. Since I have the search.js and movie.js ( component where i fetch api data and display), the search.js will not trigger as it cant pinpoint what needs to change. Basically my problem is that on submit, nothing changes.
search.js code:
import { useState } from 'react';
import React from 'react';
// search API used to search through database
const searchUrl = "https://api.themoviedb.org/3/search/movie?api_key=d62e1adb9803081c0be5a74ca826bdbd&query="
const Search = ({ }) => {
const [movies, setMovies] = useState([]);
const [search, setSearch] = useState("");
// Search form that fetches search API and returns results
const submitForm = (e) => {
e.preventDefault();
// API used to search for any movie in the database
fetch(searchUrl + search)
.then(res => res.json())
.then(data => {
setMovies(data.results);
})
setSearch("");}
// user search input
const searchQuery = (e) => {
setSearch(e.target.value)
}
return (
<form onSubmit={submitForm}>
<i class="fas fa-search"></i>
<label className="sr-only" htmlFor="searchMovie">Search for a movie</label>
<input
className="search"
type="search"
placeholder="Search for a movie.."
value={search}
onChange={searchQuery}
/>
</form>
)
}
export default Search;
and my movie.js
import { Link } from 'react-router-dom';
import { useState, useEffect } from "react";
const images = "https://image.tmdb.org/t/p/w500/";
// main API used to display trending page
const apiUrl = `https://api.themoviedb.org/3/movie/now_playing?api_key=d62e1adb9803081c0be5a74ca826bdbd&page=`;
const Movie = ( {
}) => {
const [movies, setMovies] = useState([]);
useEffect(() => {
fetch(apiUrl)
.then((res) => res.json())
.then((data)=> {
setMovies(data.results)
})
}, []);
return (
<section className="movieslist">
{movies.length > 0 ? movies.map((movie) => {
return (
<Link to={`/movie/${movie.id}`}>
<div className="moviePoster">
<img src={movie.poster_path ? `${images}${movie.poster_path}` : "https://www.movienewz.com/img/films/poster-holder.jpg"} alt={movie.title} />
<div className="movieInfo">
<h2>{movie.title}</h2>
<p className="voteStyle">Rating: {movie.voteAverage}</p>
<p className="release">Release Date: {movie.release}</p>
<p className="summary">{movie.overview}</p>
<p className="key">{movie.id}</p>
</div>
</div>
</Link>
);
}): <p class="noResults">No results found. Please try again?</p>}
</section>
)
}
export default Movie;
If I understand the expected behavior correctly, you're trying to update the movies state in movies.js from the search.js.
You are updating two different states of two different components that have no relationship with themselves and that is why nothing is happening on submit.
What you'll need is a parent component (for example home.js) that holds search and movies component as children and holds the movies state. The child components should use and update the parent's movie state.
import Movies from "./movies";
import Search from "./search";
const Home = ()=>{
const [movies, setMovies] = useState([]);
// some other code
return (
<>
<Search onSearh={setMovies} />
<Movies movies={movies} onMovies={setMovies}/>
</>);
}
and your movies.js and search.js should consume these props
import { useState } from 'react';
import React from 'react';
// search API used to search through database
const searchUrl = "https://api.themoviedb.org/3/search/movie?api_key=d62e1adb9803081c0be5a74ca826bdbd&query="
const Search = ({ onSearch }) => {
const [search, setSearch] = useState("");
// Search form that fetches search API and returns results
const submitForm = (e) => {
e.preventDefault();
// API used to search for any movie in the database
fetch(searchUrl + search)
.then(res => res.json())
.then(data => {
onSearch(data.results);
})
setSearch("");}
...
import { Link } from 'react-router-dom';
import { useState, useEffect } from "react";
const images = "https://image.tmdb.org/t/p/w500/";
// main API used to display trending page
const apiUrl = `https://api.themoviedb.org/3/movie/now_playing?api_key=d62e1adb9803081c0be5a74ca826bdbd&page=`;
const Movie = ( {movies, onMovies}) => {
useEffect(() => {
fetch(apiUrl)
.then((res) => res.json())
.then((data)=> {
onMovies(data.results)
})
}, []);
...

Pass a function from a component to another (ReactJS)

I'm building an application focused on showing a user's github repositories and information. In a "Section" component I fetch these repositories and display them on the screen.
In the other component "Menu" I wanted it to count these repositories and display them. Should I use props in this case?
Section Component
import React, { useState } from 'react'
import axios from 'axios'
import { Square, Wrapper, Input, Button } from './Section.styled'
export default function Section() {
const [username, setUsername] = useState("");
const [loading, setLoading] = useState(false);
const [repos, setRepos] = useState([]);
const searchRepos = () => {
setLoading(true);
axios({
method: "get",
url: `https://api.github.com/users/${username}/repos`,
}).then(res => {
setLoading(false);
setRepos(res.data);
})
}
const handleSubmit = (e) => {
e.preventDefault();
searchRepos()
}
const renderRepo = (repo)=>{
return(
<Square>
{repo.name}
</Square>
)
}
return (
<>
<Wrapper>
<Input
placeholder="Usuário"
value={username}
onChange={e => { setUsername(e.target.value) }}
/>
<Button
onClick={handleSubmit}
type="submit">
{loading ? "Buscando..." : "Buscar"}
</Button>
{repos.map(renderRepo)}
</Wrapper>
</>
)
}
Menu Component
import React from "react";
import { bool } from "prop-types";
import { StyledMenu } from "./Menu.styled";
const Menu = ({ open, ...props }) => {
const isHidden = open ? true : false;
const tabIndex = isHidden ? 0 : -1;
return (
<>
<StyledMenu open={open} aria-hidden={!isHidden} {...props}>
<a href="/" tabIndex={tabIndex}>
Repositories:
</a>
<a href="/" tabIndex={tabIndex}>
Followeres:
</a>
<a href="/" tabIndex={tabIndex}>
Following:
</a>
</StyledMenu>
</>
);
};
Menu.propTypes = {
open: bool.isRequired,
};
export default Menu;
These solutions could be possible in this case when we have received data in one component and want it to appear in another component.
Pass the function searchRepos as a prop from the parent component of both section and menu component to section component, call the function from section, this will set data in parent, and send data to the menu component as props, i.e. called Lifting up the state.
If the components are far away (deeply nested or have unrelated parent, branch) you can simply make use of context store
3. Last way is to store the data of called API of the component section in browser local storage and use it in menu component. (NOT RECOMMENDED)

Categories