useEffect hook doesn't rerender when state is updated - javascript

I'm buildind a React Calendar using Tailwind and Redux and I have a little problem. I have a form where I create a new event and I push this date in my state 'savedEvents'. Atfer that, I want to use events from my state to generate dinamically some spans but my useEffect hook can't rerender when my state is updated and I don't understand why.
GitHub repository for code: https://github.com/snnack123/Events-Calendar
My global state (/store/events.js):
import dayjs from 'dayjs'
const initialState = {
monthIndex: dayjs().month(),
compare: dayjs().month(),
smallCalendarMonth: 0,
daySelected: dayjs(),
showEventModal: false,
savedEvents: [],
}
export function eventsReducer(state = initialState, action) {
switch (action.type) {
case 'events/setMonthIndex':
return { ...state, monthIndex: action.payload };
case 'events/setCompare':
return { ...state, compare: action.payload };
case 'events/setDaySelected':
return { ...state, daySelected: action.payload };
case 'events/setShowModal':
return { ...state, showEventModal: action.payload };
case 'events/setNewEvent':
return { ...state, ...state.savedEvents.push(action.payload) };
default:
return state;
}
}
My submit form (/components/EventModal.js):
<form className='bg-white rounded-lg shadow-2xl w-1/4'>
<header className='bg-gray-100 px-4 py-2 flex justify-between items-center'>
<span className='material-icons-outlined text-gray-400'>
drag_handle
</span>
<button onClick={setShowModal}>
<span className='material-icons-outlined text-gray-400'>
close
</span>
</button>
</header>
<div className='p-3'>
<div className="grid grid-cols-1/5 items-end gap-y-7">
<div></div>
<input
type="text"
name="title"
placeholder='Add title'
value={title}
onChange={(e) => setTitle(e.target.value)}
required
className='pt-3 border-0 text-gray-600 text-xl font-semibold pb-2 w-full border-b-2
border-gray-200 focus:outline-none focus:ring-0 focus-blue-500'
/>
<span className='material-icons-outlined text-gray-400'>
schedule
</span>
<p>{daySelected.format('dddd, MMMM DD')}</p>
<span className='material-icons-outlined text-gray-400'>
segment
</span>
<input
type="text"
name="description"
placeholder='Add a description'
value={description}
onChange={(e) => setDescription(e.target.value)}
required
className='pt-3 border-0 text-gray-600 pb-2 w-full border-b-2
border-gray-200 focus:outline-none focus:ring-0 focus-blue-500'
/>
<span className='material-icons-outlined text-gray-400'>
bookmark_border
</span>
<div className="flex gap-x-2">
{labelsClasses.map((lblClass, i) => (
<span key={i} className={`bg-${lblClass}-500 w-6 h-6 rounded-full flex items-center justify-center cursor-pointer`}
onClick={() => setSelectedLabel(lblClass)}>
{selectedLabel === lblClass && (
<span className="material-icons-outlined text-white text-sm">
check
</span>
)}
</span>
))}
</div>
</div>
</div>
<footer className='flex justify-end border-t p-3 mt-5 '>
<button type='button' className='bg-blue-500 hover:bg-blue-600 px-6 py-2 rounded text-white' onClick={saveEvent}>
Save
</button>
</footer>
</form>
Function which update my state from this form:
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const labelsClasses = [
"indigo",
"gray",
"green",
"blue",
"red",
"purple",
];
const [selectedLabel, setSelectedLabel] = useState(labelsClasses[0]);
function saveEvent() {
let new_event = {
title,
description,
label: selectedLabel,
day: daySelected.valueOf(),
id: Date.now(),
};
dispatch({ type: 'events/setNewEvent', payload: new_event });
setShowModal();
}
This is my useEffect that doesn't work (/components/Day.js)
const savedEvents = useSelector((state) => state.events.savedEvents);
const [dayEvents, setDayEvents] = useState([]);
useEffect(() => {
const events = savedEvents.filter(
(evt) =>
dayjs(evt.day).format("DD-MM-YY") === day.format("DD-MM-YY")
);
setDayEvents(events);
}, [savedEvents, day]);
I want to use my state here:
{dayEvents.map((evt, idx) => (
<div
key={idx}
className={`bg-${evt.label}-200 p-1 mr-3 text-gray-600 text-sm rounded mb-1 truncate`}
>
{evt.title}
</div>
))}

