Context API, Cannot retreive the data I am setting in seperate component - javascript

Working on a little project of mine but ran into an issue while working with the so called Context API. First time doing it as well.
The issue I am having is that I am unable to console.log the data I am setting. Now I am trying to make a "Add To Cart" button, and whenever the button is pressed. An object containing things such as name, price, color etc will be sent over to my ShoppingCart component.
I am able to set the value, whenever I press the button. And it appears in the console.log. Though, I am unable to console.log the value from ShoppingCart.
My files look like this:
./Contexts
./AddToCartContext.js
./Components
./Product
./ProductContent.js // Here the "add to cart" button is located.
./ShoppingCart
./ShoppingCart.js // Here I need the data from ProductContent.js
App.js
Here is my App.js code:
const [cartItems, setCartItems] = useState({});
console.log(cartItems); // The data is being logged here perfectly.
return (
<AddToCartContext.Provider value={{ cartItems, setCartItems }}>
<ThemeProvider theme={themeMode}>
{/* GlobalStyles skapas i ./themes.js */}
<GlobalStyles />
<Router>
<Route exact path="/cart">
<ShoppingCart theme={theme} toggleTheme={toggleTheme} />
</Route>
<Route exact path="/category/:type/:id/:productid">
<FetchAPI />
</Route>
// ...
</Router>
</ThemeProvider>
</AddToCartContext.Provider>
);
Here is my shoppingcart.js code:
import { AddToCartContext } from "../../Contexts/AddToCartContext";
const ShoppingCart = (props) => {
const { cartItems } = useContext(AddToCartContext);
console.log(cartItems); // This always result in an empty Object.
return (
<Fragment>
<TopNavigation />
<BottomNavigation theme={props.theme} toggleTheme={props.toggleTheme} />
<Cart />
<Footer />
</Fragment>
);
};
And here is the code in ProductContent.js (shorten as its pretty long):
import { AddToCartContext } from "../../Contexts/AddToCartContext";
const ProductContent = (props) => {
const { setCartItems } = useContext(AddToCartContext);
return (
<button
type="button"
onClick={() =>
setCartItems({ // This returns the object to App.js, but not Shoppingcart.js
name: props.name,
price: props.price,
color: props.mainImg?.colour,
img: props.mainImg?.url,
id: params.productid,
})
}
className={classes.add_to_cart}
>
<Cart />
add to cart
</button>
);
}
As mentioned, it's the first time I work with Context API. Am I missing anything?
EDIT:
Did some testing, and if I set the initial value on const [cartItems, setCartItems] = useState({}); to for example "test", then it is being rendered inside of the Cart page.
Nothing is logging inside of the cart upon the button press. But whenever the initial value is set to "test" it is loaded instantly.
Here is the code for creating the context (AddToCartContext.js):
import { createContext } from "react";
export const AddToCartContext = createContext({});

try without destructuring like this below
const cartItems = useContext(AddToCartContext);
console.log(cartItems);
and check if there is anything in that object

Related

localStorage removing elements in array after browser refresh

I have a react app and I want to persist the array of favorites when the page refreshes.
The data is set correctly, I can see it in the dev tools. But when i refresh the page, the data is removed. Any ideas why this may be?
Link to sandbox - https://codesandbox.io/s/sad-surf-sqgo0q?file=/src/App.js:368-378
const App = () => {
const [favourites, setFavourites] = useState([]);
useEffect(() => {
localStorage.setItem("favourites", JSON.stringify(favourites));
}, [favourites]);
useEffect(() => {
const favourites = JSON.parse(localStorage.getItem("favourites"));
if (favourites) {
setFavourites(favourites);
}
}, []);
return (
<FavContext.Provider value={{ favourites, setFavourites }}>
<HashRouter>
<Routes>
<Route path={"/"} element={<Dashboard />} />
<Route path={"/favorites"} element={<Favorites />} />
</Routes>
</HashRouter>
</FavContext.Provider>
);
};
export default App;
Make sure to set item if array is not empty
useEffect(() => {
if(favourites.length) localStorage.setItem("favourites", JSON.stringify(favourites));
}, [favourites]);
Yes, it is because when you reload the app the useEffect will trigger and you have an empty array in your favorite for the first time so it set the empty array in local storage.
You can fix it by adding a simple check
useEffect(() => {
if(favourites.length > 0){
localStorage.setItem("favourites", JSON.stringify(favourites));
}
}, [favourites]);
By this setItem only work when there is something in the favorites state

Maintain the context data for each child component on React router change

