Use redux to track recent viewed product, is this ok? - html

I am a newbie in ReactJS. To show the recent viewed product, many pages say that we will use cookie. But I am using redux to do it. My idea is: each time visitor open the product page, we will dispatch information of the product (suchs as ID) to store of redux. Then, we only need to get data from store. I don't see any one saying about this way so I am a bit worried about my solution. In case it's better to use cookies, could you please advise a link/ website to learn about it. I search many page but there's no details so it's so hard. Thank you so much.
Here my code:
Store;
const Viewed = [];
const Viewed_reducer = (init = Viewed, action) => {
switch (action.type) {
case "ADD_TO_VIEWED":
if (init.indexOf(action.id) === -1) {
init.push(action.id)
}
return init;
default:
return init;
}
}
const All_reducer = redux.combineReducers({
Viewed: Viewed_reducer
});
const Store = redux.createStore(All_reducer);
export default Store;
Product page, this will dispatch the product ID to Store each time visitor open:
import React, {useEffect} from 'react';
import urlSlug from "url-slug";
import {connect} from "react-redux";
import ProductsInfo from '../../../Data/ProductInfo';
import RecentViewed from './RecentViewed/RecentViewed';
const ProductDetail = (props) => {
const nameSlug = props.match.params.slug;
const {dispatch} = props;
const Product = ProductsInfo.find((product) => {
return urlSlug(product.name) === nameSlug
});
useEffect(() => {
dispatch({type: "ADD_TO_VIEWED", id: Product.id})
},[dispatch, Product])
return (
<section className="product-detail">
<RecentViewed key={`recent${Product.id}`}/>
</section>
);
};
const mapStateToProps = (state) => {
return {
Store: state
}
}
export default connect(mapStateToProps)(ProductDetail)
Component to render the data from Store:
import React from "react";
import ProductsInfo from "../../../../Data/ProductInfo";
import {connect} from "react-redux";
const RecentViewed = (props) => {
const {Viewed} = props.Store;
let viewedProducts = [];
Viewed.forEach((id) => {
ProductsInfo.forEach((product) => {
if (product.id === id){
viewedProducts.push(product)
}
})
})
return (
<div className="container-fluid also-container recent-viewed-container">
<div className="row">
<div className="col">
<div className="title">Sản phẩm vừa xem</div>
<div className="wrap outside">
{viewedProducts.map((product) => {
return (
// inside here we will show information of product such as name, image, etc
)
})}
</div>
</div>
</div>
</div>
);
};
const mapStateToProps = (state) => {
return {
Store: state
}
}
export default connect(mapStateToProps)(RecentViewed)