The hook UseEffect is working fine, the source of the problem is this:
export function eventsReducer(state = initialState, action) {
switch (action.type) {
case 'events/setMonthIndex':
return { ...state, monthIndex: action.payload };
case 'events/setCompare':
return { ...state, compare: action.payload };
case 'events/setDaySelected':
return { ...state, daySelected: action.payload };
case 'events/setShowModal':
return { ...state, showEventModal: action.payload };
case 'events/setNewEvent':
return { ...state, ...state.savedEvents.push(action.payload) }; // <== This will not trigger the re-rendering of your component
default:
return state;
}
}
Your component will not re-render even the change in the state, because you mutated the array savedEvents and Redux will not know that there is a change in the array savedEvents.
Solution:
export function eventsReducer(state = initialState, action) {
switch (action.type) {
case 'events/setMonthIndex':
return { ...state, monthIndex: action.payload };
case 'events/setCompare':
return { ...state, compare: action.payload };
case 'events/setDaySelected':
return { ...state, daySelected: action.payload };
case 'events/setShowModal':
return { ...state, showEventModal: action.payload };
case 'events/setNewEvent':
return { ...state, savedEvents: [...state.savedEvents, action.payload] }; // <== add this to trigger the re-rendering of your component
default:
return state;
}
}

...state.savedEvents.push(action.payload)
You're mutating your array, so it is the same array and doesn't trigger the effect. Create a new one instead.
savedEvents: [...state.savedEvents, action.payload]

Related

Getting custom components to work within react-hook-form using onChange

I have a checkout cart where you have different cart items, and for each one you can change the quantity prior to purchase.
Here's how the code looks:
import React, { useEffect, useState } from "react";
import PureInput from "./PureInput";
import { useForm, Controller } from "react-hook-form";
const CartInner = React.forwardRef(
(
{
control,
element,
value,
handleOnChange,
images,
name,
monthlyAmount,
price,
projectedGrowth,
id,
...inputProps
}: any,
ref: any
) => {
return (
<div className="grid gap-8 grid-cols-2 mb-12 py-6 px-8 border-2 border-slate-200">
<div>
<PureInput
min={200}
max={price}
onChange={handleOnChange}
type="number"
step={200}
defaultValue={element.price}
id={id}
ref={ref}
{...inputProps}
/>
</div>
</div>
);
}
);
export default function Checkout() {
const { control, handleSubmit } = useForm();
const handleOnChange = (index: any, e: any) => {
console.log(e, "e");
};
const onSubmit = async (data: any) => {
console.log(data, "data from Form.tsx");
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="grid gap-8 grid-cols-3">
<div className="col-span-2">
{[0, 2].map((element, index) => {
return (
<fieldset key={index}>
<Controller
render={({ field }) => (
<CartInner
element={element}
handleOnChange={(e) => handleOnChange(index, e)}
{...field}
/>
)}
name={`test.${index}.lastName`}
control={control}
/>
</fieldset>
);
})}
<button>Progess to payment</button>
</div>
</form>
);
}
And the PureInput:
import * as React from "react";
type IProps = any;
const PureInput = React.forwardRef(
({ className, id, onChange, ...inputProps }: IProps, ref: any) => {
return (
<input
id={id}
ref={ref}
onChange={onChange}
type="input"
className={`${className} block w-full bg-white text-black rounded-md border-2 font-bold border-grey-200 text-xl px-4 py-4 focus:border-orange-500 focus:ring-orange-500`}
{...inputProps}
/>
);
}
);
export default PureInput;
Everything works fine in terms of submitting the form. When I do, I get an array of whatever values I have entered into the input:
[{lastName: "1600"}
{lastName: "800"}]
My package versions:
"react-dom": "18.2.0",
"react-hook-form": "^7.29.0",
But my onChange no longer fires. How can I get the onChange to fire so I can log the value of the input inside <Checkout /> component?
Here's a codesandbox if it helps
You can make the following changes to plug into onChange event
// pass an event handler name with different name
<PureInput
min={200}
max={price}
// pass a handler with different name as inputOptions overrides that prop
handleOnChange={handleOnChange}
type="number"
step={200}
defaultValue={element.price}
id={id}
ref={ref}
{...inputProps}
/>
//plug into the default onchange to call you handler also
<input
id={id}
ref={ref}
onChange={(e) => {
console.log("on change");
// call react-hook-form onChange
onChange(e);
// call your handler
handleOnChange(e);
}}
type="input"
className={`${className} block w-full bg-white text-black rounded-md border-2 font-bold border-grey-200 text-xl px-4 py-4 focus:border-orange-500 focus:ring-orange-500`}
{...inputProps}
/>
Hope it helps you in solving your problem,
Cheers
This part of the documentation led me to the answer:
import React, { useEffect, useState } from "react";
import PureInput from "./PureInput";
import { useForm, Controller } from "react-hook-form";
const CartInner = React.forwardRef(
({ onChange, onBlur, name, label, ...inputProps }: any, ref: any) => {
return (
<input
name={name}
ref={ref}
onChange={onChange}
onBlur={onBlur}
type="number"
/>
);
}
);
export default function Checkout() {
const { control, handleSubmit } = useForm();
const handleOnChange = (index: any, e: any) => {
console.log(e.target.value, "e");
};
const onSubmit = async (data: any) => {
console.log(data, "data from Form.tsx");
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="grid gap-8 grid-cols-3">
<div className="col-span-2">
{[0, 2].map((element, index) => {
return (
<fieldset key={index}>
<Controller
render={({ field: { onBlur, value, name, ref } }) => (
<CartInner
key={index}
name={name}
ref={ref}
onChange={(e) => handleOnChange(index, e)}
onBlur={onBlur}
/>
)}
name={`test.${index}.lastName`}
control={control}
/>
</fieldset>
);
})}
<button>Progess to payment</button>
</div>
</form>
);
}
// add delete
// total money
// add the cart documents to a history with a timestamp and show it was a BUY ORDER
// delete the documents from the cart