I am beginner in React and working on React app where I am using the context to maintain the button state which can be in any one phase out of start, loading, stop.
I am passing the context to app component and have a React router to render the component on basis of route. I am rendering card component by looping through data where each card have one Button Component.
On button click of card1 the button1 should get in loading phase for 10-15 seconds depending on api response time. Once response comes it should be in stop phase. Similarly for button2 and button3 if clicked together. Now that seems to be working fine when I click on button1 and button2 instantly.
But when I click on 2 buttons together and move to another route and quickly come back I don't see my buttons to be in loading state though the api response is still pending. I should be seeing them in loading state and when response comes I should see them in start or stop phase.
I know I can use local or session storage but I don't want to due to some code restrictions.
Here is the stack blitz link : https://stackblitz.com/edit/node-3t59mt?file=src/App.js
Github Link: https://github.com/mehulk05/react-context-api
Button.jsx
import React, { useContext,useEffect, useState } from 'react'
import DbContext from '../Context/sharedContext'
function Button(props) {
console.log(props)
const {
requestStartDbObj,
setRequestStartDbObj
} = useContext(DbContext)
const [state, setstate] = useState(props.text?.status ?? "start")
useEffect(() => {
setstate(props.text?.status??"start")
return () => {
}
}, [state])
console.log(requestStartDbObj)
const start = ()=>{
setRequestStartDbObj({id:props.status.id, status:"loading"})
setstate("loading")
setTimeout(()=>{
setstate("stop")
setRequestStartDbObj({id:props.status.id, status:"stop"})
},5000)
}
return (
<div>
<button onClick={start}>{state}1</button>
</div>
)
}
export default Button
Card.jsx
function Card(props) {
const {
requestStartDbObj,
} = useContext(DbContext)
return (
<div>
<h1>{props.data.name}</h1>
<Button status={props.data} text={requestStartDbObj} />
</div>
)
}
Component1.jsx
function Component1() {
let data = [
{
id: 1,
name: "card1",
status: "start",
},
{
id: 2,
name: "card2",
status: "start",
},
{
id: 3,
name: "card3",
status: "start",
},
];
return (
<div>
<h1>Hello</h1>
{data.map((d, i) => (
<Card key={i} data={d} />
))}
</div>
);
}
ComponentWrapper.jsx
<h3>Wrpper</h3>
<Routes>
<Route path="/" element={<Component1 />} />
<Route path="about" element={<Component2 />} />
</Routes>
</div>
App.js
function App() {
return (
<div className="App">
<BrowserRouter>
<Link to="/">Home</Link> <br></br>
<Link to="/about">Comp 2</Link>
<DbProvider>
<ComponentWrapper/>
</DbProvider>
</BrowserRouter>
</div>
);
}
The issue is that your DbProvider context isn't the source of truth as to the status, it's not the component maintaining the requestStartDbObj state. Each Button is duplicating the state locally and using its own start function. Each Button is also replacing the requestStartDbObj state of the context, so when switching back to the home path all the buttons get the same initial state value. Upon navigating away from the home path the Button component is unmounted, so the state updates on timeout are lost.
You should move the start logic to the sharedContext so DbProvider maintains control over the state updates. start should consume an id argument so it can correctly update the status for that specific object.
DbProvider
const DbProvider = (props) => {
const [requestStartDbObj, setRequestStartDbObj] = useState({});
const { children } = props;
const start = (id) => {
setRequestStartDbObj((state) => ({
...state,
[id]: { status: "loading" }
}));
setTimeout(() => {
setRequestStartDbObj((state) => ({
...state,
[id]: { status: "stop" }
}));
}, 5000);
};
return (
<DbContext.Provider
value={{
requestStartDbObj,
start
}}
>
{children}
</DbContext.Provider>
);
};
Card
Only pass the id prop through to Button from data prop that was passed from Component1 when mapped.
function Card({ data }) {
return (
<div>
<h1>{data.name}</h1>
<Button id={data.id} />
</div>
);
}
Button
Use the id prop to pass to start function provided by the context. Also use the id to access the current status.
function Button({ id }) {
const { requestStartDbObj, start } = useContext(DbContext);
return (
<div>
<button onClick={() => start(id)}>
{requestStartDbObj[id]?.status || "start"}-{id}
</button>
</div>
);
}

history.push() does not work on the same page using react