If you're only going to remember the last viewed product for the length of the user's browsing session, then the redux store should be fine. Just remember that it only exists in-memory, so if the user closes their tab or hard-refreshes the page, the store isn't persisted and you'll lose any information there.
Setting it in a cookie will make the information survive a refresh or close/reopen, since cookies are persisted between sessions (assuming you're not using a session cookie).

Related

Ionic Route replace Url without page animation

Im about to setup a product Page with ionic-react and #ionic-react-router.
The products have different variants, soo I created a optional url parameter to enable smooth sharing.
My route is defined like this:
product: {
component: ProductPage,
background: '/images/dashboard-navigation-productOverview.png',
key: 'PRODUCT',
path: '/product/:id/:variantId?'
},
My ProductPage Component looks like this:
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { IonPage, IonContent } from '#ionic/react';
....
const ProductPage = ({match}) => {
const [variants, setVariants] = useState([]);
const [source, setSource] = useState(null);
const [variant, setVariant] = useState(null);
useEffect(() => {
(async () => {
const response = await productService.getProduct(parseInt(match.params.id, 10))
setSource(response);
})();
}, [match.params.id]);
useEffect(() => {
const variants = productHelpers.allVariantsFromSource(source);
if (match.params.variantId) {
const variant = productHelpers.getProductVariationById(variants, parseInt(match.params.variantId, 10));
setVariant(variant || variants[0]);
} else {
// 0 is always main detail
setVariant(variants[0]);
}
setVariants(variants);
}, [source, match.params.variantId]);
return <IonPage>
<Toolbar title={(source) ? source.name : ''}/>
<IonContent>
<Product source={source} variant={variant} variants={variants}/>
</IonContent>
</IonPage>
}
export default ProductPage;
And somewhere in product I want to change the variation with replacing the url.
So the user can use the back button, to get back where he comes from (mostly product list).
import React, {useContext} from "react";
import {IonButton, NavContext} from '#ionic/react';
const ProductVariant = ({source, newVariant}) => {
const {navigate} = useContext(NavContext);
return (
<IonButton
onClick={() => navigate(`/product/${source.id}/${newVariant.id}`, 'none', 'replace')}>{newVariant.name}</IonButton>
);
};
My Problem
The page URL is updating like I want to, The back function is working too.
But the page transition is still happening.
What i'm doing wrong??

How do you loop through json with map and filter in React?

I have data in json format and I want to loop through it to render the same component (ContentThumbnail) eight times but with different titles and other content.
I have tried creating a function which accepts four parameters to achieve this. Here is the function I've written in a separate file called RenderContent.js:
import React from 'react';
import ContentThumbnail from './ContentThumbnail';
function RenderContentThumbnail(data, sectionName, wrapperStart, wrapperEnd) {
return (
<div>
{data
.filter(d => d.sectionName === { sectionName })
.map(filteredSection => (
{wrapperStart}
<ContentThumbnail {filteredSection.title} />
{wrapperEnd}
))}
</div>
);
}
export default RenderContentThumbnail;
And here is where I'm trying to execute that function in my component DefaultDashboard.js:
import React, { useEffect } from 'react';
import RenderContent from '../../content-thumbnail/RenderContent';
const DefaultDashboard = () => {
const { data } = useFetchData({ queryString: `${contentLibraryApiUrl}/GetContentForPage/Home` });
return (
RenderContentThumbnail(data, "topSection", "<div>", "</div>")
);
};
export default DefaultDashboard;
Is anyone able to help me see where I'm going wrong? I'm getting errors inside my map function and the page won't render at all.:(
Many thanks!
Katie
UPDATE!
I have made a tweak to the code to specify the prop, which is called "title", but I'm getting the following:
You should change the way you are rendering RenderContent:
const DefaultDashboard = () => {
const { data } = useFetchData({ queryString: `${contentLibraryApiUrl}/GetContentForPage/Home` });
return (
<RenderContent data={data} sectionName="topSection" wrapperStart="<div>" wrapperEnd= "</div>")
);
};
You can make it a lot easier, removing RenderContentThumbnail:
const DefaultDashboard = () => {
const { data } = useFetchData({ queryString: `${contentLibraryApiUrl}/GetContentForPage/Home` });
return (
{data
.filter(d => d.sectionName === "topSection")
.map(filteredSection => (<div>
<ContentThumbnail title={filteredSection.title} />
</div>))
}
);
};
export default DefaultDashboard;

In React, is it possible to store a ref in a context?