File upload in React-Hook-Form returns empty object upon submission

I am trying to submit the form using the react-hook form. But it always returns empty data.
The Request payload returns something like this in the browser
{nameOnCurrentLicense: "test Biden",
driversLicenseNumber: "dfghj3243546",
deliveryAddress: "vbn 456 fghjgfs, UK",
driversLicenseNumber: "dfghj3243546",
driversLicensePhoto: {0: {}},
driversLicensePricingId: 1,
nameOnCurrentLicense: "test Biden",
passportPhoto: {0: {}}
}
import { data } from 'autoprefixer';
import { useEffect, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Link } from 'react-router-dom';
import { toast } from 'react-toastify';
import {
Button,
Container,
FileInput,
Input,
Line,
Radio,
TextInput,
Typography,
} from '../../../../common/components';
import { axiosInstance } from '../../../../config/axiosInstance';
import { createFormData } from '../../../../helpers';
import { useAppDispatch, useAppSelector } from '../../../../hooks';
import {
useDriverLicenseMutation,
useDriverLicensePayCardMutation,
useDriverLicensePayWalletMutation,
} from '../../apis';
import { formTwo } from '../../apis/slice/FormSlice';
import { formProps } from '../../types';
import { DriverLicenseSubmitPayload } from './types';
function Drift() {
const [price, setPrice] = useState([]);
const [passportFile, setPassportFile] = useState(null);
const [licenseFile, setLicenseFile] = useState(null);
const [driversLicensePricingId, setDriversLicensePricingId] = useState('');
// api hooks
const [onDriverLicense, { isLoading }] = useDriverLicenseMutation();
const [onInitPaymentViaWallet, { isLoading: isInitiatingWallet }] =
useDriverLicensePayWalletMutation();
const [onInitPaymentViaCard, { isLoading: isInitiatingCard }] = useDriverLicensePayCardMutation();
//RHF context
const priceSelection = useAppSelector((state) => state.formAction.FormTwo);
const { register, handleSubmit, errors } = useForm({
defaultValues: {
nameOnCurrentLicense: data.nameOnCurrentLicense,
driversLicenseNumber: data.driversLicenseNumber,
deliveryAddress: data.deliveryAddress,
passportPhoto: data.passportPhoto,
driversLicensePricingId: Number(priceSelection.id),
driversLicensePhoto: data.driversLicensePhoto,
},
mode: 'onBlur',
// resolver: yupResolver(schema),
});
//submit handler
const onSubmitForm = async (data) => {
try {
let result = await onDriverLicense({
nameOnCurrentLicense: data.nameOnCurrentLicense,
driversLicenseNumber: data.driversLicenseNumber,
deliveryAddress: data.deliveryAddress,
passportPhoto: data.passportPhoto,
driversLicensePricingId: Number(priceSelection.id),
driversLicensePhoto: data.driversLicensePhoto,
}).unwrap();
result.success === true &&
toast.success(result.message, {
position: toast.POSITION.BOTTOM_RIGHT,
});
} catch (error) {
console.log('This is the error', error);
}
};
useEffect(() => {
axiosInstance
.get('drivers-license/pricing')
.then((response) => {
setPrice(response.data.data);
})
.catch((error) => console.log(error));
return () => {};
}, []);
const dispatch = useAppDispatch();
//form field
const personalInputs = [
{ name: 'nameOnCurrentLicense', type: 'text', label: 'Name on current License' },
{ name: 'driversLicenseNumber', type: 'text', label: "Driver's license number" },
{ name: 'deliveryAddress', type: 'text', label: 'Delivery address' },
];
return (
<Container containerStyle='py-12'>
<form onSubmit={handleSubmit(onSubmitForm)}>
<>
<div className='w-full flex md:flex-row flex-col justify-between items-start flex-wrap h-full'>
<div className=' w-full md:w-[55%] px-6 py-3'>
<Typography textStyle='text-md font-semibold'>Fill out your informations</Typography>
<Typography variant='label'>
We’ll require you to fill out some information before we get you started.
</Typography>
<Line variant='header' title='Personal Information' />
{personalInputs.map((item, index) => (
<TextInput
key={index}
{...register(`${item.name}`)}
id={item.name}
type='text'
label={item.label}
name={item.name}
// error={!!errors.item.name}
// helperText={errors?.field.name?.message}
/>
))}
<Line variant='header' title='Upload Document' />
<div className='flex justify-between items-center'>
<div className='w-[45%] pt-4'>
<input
{...register('passportPhoto')}
type='file'
name={'passportPhoto'}
accept='image/png, image/gif, image/jpeg'
/>
</div>
<div className='w-[45%] pt-4'>
<input
{...register('driversLicensePhoto')}
type='file'
name={'driversLicensePhoto'}
accept='image/png, image/gif, image/jpeg'
/>
</div>
</div>
</div>
<div className='w-full md:w-[45%] pl-0 lg:pl-6 pt-12 md:pt-0'>
<Typography variant='h6' textStyle='font-semibold text-center'>
Pricing / Years of Validity
</Typography>
<div className='pt-8'>
<Radio
ref={register}
name='driversLicensePricingI'
onChange={(e) => setDriversLicensePricingId(e.target.value)}
data={price}
/>
</div>
<div className='w-full bg-GRAY flex justify-between items-center p-8 rounded-xl'>
<Typography variant='h5' textStyle='font-semibold'>
Total cost
</Typography>
{priceSelection.amount !== undefined ? (
<Typography textStyle='text-3xl font-medium text-black'>{`₦ ${priceSelection.amount}`}</Typography>
) : (
<Typography textStyle='text-3xl font-medium text-black'>{`₦ 0.00`}</Typography>
)}
</div>
</div>
</div>
<div className='w-full md:w-[55%]'>
<div className='flex flex-row justify-end items-center w-full pt-16'>
<Link to='' className='font-medium text-xs lg:text-sm hover:text-opacity-50'>
Cancel
</Link>
<div className='w-40 ml-4'>
<Button
type='submit'
height={'h-10'}
variant='secondary'
containerButton=' w-full px-4 text-black'
text='Buy now'
/>
</div>
</div>
</div>
</>
</form>
</Container>
);
}
export default Drift;

