I'm creating a blog with ReactJS. I have a data component that only contains my data for each articles. I'm able to show all my list of articles on my blog page. The problems comes when I click on an article. I'm able to get the ID in the URL and other params and show it in my article.
The problem: I'd like to not pass all the params into the URL...
I just want to pass ID into the URL, and say to my app:
Get all the values of the row that contain this ID from this data file.
Here the code I tried to fix it (it show a blank page):
import React, { useEffect, useState } from 'react'
import imgBien1 from '../../images/imgBien1.JPG'
import { ColumnSection, Section, SectionInterne, Column2, ContentWrapper2, Img, Column1,ContentWrapper, TopLine, Heading, Subtitle } from './stockUnique'
import {useParams} from 'react-router-dom';
import { posts } from '../Data/data'
const StockUnique = () => {
const { id } = useParams();
const [blog, setBlog] = useState(null);
useEffect(() => {
let blog = posts.find((blog) => blog.id === parseInt(id));
if (blog) {
setBlog(blog);
}
}, []);
return (
<Section >
<ColumnSection>
<SectionInterne>
<Column2>
<ContentWrapper2>
<Img src={imgBien1} alt='ok' />
</ContentWrapper2>
</Column2>
<Column1>
<ContentWrapper>
<TopLine>{id}</TopLine>
<Heading>{blog.title}</Heading>
<Subtitle>{blog.content}</Subtitle>
</ContentWrapper>
</Column1>
</SectionInterne>
</ColumnSection>
</Section>
)
}
export default StockUnique
My data list:
export const posts = [
{ id: 1, title: 'Hello World', content: 'Mon article 1', typeDeBien: "maison" },
{ id: 2, title: 'Bravo', content: 'Mon article2', typeDeBien: "terrain" },
{ id: 3, title: 'Charlie', content: 'Mon article 3', typeDeBien: "appartement" }
];
edit: Navigation code:
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'
import React from 'react'
import StockPage from './pages/stock';
import UniqueStockPage from './pages/unique'
const App = () => {
return (
<Router>
<Routes>
<Route path="/stock" element={<StockPage/>} exact />
<Route
path="/unique/:id"
// path="/unique/:id/:title/:content"
element={<UniqueStockPage/>} exact
/>
</Routes>
</Router>
)
}
export default App;
You might have to check if id has a value
useEffect(() => {
if(id){
let blog = posts.find((blog) => blog.id === parseInt(id));
if (blog) {
setBlog(blog);
}
}
}, [id]);
Given route:
<Route path="/unique/:id" element={<UniqueStockPage />} />
Then the UniqueStockPage has the id value it needs to search the posts array for the matching item.
There's no need for state here, just use the posts array and the id route path param to access the matching element and render directly. Don't forget to handle the case where no matching element is found.
Example:
const StockUnique = () => {
const { id } = useParams();
const blog = posts.find((blog) => String(blog.id) === id);
if (!blog) {
return <div>No Blog Found</div>
}
return (
<Section >
<ColumnSection>
<SectionInterne>
<Column2>
<ContentWrapper2>
<Img src={imgBien1} alt='ok' />
</ContentWrapper2>
</Column2>
<Column1>
<ContentWrapper>
<TopLine>{id}</TopLine>
<Heading>{blog.title}</Heading>
<Subtitle>{blog.content}</Subtitle>
</ContentWrapper>
</Column1>
</SectionInterne>
</ColumnSection>
</Section>
)
}
If you want to avoid searching the posts array each render cycle then use the useMemo hook to memoize the blog value.
Example:
const blog = useMemo(() => {
return posts.find((blog) => String(blog.id) === id);
}, [id]);
Related
I have a shopping site built with React and React Router, There is a main shopping page and a product page for each product, the product page has a next page button that calls
const handleNext = () => {
navigate(`/shop/product/${product.id + 1}`, { replace: true })
}
The URL changes but the page does not rerender and stays on the same product, if I refresh the page manually the page does update.
I have also tried using and it does the same thing
<Link to=`/shop/product/${product.id + 1}`>Next</Link>
Although when I reroute to '/home' or '/contact' it works.
RouteSwitch.JS
import { BrowserRouter, Routes, Route } from "react-router-dom";
import HomePage from "./components/Homepage/Homepage";
import ShoppingPage from "./components/ShoppingPage/ShoppingPage";
import Nav from "./components/Nav/Nav";
import Contact from "./components/Contact/Contact";
import ProductPage from "./components/ProductPage/ProductPage";
import Checkout from "./components/Checkout/Checkout";
const RouteSwitch = (props) => {
const { cart, setCart } = props.props;
return (
<BrowserRouter>
<Nav cart={cart} />
<Routes>
<Route exact path="/" element={<HomePage />} />
<Route path="/shop" element={<ShoppingPage />} />
<Route
exact
path="/shop/product/:id"
element={<ProductPage props={{ setCart, cart }} />}
/>
<Route path="/contact" element={<Contact />} />
<Route
path="/checkout"
element={<Checkout cart={cart} setCart={setCart} />}
/>
</Routes>
</BrowserRouter>
);
};
export default RouteSwitch;
ProductPage.js
import { useLocation, useParams, useNavigate, Link } from "react-router-dom";
import { useEffect, useState } from "react";
import './ProductPage.css'
import data from "../toy-api/data"
import ProductMenu from "../ProductMenu/ProductMenu"
const ProductPage = (props) => {
const { setCart, cart } = props.props;
const getProdByID = (id) => data.find((item) => item.id === parseInt(id));
const navigate = useNavigate();
const params = useParams();
const location = useLocation();
const [product, setProduct] = useState({});
const { fromShopRoute = false, prodProps = errorObj } = location.state || {};
// Use Effect that runs when the COMP mounts
useEffect((e) => {
let _product = getProdByID(params.id);
if (_product !== undefined) {
setProduct(_product);
}
// If the search returns undefined it will set the Product
// to an errorObj and reroute the user to shop page
else {
setProduct(errorObj);
navigate("/shop");
}
}
}, []);
const handleNext = () => {
navigate(`/shop/product/${product.id + 1}`, { replace: true })
}
return (
<div>
<div className="product-page-container">
<div className="product-header">
<h1>{product.name}</h1>
<ProductMenu
setCart={setCart}
product={product}
setProduct={setProduct}
cart={cart}
>
</ProductMenu>
</div>
<div className="img-container">
<img
className="product-img"
loading="lazy"
src={product.src}
alt={product.name}
/>
<div onClick={handleNext} className="slide-btn ">
Next
</div>
</div>
</div>
</div >
);
};
export default ProductPage;
I tried all sorts of "UseNavigate(0)" and other methods with useEffect watching the params.id but I have found that they are messy solutions that aren't giving me my required goal.
The ProductPage remains mounted when the "/shop/product/:id" route path is matched and only the id path parameter is changing.
The useEffect hook for fetching data depends on the id route path param.
const { id } = useParams();
// Use Effect that runs on initial render and when id updates
useEffect(() => {
let _product = getProdByID(id);
if (_product !== undefined) {
setProduct(_product);
} else {
// If the search returns undefined it will set the Product
// to an errorObj and reroute the user to shop page
setProduct(errorObj);
navigate("/shop");
}
}, [id]); // <-- add id dependency
I have been trying to set a search filter form. I am getting data from API (an array of cake objects with "id", "cake_name", "category" etc properties), these get displayed properly. But somehow my search function is not working? It should allow the user to input a name of a cake which then would be filtered through the cakes available and only the searched one(s) would be displayed.
I am getting this error:
error
Here is my code:
context.js:
import React, { useState, useContext, useEffect } from "react";
import { useCallback } from "react";
const url = "https://cakeaddicts-api.herokuapp.com/cakes";
const AppContext = React.createContext();
const AppProvider = ({ children }) => {
const [loading, setLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [cakes, setCakes] = useState([]);
const [filteredData, setFilteredData] = useState([]);
const fetchCakes = async () => {
setLoading(true);
try {
const response = await fetch(url);
const cakes = await response.json();
if (cakes) {
const newCakes = cakes.map((cake) => {
const {
id,
image,
cake_name,
category,
type,
ingredients,
instructions,
} = cake;
return {
id,
image,
cake_name,
category,
type,
ingredients,
instructions,
};
});
setCakes(newCakes);
console.log(newCakes);
} else {
setCakes([]);
}
setLoading(false);
} catch (error) {
console.log(error);
setLoading(false);
}
};
useEffect(() => {
fetchCakes();
}, []);
return (
<AppContext.Provider
value={{
loading,
cakes,
setSearchTerm,
searchTerm,
filteredData,
setFilteredData,
}}
>
{children}
</AppContext.Provider>
);
};
// make sure use
export const useGlobalContext = () => {
return useContext(AppContext);
};
export { AppContext, AppProvider };
SearchForm.js
import React from "react";
import { useGlobalContext } from "../context";
import CakeList from "./CakeList";
const SearchForm = () => {
const { cakes, setSearchTerm, searchTerm, setFilteredData } =
useGlobalContext;
const searchCakes = () => {
if (searchTerm !== "") {
const filteredData = cakes.filter((item) => {
return Object.values(item)
.join("")
.toLowerCase()
.includes(searchTerm.toLowerCase());
});
setFilteredData(filteredData);
} else {
setFilteredData(cakes);
}
};
return (
<section className="section search">
<form className="search-form">
<div className="form-control">
<label htmlFor="name">Search Your Favourite Cake</label>
<input
type="text"
id="name"
onChange={(e) => searchCakes(e.target.value)}
/>
</div>
</form>
</section>
);
};
export default SearchForm;
CakeList.js:
import React from "react";
import Cake from "./Cake";
import Loading from "./Loading";
import { useGlobalContext } from "../context.js";
const CakeList = () => {
const { cakes, loading, searchTerm, filteredResults } = useGlobalContext();
if (loading) {
return <Loading />;
}
return (
<section className="section">
<h2 className="section-title">Cakes</h2>
<div className="cakes-center">
{searchTerm.length > 1
? filteredResults.map((cake) => {
return <Cake key={cake.id} {...cake} />;
})
: cakes.map((item) => {
return <Cake key={item.id} {...item} />;
})}
</div>
</section>
);
};
export default CakeList;
App.js:
import React from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
// import pages
import Home from "./pages/Home";
import About from "./pages/About";
import SingleCake from "./pages/SingleCake";
import Error from "./pages/Error";
// import components
import Navbar from "./components/Navbar";
function App() {
return (
<Router>
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/cake/:id" element={<SingleCake />} />
<Route path="*" element={<Error />} />
</Routes>
</Router>
);
}
export default App;
Can someone please help me with this search form? I have tried so many things and nothing is working :( Anyone?
On line 11 of SearchForm.js, there is a part that reads cakes.filter(. To resolve the typeError, change this to cakes?.filter(. This will only execute the filter if cakes is defined. It's a feature in javascript called Optional Chaining, introduced in ES2020.
Learn about it more here
I have a question about React and am a newbie. So, my concern is what is the best practice when fetching kind of large data. I have some components that require data called quizzes. As I was writing, I noticed I wrote useEffect to get all of the quizzes I have in Firestore and started wondering, "how about fetching all quizzes in App.js and include it as one of the props in components that require the data. On the other hand, the next Because App.js called every time, to fetch quizzes in App.js can make my app work slow. I recently have only a few quizzes so I don't really think or see how slow my app becomes between the two ways. If I have large data, it takes some time to fetch all of the quizzes, right?? So, I also concern how to use async await for fetching data.
My code will work in both ways, but I just wanna know the best practice for fetching large data to step up to a mid-level React developer.
Thanks for your time and effort in advance here:)
App.js
// Import from 3rd parties
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { onAuthStateChanged } from 'firebase/auth'
import { collection, onSnapshot } from 'firebase/firestore';
// Import files existing in this project
import './styles/Style.css';
import './styles/quiz.css';
import Header from "./components/Header";
import Footer from "./components/Footer";
import Test from "./components/Test";
import QuizHeader from "./components/QuizHeader";
import QuizEdit from "./components/QuizEdit";
// import About from "./components/About";
import ErrorPage from "./components/ErrorPage";
import Profile from "./components/Profile";
import QuizHome from "./components/QuizHome";
import AllQuizzes from "./components/AllQuizzes";
import FormikNewQuiz from "./components/FormikNewQuiz";
import { auth, db } from './config/firebase';
// Actual Coding
function App() {
const [currentUser, setCurrentUser] = useState({});
const [quizzes, setQuizzes] = useState({});
useEffect(() => {
onAuthStateChanged(auth, (user) => {
if (user !== null) {
setCurrentUser({ username: user.displayName, uid: user.uid, email: user.email, photoURL: user.photoURL })
}
})
const collectionRef = collection(db, 'quizzes');
const unsub = onSnapshot(collectionRef, {
next: snapshot => {
setQuizzes(snapshot.docs.map(doc => ({ ...doc.data(), id: doc.id })));
},
error: err => {
console.error('quizes listener failed: ', err);
},
});
return unsub;
}, [])
console.log(currentUser)
console.log(quizzes)
return (
<BrowserRouter>
<Header user={currentUser} />
<div id="main">
<Routes>
<Route path="kinniku-quiz/" element={<QuizHeader />} >
<Route path="home" element={<QuizHome />} />
<Route path="new" element={<FormikNewQuiz uid={currentUser === {} ? null : currentUser.uid} username={currentUser === {} ? null : currentUser.username} />} />
<Route path="test" element={<Test />} />
<Route path="all-quizzes" element={<AllQuizzes uid={currentUser === {} ? "" : currentUser.uid} />} />
<Route path="edit/:id" element={<QuizEdit quiz={"quiz props!!"} />} />
</Route>
<Route path="profile/:uid" element={<Profile setCurrentUser={setCurrentUser} user={currentUser} />} />
<Route path="*" element={<ErrorPage />} />
</Routes>
</div>
<Footer />
</BrowserRouter>
);
}
export default App;
This is an example component in which I used useEffect to fetch all quizzes.
Test.jsx
import { useEffect, useState } from 'react';
import { collection, onSnapshot } from 'firebase/firestore';
import Loading from 'react-simple-loading';
import {db} from '../config/firebase';
import GoodBad from './GoodBad';
import GoNextQuizBtn from './GoNextQuizBtn';
import GoPrevQuizBtn from './GoPrevQuizBtn';
import QuizResultWindow from './QuizResultWindow';
import { biCircle, biPlus } from '../icons/icons';
const Test = () => {
const [quizzes, setQuizzes] = useState([]);
// const [clickedAnswers, setClickedAnswers] = useState([]);
const [currentQIndex, setCurrentQIndex] = useState(0);
const [points, setPoints] = useState(0);
const [usersCorrectAnswers, setUsersCorrectAnswers] = useState([]);
const [clickedAnswerIndex, setClickedAnswerIndex] = useState();
useEffect(() => {
const collectionRef = collection(db, 'quizzes');
const unsub = onSnapshot(collectionRef, {
next: snapshot => {
setQuizzes(snapshot.docs.map(doc => ({ ...doc.data(), id: doc.id })));
},
error: err => {
// don't forget error handling! e.g. update component with an error message
console.error('quizes listener failed: ', err);
},
});
return unsub;
// const unsub = onSnapshot(collectionRef, snapshot => {
// setQuizzes(snapshot.docs.map(doc => ({ ...doc.data(), id: doc.id })));
// });
// return unsub;
// getData(): run once
// onSnapshot(): listen for realtime updates
}, []);
const handleJudge = async (e, answer, quiz, answerIndex, quizIndex) => {
// It may be unnecessary to add 1. I jsut thought users don't like index 0 for answer/quiz 1.
answerIndex++;
quizIndex++;
const correctAnswerIndex = quiz.correctAnswer;
console.log(
`answer => ${answer}, answerIndex => ${answerIndex}, correctAnswerIndex => ${correctAnswerIndex}, quizIndex => ${quizIndex}`
);
setClickedAnswerIndex(answerIndex);
// When an answer is clicked:
// 1. make all disable to be clicked <- can be done by adding the same className to all.
// 2. show the answer with a slight dif style
// 3. add style to a selected answer.
// add some styles to answers depending on correct or not
if (correctAnswerIndex === answerIndex) {
setPoints(prevState => prevState + 1);
setUsersCorrectAnswers([...usersCorrectAnswers, quizIndex]);
e.target.className = await 'selected correctAnswerClicked';
} else {
e.target.className = await 'selected incorrectAnswerClicked';
}
};
const goNextQuiz = () => {
if (currentQIndex !== quizzes.length) {
setCurrentQIndex(prevState => prevState + 1);
}
setClickedAnswerIndex();
};
const goPrevQuiz = () => {
if (currentQIndex !== 0) {
setCurrentQIndex(prevState => prevState - 1);
} else {
setCurrentQIndex(currentQIndex);
}
setClickedAnswerIndex();
};
return (
<div className='quizContainer'>
{quizzes.length === 0 ? <Loading color={'#005bbb'} /> : ''}
{quizzes.map((quiz, quizIndex) => {
if (quizIndex === currentQIndex) {
return (
<div key={quiz.id} className='quiz'>
<div className='quizHeader'>
<span className='createdBy'>Created by: {quiz.username ? quiz.username : "Anonymous"}</span>
<span className='quizNumber'>
{quizIndex + 1}/{quizzes.length}
</span>
</div>
<div className='quizQuestionContainer'>
<p className='quizQuestionText'>{quiz.question}</p>
</div>
<ul
className={
clickedAnswerIndex
? 'quizAnswersContainer answerDefined'
: 'quizAnswersContainer'
}
>
{quiz.answers.map((answer, answerIndex) => (
<li
key={answer}
onClick={e => {
handleJudge(e, answer, quiz, answerIndex, quizIndex);
}}
className={
clickedAnswerIndex &&
answerIndex + 1 === clickedAnswerIndex
? 'selected'
: null
}
>
<span className='answer'>{answer}</span>
<div className='correctIncorrectIcons'>
<span className='correctIcon'>{biCircle}</span>
<span className='incorrectIcon'>{biPlus}</span>
</div>
</li>
))}
</ul>
<div className='quizFooter'>
{quizIndex !== 0 ? (
<GoPrevQuizBtn
goPrevQuiz={goPrevQuiz}
text='Prev'
disable=''
/>
) : (
<GoPrevQuizBtn
goPrevQuiz={goPrevQuiz}
text='Prev'
disable='disable'
/>
)}
<GoodBad quiz={quiz} />
{quizIndex + 1 === quizzes.length ? (
<GoNextQuizBtn goNextQuiz={goNextQuiz} text='Result' clickedAnswerIndex={clickedAnswerIndex ? true : false } />
) : (
<GoNextQuizBtn goNextQuiz={goNextQuiz} text='Next' clickedAnswerIndex={clickedAnswerIndex ? true : false } />
)
}
</div>
</div>
);
}
})}
{quizzes.length !== 0 && currentQIndex >= quizzes.length ? (
<QuizResultWindow
usersCorrectAnswers={usersCorrectAnswers}
points={points}
quizzes={quizzes}
/>
) : (
''
)}
</div>
);
};
export default Test;
This is my Json file which I created in my app.
export const Data = [
{
id: 1,
title: "Tilte 1",
description: "Decription 1 Data",
},
{
id: 2,
title: "Tilte 2",
description: "Decription 2 Data",
}
];
This is my main file from where I navigate it. I use json file to display all the records on page. When I click on selected item it will get its id and navigate to another page, where i can get the data of selected item coming from json.
import React from "react";
import { Data } from "./JSON"
import { useNavigate } from 'react-router-dom'
const Home = () => {
let naviagte = useNavigate();
return (
<>
{Data.map((data, key) => {
return (
<div class="card" >
<div class="card-body">
<h5 class="card-title" key={key.id}>{data.title}</h5>
<p class="card-text">{data.description}</p>
<button onClick={() => naviagte(`/service/${data.id}`)}>{data.title} </button>
</div>
</div>
);
})}
</>
)
}
export default Home;
When I navigate to another page where I want to display all data regarding the selected id. It shows only id not all data.
import React, {useState, useEffect} from "react";
import { Data } from "../home/JSON"
import { useParams } from "react-router-dom";
const Service = () => {
const { id } = useParams();
const [data, setData] =useState('');
console.log("check", data);
useEffect(() => {
setData (Data.map((_data) => _data.id === id ))
}, [id])
return(
<>
{id}
{data.title}
{data.description}
</>
)
}
export default Service;
Please guide me what I miss here. Thanks in Advance
Since you are importing the data in both places you just need to find the data by the id property instead of mapping it to booleans. Keep in mind that your id property is a number but the id route param will be a string, so you will need to convert them to a compatible type for the strict equality (===) check.
Example:
useEffect(() => {
setData(Data.find((_data) => String(_data.id) === id));
}, [id]);
Since data is treated as an object in the render return you'll want to insure you maintain a valid state invariant. Update the initial data state to be an object, and check that Array.prototype.find returned a defined object from the Data array before updating state.
const Service = () => {
const { id } = useParams();
const [data, setData] = useState({});
console.log("check", data);
useEffect(() => {
const data = Data.find((_data) => String(_data.id) === id);
if (data) {
setData(data);
}
}, [id]);
return (
<>
{id}
{data.title}
{data.description}
</>
);
};
I have an app that lists books on a users shelf and then on a subsequent search page.
The user goes to the search page, finds a title and selects a shelf for the title to be shelved on. When they go back to the home page this title should then show on the correct shelf.
The functionality works in that the changes are made to the objects, but when I click on the home button or back button in the browser the changes do not show until I have refreshed the browser.
What do I need to do to ensure this change is shown when the user browses to the home page?
I've put the bulk of the code into Codesandbox
App.js
import React, { Component } from 'react'
import ListBooks from './ListBooks'
import SearchBooks from './SearchBooks'
import * as BooksAPI from './utils/BooksAPI'
import { Route } from 'react-router-dom'
class BooksApp extends Component {
state = {
books: []
}
componentDidMount() {
BooksAPI.getAll()
.then((books) => {
this.setState(() => ({
books
}))
})
}
updateShelf = (book, shelf) => {
const bookFromState = this.state.books.find(b => b.id === book.id);
if (bookFromState) {
// update existing
bookFromState.shelf = shelf;
this.setState(currentState => ({
books: currentState.books
}));
} else {
// add new one
this.setState(prevState => ({
books: prevState.books
}));
}
BooksAPI.update(book, shelf);
};
render() {
return (
<div>
<Route exact path='/' render={() => (
<ListBooks
books={this.state.books}
onUpdateShelf={this.updateShelf}
/>
)} />
<Route exact path='/search' render={() => (
<SearchBooks
books={this.state.books}
onUpdateShelf={this.updateShelf}
/>
)} />
</div>
)
}
}
export default BooksApp
So I checked your code. You had problem updating your state actually. The books wasn't changing after you selected a book from within the search screen. Here's the code from your App.js:
updateShelf = (book, shelf) => {
console.log(book, shelf)
const bookFromState = this.state.books.find(b => b.id === book.id);
if (bookFromState) {
// update existing
bookFromState.shelf = shelf;
this.setState(currentState => ({
books: currentState.books
}));
} else {
// add new one
// the following lines of yours were different
book.shelf = shelf;
this.setState(prevState => ({
books: prevState.books.concat(book)
}));
}
BooksAPI.update(book, shelf);
};
So you merely had books: prevState.books instead of actually concatenating the book to the prev state. And just before that the shelf of book has to be changed to the one you pass.
PS: I might have left some console.log statements. Hope that is not a problem and you will clean the mess.