I need global app-wide access to a VideoElement to play it on user events on browsers like Safari and was wondering if storing the VideoElement in a context would be the best way to do that. I programmatically play my video through a redux action and in Safari that is not possible unless it has been played once through a user triggered event (like a click)
Is it possible to store an element (ref) within a context? The VideoElement will be then rendered within the component which I want to have my video, and then other components will also have access to the context and be able to call functions such as usePlayVideo that based on the context's state, will either call videoElement.play() if this is the first time the video is being played, or dispatch the redux action to play the video programmatically otherwise
It is possible to store a ref into context! You need to create a context at first. Then you need to pass value to the context provider and create a ref object using useRef hook. After that, you pass the ref into the value.
Now, You have a ref object sharing between components under the context provider and if you want to retrieve or pass a new ref, you could use useContext hook to deal with it.
Here is the demo (codesandbox).
Here is the sample code.
import { createContext, useContext, useEffect, useRef, useState } from "react";
import "./styles.css";
const MyContext = createContext();
export const ContextStore = (props) => {
const ref = useRef();
return <MyContext.Provider value={ref}>{props.children}</MyContext.Provider>;
};
export default function App() {
return (
<>
<ContextStore>
<MyComponent />
<MyComponent2 />
</ContextStore>
</>
);
}
const MyComponent = () => {
const myContext = useContext(MyContext);
return (
<div className="App" ref={myContext}>
<h1>Hello MyComponent1</h1>
</div>
);
};
const MyComponent2 = () => {
const myContext = useContext(MyContext);
const [divRef, setDivRef] = useState();
useEffect(() => {
setDivRef(myContext);
}, [myContext]);
return (
<div className="App">
<h1>{divRef?.current && divRef.current.innerText}</h1>
</div>
);
};
You can use this approach:
VideoContext.js
import { createContext, createRef, useContext } from "react";
const VideoContext = createContext();
const videoRef = createRef();
export const VideoContextProvider = (props) => {
return (
<VideoContext.Provider value={videoRef}>
{props.children}
</VideoContext.Provider>
);
};
export const useVideoContext = () => useContext(VideoContext);
and App.js for example:
import { useState, useEffect } from "react";
import { useVideoContext, VideoContextProvider } from "./VideoContext";
const SomeComponent = () => {
const videoRef = useVideoContext();
return (
<div ref={videoRef}>
<h1>Hey</h1>
</div>
);
};
const SomeOtherComponent = () => {
const [ref, setRef] = useState();
const videoRef = useVideoContext();
useEffect(() => {
setRef(videoRef);
}, [videoRef]);
return (
<div>
<h1>{ref?.current?.innerText}</h1>
</div>
);
};
export default function App() {
return (
<>
<VideoContextProvider>
<SomeComponent />
</VideoContextProvider>
{/* ... */}
{/* Some other component in another part of the tree */}
<VideoContextProvider>
<SomeOtherComponent />
</VideoContextProvider>
</>
);
}
code sandbox
Why not? I'll say. Let's see if we can setup an example.
const fns = {}
const addDispatch = (name, fn) => { fns[name] = fn }
const dispatch = (name) => { fns[name] && fns[name]() }
const RefContext = createContext({ addDispatch, dispatch })
export default RefContext
const Child1 = () => {
const [video, dispatchVideo] = useState(...)
const { addDispatch } = useContext(RefContext)
useEffect(() => {
addDispatch('video', dispatchVideo)
}, [])
}
const Child2 = () => {
const { dispatch } = useContext(RefContext)
const onClick = () => { dispatch('video') }
...
}
The above two childs do not have to share the same ancestor.
I didn't use ref the way you wanted, but i think you can pass your ref to one of the function. This is a very basic idea. I haven't tested it yet. But seems it could work. A bit
I used this approach:
first I creacted the context and ContextProvider;
import React, { useRef } from "react";
export const ScrollContext = React.createContext();
const ScrollContextProvider = (props) => {
return (
<ScrollContext.Provider
value={{
productsRef: useRef(),
}}
>
{props.children}
</ScrollContext.Provider>
);
};
export default ScrollContextProvider;
then Added my provider in my index.js:
root.render(
<React.StrictMode>
<ScrollContextProvider>
<App />
</ScrollContextProvider>
</React.StrictMode>
);
after that I used my context where I needed it:
import React, { useContext } from "react";
import { ScrollContext } from "../../store/scroll-context";
const Products = () => {
const scrollCtx = useContext(ScrollContext);
return (
<section ref={scrollCtx.productsRef}>
// your code...
</section>
);
};
In my case I wanted to to scroll to the above component clicking a button from a different component:
import React, { useContext } from "react";
import { ScrollContext } from "../../store/scroll-context";
function Header() {
const scrollCtx = useContext(ScrollContext);
const scrollTo = () => {
setTimeout(() => {
scrollCtx.productsRef.current.scrollIntoView({ behavior: "smooth" });
}, 0);
};
return (
<header>
//your code ...
<button alt="A table with chair" onClick={scrollTo}>Order Now<button />
</header>
);
}
No. It's not possible to use Ref on context api. React ref is considered to be used on rendering element.
What you're looking for is to forward the ref, so that you can consume them wherever you want.

Strange behavior of useState() method of react hook while fetching data from axios