How to use socket.on in react?

I am building a small chat app, but I have problems with receiving messages in react. The problem is that when I receive a message I setMessages, but at the same time the messages state gets cleared. The problem is problably that I don't know where to place socket.on, currently it's inside a useEffect hook.
export const Room = ({ socket }) => {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
const { id } = useParams();
useEffect(() => {
socket.emit("join room", id);
}, []);
useEffect(() => {
socket.on("message", (message) => {
setMessages([
...messages,
{ message: message, createdBy: "other", id: uuidv4() },
]);
});
}, []);
const sendMessage = () => {
console.log("send");
socket.emit("message", input);
setMessages([
...messages,
{ message: input, createdBy: "me", id: uuidv4() },
]);
};
console.log("foo");
return (
<div className="flex justify-center text-center">
<div className="w-screen h-screen px-2 max-w-4xl mt-10">
<p className="text-3xl">Code: {id}</p>
<div className="bg-white w-full h-3/4 rounded-xl border-2 border-black overflow-y-auto">
{messages.map((message) => (
<Message
text={message.message}
createdBy={message.createdBy}
key={message.id}
/>
))}
</div>
<div className="flex flex-row border-2 border-black mt-2 rounded-xl p-2 bg-white">
<input
className="flex-grow border-none focus:outline-none"
onInput={(e) => setInput(e.target.value)}
/>
<button
className=" bg-green-500 rounded-xl px-2 py-1"
onClick={sendMessage}
>
Send
</button>
</div>
</div>
</div>
);
};
I'm pretty sure you have a closure problem. messages is not what you think it is when it is run inside the useEffect -- it may not be the latest version.
It's safer and good practice to use the functional variant of the setter. This way you can be certain that messages is the current version:
setMessages(messages => [
...messages,
{ message: message, createdBy: "other", id: uuidv4() },
]);