I am building a listing page where products are displayed and i have a search window in my header (on every page).
The search window works fine. I enter a searchword, it forwards to the listing page and it gives me the results. This works on every site, except when i am already on the listing page. If i enter a searchword while i am on the listing page, it changes the url, but nothing else.
Code Search: The Searchinput triggers the change and is a component inside Search and looks as follows:
import React, { useState, useRef } from 'react';
import { LISTING_POSTS_PAGE } from 'settings/constant';
import { HOME_PAGE } from 'settings/constant';
const SearchInput = ({history}) => {
const [searchword, setSearchword] = useState('');
const submitHandler = (e) => {
e.preventDefault();
history.search= '?'+searchword;
history.push({
pathname: LISTING_POSTS_PAGE,
})
}
return (
<form className="search" onSubmit={submitHandler}>
<div className = "row">
<input
type = "text"
searchword = "q"
id = "q"
placeholder = "What do you want to buy?"
onChange = {(e) => setSearchword(e.target.value)}>
</input>
</div>
</form>
);
};
const Search= (props) => {
console.log(props)
const { updatevalue } = props;
return <SearchInput getinputvalue={updatevalue} history={props.history} />;
};
export default Search;
The listing page looks like this and takes the history object to make an api request to my db before rendering.
import React, { useState, Fragment } from 'react';
import Sticky from 'react-stickynode';
import Toolbar from 'components/UI/Toolbar/Toolbar';
import { Checkbox } from 'antd';
import CategotySearch from 'components/Search/CategorySearch/CategotySearch';
import { PostPlaceholder } from 'components/UI/ContentLoader/ContentLoader';
import SectionGrid from 'components/SectionGrid/SectionGrid';
import ListingMap from './ListingMap';
import FilterDrawer from 'components/Search/MobileSearchView';
import useWindowSize from 'library/hooks/useWindowSize';
import useDataApi from 'library/hooks/useDataApi';
import { SINGLE_POST_PAGE } from 'settings/constant';
import ListingWrapper, { PostsWrapper, ShowMapCheckbox } from './Listing.style';
export default function Listing({ location, history }) {
let url = 'http://127.0.0.1:5000/api/products'
if (history.search) {
url = url + history.search;
}
console.log(url)
const { width } = useWindowSize();
const [showMap, setShowMap] = useState(false);
const { data, loading, loadMoreData, total, limit } = useDataApi(url);
let columnWidth = [1 / 1, 1 / 2, 1 / 3, 1 / 4, 1 / 5];
if (showMap) {
columnWidth = [1 / 1, 1 / 2, 1 / 2, 1 / 2, 1 / 3];
}
const handleMapToggle = () => {
setShowMap((showMap) => !showMap);
};
return (
<ListingWrapper>
<Sticky top={82} innerZ={999} activeClass="isHeaderSticky">
<Toolbar
left={
width > 991 ? (
<CategotySearch history={history} location={location} />
) : (
<FilterDrawer history={history} location={location} />
)
}
right={
<ShowMapCheckbox>
<Checkbox defaultChecked={false} onChange={handleMapToggle}>
Show map
</Checkbox>
</ShowMapCheckbox>
}
/>
</Sticky>
<Fragment>
<PostsWrapper className={width > 767 && showMap ? 'col-12' : 'col-24'}>
<SectionGrid
link={SINGLE_POST_PAGE}
columnWidth={columnWidth}
data={data}
totalItem={total.length}
loading={loading}
limit={limit}
handleLoadMore={loadMoreData}
placeholder={<PostPlaceholder />}
/>
</PostsWrapper>
{showMap && <ListingMap />}
</Fragment>
</ListingWrapper>
);
}
I tried to pass down the history object so i do not use different history objects (like useHistory from "react-router-dom") but it didnt changed anything on that behaviour.
I Do assume this is because i try to do history.push(LISTING_PAGE) while i am already on this page. But as far i read, this should be irrelevant. What do you think?
EDIT:
My index.js lloks as follows:
const App = () => (
<ThemeProvider theme={theme}>
<>
<GlobalStyles />
<BrowserRouter>
<AuthProvider>
<Routes />
</AuthProvider>
</BrowserRouter>
</>
</ThemeProvider>
);
React re-renders the page when a key of the component is changed. So you can do this in your router. This will make sure the key is change every time a param updates, thus result in re-render of the component.
<Route
exact
path="/your-page/:param"
render={(props) => (
<YourComponent
key={props.match.params.prodId}
/>
)}
/>
You need to add a hidden Link element in your SearchInput component. also need to create a reference and pass it to the Link element to trigger the click action on it:
import React, {useRef} from 'react';
import {Link} from 'react-router-dom';
// rest of the codes ...
// inside of SearchInput component
const linkRef = useRef(null);
return (
<form className="search" onSubmit={submitHandler}>
<div className = "row">
<Link to={LISTING_POSTS_PAGE} className={{display: "none"}} ref={linkRef} />
// rest of the codes ...
Now, it's time to change the submitHandler method to trigger a click action on the Link element after submitting the form:
const submitHandler = (e) => {
e.preventDefault();
history.search= '?'+searchword;
linkRef.current.click() // ---> instead of using history.push()
}
Note: better solution may be available, like force page to re-render and so on but using a simple concept of Link will be helpful as I explained above.

React render list only when data source changes

Basically I have a modal with a state in the parent component and I have a component that renders a list. When I open the modal, I dont want the list to re render every time because there can be hundreds of items in the list its too expensive. I only want the list to render when the dataSource prop changes.
I also want to try to avoid using useMemo if possible. Im thinking maybe move the modal to a different container, im not sure.
If someone can please help it would be much appreciated. Here is the link to sandbox: https://codesandbox.io/s/rerender-reactmemo-rz6ss?file=/src/App.js
Since you said you want to avoid React.memo, I think the best approach would be to move the <Modal /> component to another "module"
export default function App() {
return (
<>
<Another list={list} />
<List dataSource={list} />
</>
);
}
And inside <Another /> component you would have you <Modal />:
import React, { useState } from "react";
import { Modal } from "antd";
const Another = ({ list }) => {
const [showModal, setShowModal] = useState(false);
return (
<div>
<Modal
visible={showModal}
onCancel={() => setShowModal(false)}
onOk={() => {
list.push({ name: "drink" });
setShowModal(false);
}}
/>
<button onClick={() => setShowModal(true)}>Show Modal</button>
</div>
)
}
export default Another
Now the list don't rerender when you open the Modal
You can use React.memo, for more information about it please check reactmemo
const List = React.memo(({ dataSource, loading }) => {
console.log("render list");
return (
<div>
{dataSource.map((i) => {
return <div>{i.name}</div>;
})}
</div>
);
});
sandbox here

Passing data from parent to child using react.State but State reset to initial state

I am facing an issue while passing the state to the child component, so basically I am getting customer info from child1(Home) and saving in the parent state(App) and it works fine.
And then I am passing the updated state(basketItems) to child2(Basket). But when I click on the Basket button the basket page doesn't show any info in console.log(basketItems) inside the basket page and the chrome browser(console) looks refreshed too.
Any suggestion why it is happening and how can I optimize to pass the data to child2(basket) from main (APP).
update:2
i have tired to simulated the code issue in sand box with the link below, really appreciate for any advise about my code in codesandbox (to make it better) as this is the first time i have used it
codesandbox
Update:1
i have made a small clip on youtube just to understand the issue i am facing
basketItems goes back to initial state
Main (APP)___|
|_Child 1(Home)
|_Child 2 (Basket)
Snippet from Parent main(App) component
function App() {
const [basketItems, setBasketItems] = useState([]);
const addBasketitems = (product, quantity) => {
setBasketItems(prevItems => [...prevItems, { ...product, quantity }])
}
console.log(basketItems) // here i can see the updated basketItems having customer data as expected [{...}]
return (
<Router>
<div className="App">
<header className="header">
<Nav userinfo={userData} userstatus={siginalready} />
</header>
<Switch>
<Route path="/" exact render={(props) => (
<Home {...props} userData={userData} userstatus={siginalready}
addBasketitems={addBasketitems}
/>
)}
/>
<Route path="/basket" exact render={(props) =>
(<Basket {...props} basketItems={basketItems} />
)}
/>
</Switch>
</div>
</Router>
Snippet from the child(basket)
function Basket({basketItems}) {
console.log(basketItems) // here i only get the [] and not the cusotmer data from parent component
return (
<div>
{`${basketItems}`} // here output is blank
</div>
);
}
export default Basket;
Snippet from the child(Home)
... here once the button is pressed it will pass the userselected details to parent
....
<Button name={producNumber} value={quantities[productName]} variant="primary"
onClick={() => {
addBasketitems(eachproduct, quantities[productName])
}}>
Add to Basket
</Button >
Your function works fine, the reason your output in addbasketItem does not change is the when using setState it takes some time to apply the changes and if you use code below you can see the result.
useEffect(()=>{
console.log('basket:',basketItems)
},[basketItems])
Your Basket component only renders once so replace it with this code and see if it works:
function Basket({ basketItems }) {
const [items, setItems] = useState([]);
useEffect(() => {
setItems(basketItems);
}, [basketItems]);
return <div>{`${items}`}</div>;
}
but for passing data between several components, I strongly suggest that you use provided it is much better.

Categories

Resources