I am using axios library to fetch data from a json file through json-server.
When I am loading and using the response object in a single component it is working perfectly. But when I am passing this response object to child component from parent component it is not loading the data. Also not receiving any errors, can someone please help me to understand the difference and what is wrong with my approach?
//Scenario-1 : working perfectly fine:
import React, { useState, useEffect } from 'react';
import Display from './Display';
import Note from './note'
import axios from 'axios';
const App = () => {
const [notes, setNotes] = useState([])
const hook = () => {
axios.get('http://localhost:3001/notes')
.then(response => {
setNotes(response.data)
})
}
useEffect(hook, [])
return (
<div>
{notes.map(n => <Note key={n.id} note={n} />)}
</div>
)
}
export default App;
//Scenario-2 : Not working as expected, also no errors.
const Display = (props) => {
//Receiving data here, can see the values in console.
console.log('inside display, props.notex: ', props.notex);
const [notes, setNotes] = useState(props.notex);
//Blank object, why useState() method is not setting the value of "notes" from "props.notex".
console.log('inside display, notes: ', notes);
const generateRows = () => {
console.log('generateRows: ', notes)
return (
notes.map(n => <Note key={n.id} note={n} />)
)
}
return (
<div>
<ul>
{generateRows()}
</ul>
</div>
)
}
const App = () => {
const [notes, setNotes] = useState([])
const hook = () => {
axios.get('http://localhost:3001/notes')
.then(response => {
setNotes(response.data)
})
}
useEffect(hook, [])
return (
<div>
<Display notex={notes} />
</div>
)
}
export default App;
My guess is that useState is asynchronous, same as setState in Class components. Due to its async nature, you are not able to log anything - the log gets executed before the useState actually does anything.
If you really want to do it this way, you could initialize the value of the useState as an empty array and set up a useEffect hook, with the props.notex in your dependency array, something like this:
useEffect(() => {
if (props.notex) setNotes(props.notex)
}, [props.notex])
And then in the return
return (
<div>
<ul>
{notes.length && generateRows()}
</ul>
</div>
)
But you could just pass the props down from the parent to child without setting the state in the child component.
Hope this helps!

Get JSON Data in multiple components using reactjs and redux

I would like to show data from a single API to different components as I want to hit the API only once and distribute the data to multiple small components. I know I can do this by using redux state but not sure how to do it. Need your help to achieve this. Below is the code done so far.
homepage/index.js
import SlidingBanner from './banner/BannerList';
import Celebslider from './celebrityslider/CelebSlider';
class HomePage extends Component {
render() {
return (
<div>
<SlidingBanner />
<anotherslider />
</div>
);
}
}
export default HomePage;
BannerList.js
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { itemsFetchData } from '../../../actions/items';
class BannerList extends Component {
componentDidMount() {
this.props.fetchData();
}
render() {
let bannerArray = [];
let banner = this.props.items.banner
for (let key in banner) {
bannerArray.push(banner[key]);
return (
<div>
<Slider {...slidersettings}>
{this.props.items.banner.map((item) => (
<div key={item.id}>
<img src={item.image_url} className="img-responsive"/>
</div>
))}
</Slider>
</div>
);
}
if (this.props.hasErrored) {
return <p>Sorry! There was an error loading the items</p>;
}
if (this.props.isLoading) {
return <p>Loading…</p>;
}
return (null);
}
}
BannerList.propTypes = {
fetchData: PropTypes.func.isRequired,
items: PropTypes.object.isRequired,
hasErrored: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired
};
const mapStateToProps = (state) => {
return {
items: state.items,
hasErrored: state.itemsHasErrored,
isLoading: state.itemsIsLoading
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (url) => dispatch(itemsFetchData(url))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(BannerList);
anotherslider.js
Now in this file, i want to fetch another array of objects or object from the same API.
I tried to mount the API in container component but did not worked, I hope i am doing some mistake. Please correct.
If you want to fetch data in anotherslider.js file you must connect reducer to class/function inside it as well as you are making it in BannerList.js file.
Now before render call componentWillReceiveProps(nextProps) function and you will get your data here.
If you want to call data in both of the sliders, you have 2 ways to handle it.
Make your redux requests in HomePage.js component and bind the data to the other components.
When you get the data on BannerList.js component, your state will be updated. Just add the redux connection to your anotherslider.js component and get data when updated.
const mapStateToProps = (state) => {
return {
items: state.items,
hasErrored: state.itemsHasErrored,
isLoading: state.itemsIsLoading
};
};
export default connect(mapStateToProps, mapDispatchToProps)(HomeList);
Apart from all these options, you can also use react's Context API as Provider/consumer to distribute your data among small components... this will save you passing props to all small components and directly access the value in component using Context.Consumer .. moreover if you do not want to store this state in global redux store, context API will save you from it...