Debounce Input while performing a query in `urql` GraphQL Client in React.js

I have a double slider like https://zillow.github.io/react-slider with min & max values. It calls a query when one of the sliders changes.
But since the query is huge, it takes a lot of time & I need to find a way to use debounce so that the query doesn't get called every so often.
I did find an excellent solution → https://stackoverflow.com/a/58594348/6141587 using just React.js but not sure how to use it with urql?
Home.tsx
import React from 'react'
import ReactSlider from 'react-slider'
import debounce from 'lodash.debounce'
import { AcquisitionList } from '../components/index'
const Home = () => {
const [price, setPrice] = React.useState([0, 1000000000])
const debounceSetPrice = React.useCallback(debounce(setPrice, 2000), [])
return (
<div className="h-full p-8 text-white bg-blue-gray-900">
<div className="flex items-center justify-center">
<div className="flex flex-col items-center text-white">
<span className="">Min</span>
<input
className="text-lg font-bold text-center min-w-16 rounded-xl bg-gradient-to-b from-indigo-700 bg-blue-gray-900"
name="minPrice"
type="text"
value={price[0]}
onChange={(e) => {
const minPrice = e.target.value
const maxPrice = price[1]
debounceSetPrice([minPrice, maxPrice])
}}
/>
</div>
<ReactSlider
step={1}
min={0}
max={1000000000}
className="w-1/2 h-5 pr-2 mx-8 my-4 rounded-md bg-blue-gray-700 cursor-grab"
thumbClassName="absolute w-8 h-8 cursor-[grab] rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 ring-offset-blue-gray-700 -top-1 bg-gradient-to-b from-indigo-700 bg-blue-gray-900 focus:ring-indigo-500 focus:border-indigo-500"
ariaLabel={['Min Price', 'Max Price']}
value={price}
onChange={(price) => {
debounceSetPrice(price)
}}
/>
<div className="flex flex-col items-center text-white">
<span className="">Max</span>
<input
className="text-lg font-bold text-center min-w-16 rounded-xl bg-gradient-to-b from-indigo-700 bg-blue-gray-900"
name="maxPrice"
type="text"
value={price[1]}
onChange={(e) => {
const minPrice = price[0]
const maxPrice = e.target.value
debounceSetPrice([minPrice, maxPrice])
}}
/>
</div>
</div>
<AcquisitionList minPrice={price[0]} maxPrice={price[1]} />
</div>
)
}
export default Home
AcquisitionsList.tsx
import React from 'react'
import { useQuery } from 'urql'
import { Card } from '../components/index'
import {
GET_ALL_ACQUISITIONS,
GET_ACQUISITIONS_BY_PRICE,
} from '../graphql/index'
export const AcquisitionList = ({ minPrice, maxPrice }) => {
const [result, reexecuteQuery] = useQuery({
query: GET_ALL_ACQUISITIONS,
variables: {
minPrice,
maxPrice,
skip: 10,
take: 10,
},
})
const { data, fetching, error } = result
if (fetching) return <p>Loading...</p>
if (error) return <p>Oh no... {error.message}</p>
return (
<div className="flex flex-wrap justify-center mt-10">
{data.getAllAcquisitions.map((startup, i) => {
return <Card key={i} startup={startup} index={i} />
})}
</div>
)
}
My current solution delays the slider values to change for 2 seconds which makes sense as I call debounceSetPrice directly. How do I go about solving this?
I found the solution thanks to URQL maintainer Jovi:
const Home = () => {
const priceRange = [0, 100000000000] // minPrice = 0, maxPrice = 100 billion
const timeout = React.useRef(null)
const [acquisitionPriceRange, setAcquisitionPriceRange] = React.useState(
priceRange
)
const [price, setPrice] = React.useState(priceRange)
React.useEffect(() => {
timeout.current = setTimeout(() => {
setAcquisitionPriceRange(price)
timeout.current = null
}, 2000)
return () => {
if (timeout.current) clearTimeout(timeout.current)
}
}, [price])
return (
<div className="h-full min-h-screen p-8 text-white bg-blue-gray-900">
.
.
.
<AcquisitionList
minPrice={acquisitionPriceRange[0]}
maxPrice={acquisitionPriceRange[1]}
undisclosed={enabled}
sortByPrice={sortByPrice}
sortByStartupName={sortByStartupName}
/>
</div>
)
}
export default Home
I set the value synchronously so the UI doesn't get delayed & queue up a debounce for the state I'm passing down in React.useEffect. I pass the same debounced state down to <AcquisitionList minPrice={acquisitionPriceRange[0]} maxPrice={acquisitionPriceRange[1]} />

ReactJS async axios in Context API

I get data from a rest-api in context.js. And I assign the data to state. Here is the code from context.js:
export class AppProvider extends Component {
state = {
products: [],
cart: [],
dispatch: action => this.setState(state => reducer(state, action))
}
fetchData = async () => {
const products = await axios.get('http://127.0.0.1:4000/product/all', { headers: { "Access-Control-Allow-Origin": "*" } })
this.setState({
products: products.data
})
}
componentDidMount(){
this.fetchData();
}
render() {
return (
<AppContext.Provider value={this.state}>
{this.props.children}
</AppContext.Provider>
)
}
}
I'm using the data in the state in other components. But when the page is refreshed, i'm not using the data in the state in other components. When the page is refreshed, the state in context.js becomes the initial. how can i solve this problem? Here is the code from other component:
import React, { useState, useContext, useEffect } from 'react';
import { Button, Badge } from 'reactstrap';
import './ProductDetail.css';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faShoppingCart } from '#fortawesome/free-solid-svg-icons';
import { AppContext } from '../../../context/context';
import { toast, Slide } from 'react-toastify';
toast.configure();
const ProductDetail = props => {
const [inputValue, setInputValue] = useState(1);
const preventNegative = e => {
const value = e.target.value.replace(/[^\d]/, '');
if (parseInt(value) !== 0) {
setInputValue(value);
}
}
const addToCart = (e, productID) => {
// eslint-disable-next-line array-callback-return
const product = products.filter(p => {
// eslint-disable-next-line eqeqeq
if (p.id == productID) {
return p
}
})
console.log(product);
dispatch({
type: 'ADD_TO_CART',
payload: [product, inputValue]
})
toast.success("The product has been added to cart successfully !", {
position: toast.POSITION.TOP_CENTER,
autoClose: 2500,
transition: Slide
})
}
const { products, dispatch } = useContext(AppContext)
const filteredProduct = products.filter(p => p.id == props.match.params.id)
return (
<React.Fragment>
<main className="mt-5 pt-4">
<div className="container dark-grey-text mt-5">
<div className="row wow fadeIn">
<div className="col-md-6 mb-4">
<img src={filteredProduct[0].image} className="img-fluid" alt="" />
</div>
<div className="col-md-6 mb-4">
<div className="p-4">
<div className="mb-3">
<Badge color="primary">New</Badge>
<Badge color="success">Best seller</Badge>
<Badge color="danger">{filteredProduct[0].discountRate}% OFF</Badge>
</div>
<h2 className="h2">{filteredProduct[0].title}</h2>
<p className="lead">
<span className="mr-1">
<del>${filteredProduct[0].prevPrice}</del>
</span>
<span>${filteredProduct[0].price}</span>
</p>
<p className="text-muted">
{filteredProduct[0].detail}
</p>
<form className="d-flex justify-content-left">
<input
min="1"
onChange={(e) => preventNegative(e)}
value={inputValue}
type="number"
aria-label="Search"
className="form-control"
style={{ width: '100px' }} />
<Button onClick={(e) => addToCart(e, filteredProduct[0].id)} color="primary">Add to Cart <FontAwesomeIcon icon={faShoppingCart} /> </Button>
</form>
</div>
</div>
</div>
</div>
</main>
</React.Fragment>
)
}
export default ProductDetail;
Your AppProvider component is correct, the only problem you have is because you fetch the results from an API and you do that in componentDidMount which is supposed to be the correct.
However when you refresh the page the set in AppProvider is reset to initial value and as you are already on ProductDetail component, you try to access the product values even before it available from the API request and hence
const filteredProduct = products.filter(p => p.id == props.match.params.id)
will return you can empty array.
The problem occurs because you try to access filteredProduct[0].image and similar other properties.
The solution here is to use a loadingState and render a loader till the data is available
Also do make sure that when the data is available filteredProduct will never be empty
export class AppProvider extends Component {
state = {
isLoading: true,
products: [],
cart: [],
dispatch: action => this.setState(state => reducer(state, action))
}
fetchData = async () => {
const products = await axios.get('http://127.0.0.1:4000/product/all', { headers: { "Access-Control-Allow-Origin": "*" } })
this.setState({
isLoading: false,
products: products.data
})
}
componentDidMount(){
this.fetchData();
}
render() {
return (
<AppContext.Provider value={this.state}>
{this.props.children}
</AppContext.Provider>
)
}
}
and in productDetails
const ProductDetail = props => {
const [inputValue, setInputValue] = useState(1);
const preventNegative = e => {
const value = e.target.value.replace(/[^\d]/, '');
if (parseInt(value) !== 0) {
setInputValue(value);
}
}
const addToCart = (e, productID) => {
// eslint-disable-next-line array-callback-return
const product = products.filter(p => {
// eslint-disable-next-line eqeqeq
if (p.id == productID) {
return p
}
})
console.log(product);
dispatch({
type: 'ADD_TO_CART',
payload: [product, inputValue]
})
toast.success("The product has been added to cart successfully !", {
position: toast.POSITION.TOP_CENTER,
autoClose: 2500,
transition: Slide
})
}
const { products, dispatch, isLoading } = useContext(AppContext)
if(isLoading) return <div>Loading...</div>
const filteredProduct = products.filter(p => p.id == props.match.params.id)
return (
<React.Fragment>
<main className="mt-5 pt-4">
<div className="container dark-grey-text mt-5">
<div className="row wow fadeIn">
<div className="col-md-6 mb-4">
<img src={filteredProduct[0].image} className="img-fluid" alt="" />
</div>
<div className="col-md-6 mb-4">
<div className="p-4">
<div className="mb-3">
<Badge color="primary">New</Badge>
<Badge color="success">Best seller</Badge>
<Badge color="danger">{filteredProduct[0].discountRate}% OFF</Badge>
</div>
<h2 className="h2">{filteredProduct[0].title}</h2>
<p className="lead">
<span className="mr-1">
<del>${filteredProduct[0].prevPrice}</del>
</span>
<span>${filteredProduct[0].price}</span>
</p>
<p className="text-muted">
{filteredProduct[0].detail}
</p>
<form className="d-flex justify-content-left">
<input
min="1"
onChange={(e) => preventNegative(e)}
value={inputValue}
type="number"
aria-label="Search"
className="form-control"
style={{ width: '100px' }} />
<Button onClick={(e) => addToCart(e, filteredProduct[0].id)} color="primary">Add to Cart <FontAwesomeIcon icon={faShoppingCart} /> </Button>
</form>
</div>
</div>
</div>
</div>
</main>
</React.Fragment>
)
}
export default ProductDetail;

Categories

Resources