How to Jest test use of lodash.get in React component? - javascript

Error
TypeError: Cannot read property 'length' of undefined
My App component is making use of import get from 'lodash.get' https://lodash.com/docs/4.17.11#get
I'm using get inside my render function like so:
const getLabel = (listings, label) => {
const componentsMap = {
Deliveries: Delivery,
Dispensaries: Dispensary,
Doctors: Doctor
};
const DynamicIcon = componentsMap[label];
if (get(listings, 'listings').length) {
return (
<div key={label}>
<DynamicIcon fill={DARK_GRAY} /> <strong> {label} </strong>
</div>
);
}
return <div />;
};
App.test.js
import React from 'react'
import { shallow } from 'enzyme'
import toJson from 'enzyme-to-json'
import { AppJest } from './App'
import listingsMock from '../__test__/mocks/listings-mock.json';
// Mock the services.
const mockLocate = jest.fn();
const mockDisplayListing = jest.fn();
jest.mock('../actions', () => ({
locate: () => mockLocate(),
displayListing: () => mockDisplayListing()
}));
describe('<App /> component', () => {
describe('when rendering', () => {
const wrapper = shallow(<AppJest
listings={listingsMock}
locate={mockLocate}
displayListing={mockDisplayListing}
/>);
it('should render a component matching the snapshot', () => {
const tree = toJson(wrapper);
expect(tree).toMatchSnapshot();
expect(wrapper).toHaveLength(1);
});
});
});
I assumed it was because I wasn't mocking listings and passing it into the props of the shallow wrapper, but I added the mock.
listings-mock.json
{
"bottom_right": {
"latitude": 32.618865,
"longitude": -96.555516
},
"id": 1390,
"latitude": 32.78143692016602,
"listings": [
{
"avatar_image": {
"small_url": "https://images.weedmaps.com/deliveries/000/028/448/avatar/square_fill/1510581750-1507658638-Knox_Medical_Logo.png"
},
"city": "Dallas",
"distance": 2,
"id": 28448,
"license_type": "medical",
"name": "Knox Medical (Delivery Now Available)",
"online_ordering": {
"enabled_for_pickup": false,
"enabled_for_delivery": false
},
"package_level": "listing_plus",
"rating": 5,
"region_id": 1390,
"retailer_services": [
"delivery"
],
"slug": "knox-medical-dallas",
"state": "TX",
"static_map_url": "https://staticmap.weedmaps.com/static_map/13/32.7736/-96.795108/402/147/map.png",
"wmid": 459977538
}
],
"longitude": -96.7899169921875,
"name": "Dallas",
"region_path": "united-states/texas/dallas",
"slug": "dallas",
"top_left": {
"latitude": 33.016492,
"longitude": -96.999319
}
}
App.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import get from 'lodash.get';
import { locate, displayListing } from '../actions';
import Header from './header';
import Hero from './hero';
import Ripple from './partials/ripple';
import ListingCards from './listing_cards';
import Delivery from '../icons/delivery';
import Dispensary from '../icons/dispensary';
import Doctor from '../icons/doctor';
import { DARK_GRAY } from '../constants/colors';
import {
AppWrapper,
AppContent,
ListingGroups,
} from './styles';
const regionTypes = ['delivery', 'dispensary', 'doctor'];
const regionLabels = {
delivery: 'Deliveries',
dispensary: 'Dispensaries',
doctor: 'Doctors',
};
export class App extends Component {
constructor(props) {
super(props);
this.state = {
loadingTimer: 0,
isLocatingStarted: false,
geoCoords: null,
width: 0
};
this.locateMe = this.locateMe.bind(this);
this.gotoListing = this.gotoListing.bind(this);
}
componentDidMount() {
// Fetch geolocation ahead of time.
navigator.geolocation.getCurrentPosition(position =>
this.setState({ geoCoords: position.coords }));
this.updateWindowDimensions();
window.addEventListener("resize", this.updateWindowDimensions);
}
componentWillUnmount() {
window.removeEventListener("resize", this.updateWindowDimensions);
}
updateWindowDimensions = () => this.setState({ width: window.innerWidth });
locateMe() {
console.log('locateMe')
const { dispatch } = this.props;
const { geoCoords } = this.state;
if (navigator.geolocation && !geoCoords) {
navigator.geolocation.getCurrentPosition(position =>
dispatch(locate(position.coords)));
} else {
dispatch(locate(geoCoords));
}
this.setState({ isLocatingStarted: true });
};
gotoListing(listing) {
const { dispatch } = this.props;
dispatch(displayListing(listing));
const link = `/listing/${listing.wmid}`;
this.props.history.push(link);
}
render() {
const { isLocating, location, regions, error } = this.props;
const { isLocatingStarted, width } = this.state;
const { state_abv: state } = location !== null && location;
const isLoading = isLocatingStarted && isLocating;
const getLabel = (listings, label) => {
const componentsMap = {
Deliveries: Delivery,
Dispensaries: Dispensary,
Doctors: Doctor
};
const DynamicIcon = componentsMap[label];
if (get(listings, 'listings').length) {
return (
<div key={label}>
<DynamicIcon fill={DARK_GRAY} /> <strong> {label} </strong>
</div>
);
}
return <div />;
};
return (
<AppWrapper>
<Header history={this.props.history} />
<Hero
location={location}
isLocating={isLocating}
locateMe={this.locateMe}
/>
{ isLoading ? <Ripple /> :
<AppContent>
{error && <div> {error.message} </div>}
{regions && (
<React.Fragment>
{regionTypes.map(regionType => (
<ListingGroups key={regionType}>
<h2>
{getLabel(regions[regionType], regionLabels[regionType])}
</h2>
<ListingCards
listings={get(regions[regionType], 'listings')}
state={state}
isMobileSize={width < 769}
gotoListing={this.gotoListing}
/>
</ListingGroups>
))}
</React.Fragment>
)}
</AppContent>
}
</AppWrapper>
);
}
}
const mapStateToProps = state => state.location;
App.propTypes = {
isLocating: PropTypes.bool.isRequired,
location: PropTypes.object,
regions: PropTypes.object,
dispatch: PropTypes.any,
error: PropTypes.object,
};
App.defaultProps = {
isLocating: false,
location: {},
regions: {},
error: {},
};
export const AppJest = App
export default connect(mapStateToProps)(App);

Ah I needed to finish adding in all the props to the wrapper component:
import React from 'react'
import { shallow } from 'enzyme'
import toJson from 'enzyme-to-json'
import { AppJest } from './App'
import Header from './header';
import Hero from './hero';
import Ripple from './partials/ripple';
import listingsMock from '../__test__/mocks/listings-mock.json';
// Mock the services.
const mockLocate = jest.fn();
const mockDisplayListing = jest.fn();
const mockGeolocation = {
getCurrentPosition: jest.fn(),
watchPosition: jest.fn()
};
global.navigator.geolocation = mockGeolocation;
jest.mock('../actions', () => ({
locate: () => mockLocate(),
displayListing: () => mockDisplayListing()
}));
describe('<App /> component', () => {
describe('when rendering', () => {
const wrapper = shallow(<AppJest
navigator={mockGeolocation}
isLocating={false}
location={null}
regions={null}
dispatch={null}
error={null}
listings={listingsMock}
locate={mockLocate}
displayListing={mockDisplayListing}
/>);
it('should render a component matching the snapshot', () => {
const tree = toJson(wrapper);
expect(tree).toMatchSnapshot();
expect(wrapper).toHaveLength(1);
expect(wrapper.find(Header)).toHaveLength(1);
expect(wrapper.find(Hero)).toHaveLength(1);
});
});
});
Required props:
App.propTypes = {
isLocating: PropTypes.bool.isRequired,
location: PropTypes.object,
regions: PropTypes.object,
dispatch: PropTypes.any,
error: PropTypes.object,
};
App.defaultProps = {
isLocating: false,
location: {},
regions: {},
error: {},
};

Related

useRef undefined error using nextjs and remirror editor component

I'm using NextJS and Remirror Editor plugin. I get an error that setContent is undefined on the index page where the editor is loaded. I want to add an "external" button after the Component is loaded to exit the text. The component is dynamically externally loaded. I'm really unsure how to make the external button/text change to work.
Index.tsx:
import { NextPage, GetStaticProps } from 'next';
import dynamic from 'next/dynamic';
import { WysiwygEditor } from '#remirror/react-editors/wysiwyg';
import React, { useRef, useEffect } from 'react';
const TextEditor = dynamic( () => import('../text-editor'), { ssr: false } );
import natural from "natural";
export interface EditorRef {
setContent: (content: any) => void;
}
type Props = {
aaa:string
bbb:string
}
const Home: NextPage< Props > = (props) => {
//hook call ref to use editor externally
const ref = useRef<EditorRef | null>(null);
const {aaa,bbb} = props;
return (
<>
<ul>
{aaa} </ul>
Next.js Home Page
<TextEditor ref={ref} />
{
useEffect(()=>{
ref.current!.setContent({content: "testing the text has changed"})
},[])}
<button onClick={() => ref.current.setContent({content: "testing the text has changed"})}>Set another text button 2</button>
</>
);
};
var stringWordNet = "";
export const getStaticProps = async ():Promise<GetStaticPropsResult<Props>> => {
var wordnet = new natural.WordNet();
wordnet.lookup('node', function(results) {
stringWordNet = String(results[0].synonyms[1]);
});
return {
props:{
aaa:stringWordNet,
bbb:"bbb"
}
}
};
export default Home;
text-editor.tsx
import 'remirror/styles/all.css';
import { useEffect, useState, forwardRef, Ref, useImperativeHandle, useRef } from 'react';
import { BoldExtension,
ItalicExtension,
selectionPositioner,
UnderlineExtension, MentionAtomExtension } from 'remirror/extensions';
import { cx } from '#remirror/core';
import {
EditorComponent,
FloatingWrapper,
MentionAtomNodeAttributes,
Remirror,
useMentionAtom,
useRemirror, ThemeProvider, useRemirrorContext
} from '#remirror/react';
import { css } from "#emotion/css";
const styles = css`
background-color: white;
color: #101010;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px;
`;
const ALL_USERS = [
{ id: 'wordnetSuggestion1', label: 'NotreSuggestion1' },
{ id: 'wordnetSuggestion2', label: 'NotreSuggestion2' },
{ id: 'wordnetSuggestion3', label: 'NotreSuggestion3' },
{ id: 'wordnetSuggestion4', label: 'NotreSuggestion4' },
{ id: 'wordnetSuggestion5', label: 'NotreSuggestion5' },
];
const MentionSuggestor: React.FC = () => {
return (
<FloatingWrapper positioner={selectionPositioner} placement='bottom-start'>
<div>
{
ALL_USERS.map((option, index) => (
<li> {option.label}</li>
))
}
</div>
</FloatingWrapper>
);
};
const DOC = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'New content',
},
],
},
],
};
//Start Imperative Handle Here
export interface EditorRef {
setContent: (content: any) => void;
}
const ImperativeHandle = forwardRef((_: unknown, ref: Ref<EditorRef>) => {
const { setContent } = useRemirrorContext({
autoUpdate: true,
});
// Expose content handling to outside
useImperativeHandle(ref, () => ({ setContent }));
return <></>;
});
//Make content to show.
const TextEditor = forwardRef((_: unknown, ref: Ref<EditorRef>) => {
const editorRef = useRef<EditorRef | null>(null);
const { manager, state, getContext } = useRemirror({
extensions: () => [
new MentionAtomExtension()
],
content: '<p>I love Remirror</p>',
selection: 'start',
stringHandler: 'html',
});
return (
<>
<button
onMouseDown={(event) => event.preventDefault()}
onClick={() => editorRef.current!.setContent(DOC)}
></button>
<div className='remirror-theme'>
<Remirror manager={manager} initialContent={state}>
<EditorComponent />
<MentionSuggestor />
<ImperativeHandle ref={editorRef} />
</Remirror>
</div>
</>
);
});
export default TextEditor;

React-Redux. After changing state by useDispatch, components not updating by useSelector

I have useSelectors to check if my App component is updating. The first at the beginning of App component and the second one at App return part. By clicking on letters, currendElementId in store is updating with no problem (I checked by Redux DevTools). But useSelectors not updating.
I know that state should update in reducers immutable, but in createSlice we may use mutable code, so that's not my case. (Anyway, I have already tried to make state copy, change it and then send it, but it works same way)
There are my code:
store.js
import { configureStore } from "#reduxjs/toolkit";
import pagesReducer from "./../features/Pages/pagesSlice.js";
export default configureStore({
reducer: {
pagesInfo: pagesReducer,
},
})
pageSlice.js
import { createSlice, nanoid } from "#reduxjs/toolkit";
const initialState = {
currentPage: 0,
currentElementId: null,
pages: [ // Pages
{ // Page
id: nanoid(),
lines: [
{ // Line
id: nanoid(),
aligned: "left",
content: [
{ // Symbol
id: nanoid(),
type: "symbol",
symbol: "F",
isBold: false,
isItalic: false,
isUnderlined: false,
fontSize: 18,
color: "black",
},
{ // Symbol
id: nanoid(),
type: "symbol",
symbol: "o",
isBold: false,
isItalic: false,
isUnderlined: false,
fontSize: 18,
color: "black",
},
{ // Symbol
id: nanoid(),
type: "symbol",
symbol: "r",
isBold: false,
isItalic: false,
isUnderlined: false,
fontSize: 18,
color: "black",
},
],
},
{
id: nanoid(),
aligned: "left",
content: [
{ // Image
id: nanoid(),
type: "image",
imageSrc: "./img/me and the boys.png",
width: 200,
height: 200,
},
],
},
],
},
],
}
const textSlice = createSlice({
name: "pagesInfo",
initialState,
reducers: {
changeCurrentElementID(state, action) {
state.currentElementId = action.payload;
// return {
// ...state,
// currentElementId: action.payload,
// }
}
},
})
export const { changeCurrentElementID } = textSlice.actions;
export default textSlice.reducer;
index.js
import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import store from "./app/store.js";
import { Provider } from 'react-redux';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
App.js
import css from "./App.module.css";
// import BoldingTextButton from "./features/BoldingTextButton/BoldingTextButton.js";
import "./nullStyle.css";
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { changeCurrentElementID } from "./features/Pages/pagesSlice.js";
import EnterImageButton from "./features/Pages/EnterImage/EnterImageButton.js";
import TextSizing from "./features/Pages/TextSizing/TextSizing.js";
import TextDecoration from "./features/Pages/TextDecoration/TextDecoration.js";
import TextAligning from "./features/Pages/TextAligning/TextAligning.js";
function App(props) {
console.log(useSelector(state => state.currentElementId));
const dispatch = useDispatch();
// document.addEventListener("click", event => {
// const target = event.target.closest("li")
// if (target) {
// const elementID = target.getAttribute("elementid");
// dispatch(changeCurrentElementID(elementID));
// }
// })
const changeElementID = event => {
const target = event.target.closest("li")
if (target) {
const elementID = target.getAttribute("elementid");
dispatch(changeCurrentElementID(elementID));
}
}
const GetPageContent = () => {
const rawPageLinesContent = useSelector(state => state.pagesInfo.pages[state.pagesInfo.currentPage].lines);
const currentElementID = useSelector(state => state.currentElementId);
const completedPageContent = rawPageLinesContent.map(line => {
return (
<ul key={line.id} className={css["page-line"]}>
{
line.content.map(element => {
let finalElement;
if (element.type == "symbol") {
const style = { fontSize: element.fontSize + "px" }
finalElement = (
<li onClick={changeElementID} key={element.id} elementid={element.id} className={`${css["page-line__item"]} ${css["page-line__symbol"]}`} style={style}>{element.symbol}</li>
)
if (currentElementID == element.id) {
finalElement = (
<input key={element.id} elementid={element.id} maxLength="1" className={`${css["page-line__item"]} ${css["page-line__symbol"]} ${css["page-line__choosen"]}`} style={style} value={element.symbol} />
)
}
}
else if (element.type == "image") {
finalElement = (
<li onClick={changeElementID} key={element.id} elementid={element.id} className={`${css["page-line__item"]} ${css["page-line__image"]}`}>
<img src={element.imageSrc} width={element.width} height={element.height} alt="image" />
</li>
)
}
// else if (element.type == "enter") {
// finalElement = (
// <li key={element.id} elementid={element.id} className={`${css["page-line__item"]} ${css["page-line__image"]}`}>
// <br />
// </li>
// )
// }
return finalElement;
})
}
</ul >
)
})
return completedPageContent;
}
return (
<div className={css["App"]}>
<header className={`${css['App__header']} ${css['tools']} `}>
<EnterImageButton />
<TextSizing />
<TextDecoration />
<TextAligning />
<div>{useSelector(state => state.currentElementId) || 0}</div>
</header >
<div className={`${css['App__content']} ${css['pages']} `}>
<div className={`${css['pages__item']}`} >
{GetPageContent()}
</div>
</div>
</div >
);
}
export default App;
Your selector is wrong.
Since you have
export default configureStore({
reducer: {
pagesInfo: pagesReducer,
},
})
your pages slice is mounted as slice.pagesInfo.
So you need to do
useSelector(state => state.pagesInfo.currentElementId)

Increase view count when each product is opened in React Redux

What i want is to increase the count of each product, when it is opened(viewed), using react redux.
AllProductsPage.js(The page starts here)
import React, { useState } from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import { Link } from "react-router-dom";
import ProductList from "./ProductList";
import Pagination from './Pagination'
import * as productActions from "../redux/actions/productActions";
import * as userActions from '../redux/actions/userActions'
import { Button } from "react-bootstrap";
import {FiSearch} from 'react-icons/fi'
import { Container, Row, Col} from "react-bootstrap";
const AllProductsPage =(props)=> {
const [quantity, showQuantity] = useState(true);
const [price, showPrice] = useState(true);
const [manufacturer,showManufacturer] = useState(true);
const data = {quantity,price,manufacturer};
const [search,setSearch]=useState("");
const loggedIn = props.loggedIn;
//Pagination Logic
const [currentPage,setCurrentPage] = useState(1)
const postsPerPage = 9
const indexOfLastPost = currentPage * postsPerPage;
const indexOfFirstPost = indexOfLastPost - postsPerPage;
const currentPosts = props.products.slice(indexOfFirstPost,indexOfLastPost)
//Change the page
const paginate =(pageNumber)=>{
setCurrentPage(pageNumber)
}
//const filteredSearch = props.products && props.products.filter(product=>product.name.toLowerCase().indexOf(search.toLowerCase())!==-1).sort( (a,b)=>(a.id>b.id)?1:-1 );
const filteredSearch = currentPosts && currentPosts.filter(product=>product.name.toLowerCase().indexOf(search.toLowerCase())!==-1).sort( (a,b)=>(a.id>b.id)?1:-1 );
return (
<div>
<div style={{"display":"flex","paddingTop":"30px"}} className="container">
{ loggedIn && <Link to="/addProduct"><Button variant="primary">Add Product</Button>{" "}</Link> }
<span style={{"marginLeft":"auto"}}><input type="text" onChange={event=>setSearch(event.target.value)}/> {" "} <FiSearch size="20px"/> </span>
</div>
<div style={{"display":"flex","justifyContent":"flex-end","alignItems":"space-between","paddingTop":"6px"}} className="container" >
<label style={{"padding":"0px 5px 0px 2px","color":"white"}}><input type="checkbox" defaultChecked={quantity} onClick={()=>showQuantity(!quantity)}/>{" "}Quantity</label>
<label style={{"padding":"0px 5px 0px 2px","color":"white"}}><input type="checkbox" defaultChecked={price} onClick={()=>showPrice(!price)}/>{" "}Price </label>
<label style={{"padding":"0px 5px 0px 2px","color":"white"}}><input type="checkbox" defaultChecked={manufacturer} onClick={()=>showManufacturer(!manufacturer)}/>{" "}Manufacturer </label>
</div>
<hr></hr>
<div style={{minHeight:"100vh"}}>
<ProductList
products={filteredSearch}
data={data}
togglePrice={showPrice}
toggleQuantity={showQuantity}
toggleManufacturer={showManufacturer}
loggedIn={props.loggedIn}
/>
<br />
<Container>
<Row>
<Col></Col>
<Col xs="auto" sm="auto" md="auto" lg="auto">
<Pagination postsPerPage={postsPerPage} totalPosts={props.products.length} paginate={paginate} />
</Col>
<Col></Col>
</Row>
</Container>
</div>
<footer>
<p style={{"textAlign":"center","backgroundColor":"#333","color":"white","padding":"20px"}}>Copyright #2020, Rohit K F</p>
</footer>
</div>
);
}
function mapStateToProps(state, ownProps) {
return {
products: state.products,
users : state.users
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(productActions, dispatch),
userAction : bindActionCreators(userActions,dispatch)
};
}
export default (connect(mapStateToProps, mapDispatchToProps))(AllProductsPage);
ProductList.js(then it takes each product and passes it to Product.js)
import React from "react";
import Product from "./Product";
import { Container, Row, Col} from "react-bootstrap";
const chunk = (arr, chunkSize = 1, cache = []) => {
const tmp = [...arr]
if (chunkSize <= 0) return cache
while (tmp.length) cache.push(tmp.splice(0, chunkSize))
return cache
}
const ProductList = (props) => {
const productsChunks = chunk(props.products, 3)
const rows = productsChunks.map((productChunk, index) => {
const productsCols = productChunk.map((product, index) => {
return (
<Col xs="auto" sm="auto" md="auto" lg="auto" key={product.id} style={{"paddingBottom":"20px"}}>
<Product
key={product.id}
id={product.id}
quantity={product.quantity}
price={product.price}
name={product.name}
description={product.description}
manufacturer={product.manufacturer}
{...props}
/>
</Col>
);
});
return (
<Row key={index} style={{"paddingBottom":"20px"}}>
{productsCols}
</Row>
)});
return (
<Container>
{rows}
</Container>
)
}
export default ProductList;
Product.js(Here we show the each product)
import React,{useState} from "react";
import { Link } from "react-router-dom";
import { Prompt, withRouter } from "react-router";
import { connect } from "react-redux";
import * as productActions from "../redux/actions/productActions";
import { bindActionCreators } from "redux";
import { Card, Button } from "react-bootstrap";
import toastr from "toastr";
import EditProduct from './EditProduct'
import {MdDelete,MdVisibility,MdCreate} from 'react-icons/md'
const Product = (props) => {
const [show, setShow] = useState(false);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
const isLoggedIn = props.loggedIn
const checkUser = (e) => {
if (!isLoggedIn) {
e.preventDefault();
toastr.options = { positionClass: "toast-top-full-width",hideDuration: 300,timeOut: 2000,};
toastr.clear();
setTimeout(() => toastr.warning("Login to view details"), 0);
}
};
const deleteProduct = () => {
props.actions.deleteProduct(props.id)
};
//<Link to={'/ProductDetail/'+props.id} >
const product = {
id :props.id,name:props.name,quantity:props.quantity,description:props.description,manufacturer:props.manufacturer,price:props.price
}
return (
<>
<Card style={{ width: "18rem", "borderRadius":"30px","border":"3px solid" }}>
{isLoggedIn && (
<Prompt when={isLoggedIn}
message={(location) => location.pathname.includes("/ProductDetail/") ? `Are you sure you want to view the details ?` : true }
/>
)}
<Card.Body>
<Card.Title style={{"fontSize":"30px","fontWeight":"bold","display":"flex", "justifyContent":"center"}}> {props.name} </Card.Title>
{props.data.quantity && ( <Card.Text> Quantity : {props.quantity} </Card.Text> )}
{props.data.manufacturer && <Card.Text> Manufacturer : {props.manufacturer}</Card.Text>}
{props.data.price && <Card.Text>$ {props.price}</Card.Text>}
<div style={{ display: "flex", justifyContent: "space-around" }}>
<Link
to={{
pathname: `/ProductDetail/${props.id}`,
productName: {
id: props.id,
name: props.name,
price: props.price,
quantity: props.quantity,
description: props.description,
manufacturer: props.manufacturer,
},
}}
>
<Button variant="primary" onClick={(event) => checkUser(event)} style={{ "fontWeight":"bold" }} >
{!isLoggedIn && <span style={{"paddingRight":"5px"}}>View</span> }
{!isLoggedIn && <MdVisibility color="black"/> }
{isLoggedIn && <MdVisibility/>}
</Button>
</Link>
{isLoggedIn && <Button variant="success" style={{"fontWeight":"bold" }} onClick={() => handleShow()} ><MdCreate/></Button> }
{isLoggedIn && <Button variant="danger" style={{"fontWeight":"bold" }} onClick={() => deleteProduct()} ><MdDelete/> </Button>}
</div>
</Card.Body>
</Card>
<EditProduct show={show} handleClose={handleClose} actions={props.actions} product={product}/>
</>
);
};
function mapStateToProps(state, ownProps) {
return {
products: state.products,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(productActions, dispatch),
};
}
export default connect(mapStateToProps,mapDispatchToProps)(withRouter(Product));
ProductDetail.js(When clicked on View, it goes to this page to view details of the product)
import React from 'react';
import { Link} from 'react-router-dom';
import {withRouter} from 'react-router'
import {Button, Card} from 'react-bootstrap'
const ProductDetail=(props)=>{
console.log(props)
const style={"display":"flex", "justifyContent":"center","alignItems":"center"}
return(
<div style={style}>
<Card style={{ width: "18rem","borderRadius":"30px" }}>
<Card.Body style={{style}}>
<Card.Title style={{"fontSize":"30px","fontWeight":"bold","display":"flex", "justifyContent":"center"}}> {props.location.productName.name} </Card.Title>
<Card.Text><strong>Quantity :</strong>{props.location.productName.quantity}</Card.Text>
<Card.Text><strong>Price :</strong>{props.location.productName.price}</Card.Text>
<Card.Text><strong>Manufacturer:</strong>{props.location.productName.manufacturer}</Card.Text>
<Card.Text><strong>Description :</strong>{props.location.productName.description}</Card.Text>
<div>
<Link to="/"><Button variant="primary" style={{ height: "6vh","fontWeight":"bold" }}>Back</Button></Link>
</div>
</Card.Body>
</Card>
</div>
);
}
export default withRouter(ProductDetail);
ProductReducer.js
import initialState from "./initialState";
import * as actionTypes from "../actions/actionTypes";
export default function productReducer(state = initialState.products, action) {
switch (action.type) {
case actionTypes.INIT:
return action.products;
case actionTypes.ADD:
return [...state, Object.assign({}, action.product)];
case actionTypes.DELETE:
return [...state.filter((product) => product.id !== action.id)];
case actionTypes.UPDATE:
return [
...state.filter((product) => product.id !== action.product.id),
Object.assign({}, action.product),
];
case actionTypes.VIEW:
return [
...state[action.product.id],
Object.assign({},action.product.view)
]
default:
return state;
}
}
ProductActions.js
import dataApi from "../../server/dataAPI";
import * as actionTypes from "../actions/actionTypes";
//======================LOADING A PRODUCT
export function loadProduct() {
return function (dispatch) {
return dataApi
.getAllProducts()
.then((products) => {
dispatch({ type: actionTypes.INIT, products });
})
.catch((error) => {
throw error;
});
};
}
//==========================ADDING A PRODUCT
export function addProduct(product) {
return function (dispatch) {
return dataApi
.addProduct(product)
.then((product) => {
dispatch({ type: actionTypes.ADD, product });
})
.catch((error) => {
throw error;
});
};
}
//==========================DELETE A PRODUCT
export function deleteProduct(id) {
return function (dispatch) {
return dataApi
.deleteProduct(id)
.then((product) => {
dispatch({ type: actionTypes.DELETE, id});
})
.catch((error) => {
throw error;
});
};
}
//==========================UPDATE A PRODUCT
export function updateProduct(product) {
return function (dispatch) {
return dataApi
.updateProduct(product)
.then((product) => {
dispatch({ type: actionTypes.UPDATE, product });
})
.catch((error) => {
throw error;
});
};
}
//Increase View Count of product
export function addView(product){
return function (dispatch){
return dataApi.addView(product)
.then(product=>{
dispatch({type:actionTypes.VIEW, product})
})
}
}
dataAPI.js(to add,delete,update to json server with axios)
import axios from 'axios'
class dataAPI {
static getAllProducts() {
return axios.get('http://localhost:4000/products?_sort=id&_order=asc').then(response=>response.data);
}
static addProduct(product) {
return axios.post('http://localhost:4000/products',product).then(response=>response.data);
}
static updateProduct(product){
return axios.patch('http://localhost:4000/products/'+product.id,product)
.then(response=>response.data);
}
static deleteProduct(id){
return axios.delete(`http://localhost:4000/products/${id}`).then(response=>response.data);
}
static getAllUsers(){
return axios.get('http://localhost:4000/users').then(response=>response.data);
}
static addUser(user) {
return axios.post('http://localhost:4000/users',user).then(response=>response.data);
}
}
export default dataAPI;
db.json(the file that contains all the data)
{
"products": [
{
"id": 1,
"name": "Moto G5 Ultra",
"quantity": 3,
"price": 10000,
"description": "Moto G5",
"manufacturer": "Motorola",
"views" : 0
},
{
"id": 2,
"name": "Racold Geyser",
"quantity": 2,
"price": 60000,
"description": "Moto G5",
"manufacturer": "Motorola",
"views" : 0
},
{
"name": "Lenovo G5",
"quantity": 3,
"price": 55000,
"manufacturer": "Lenovo",
"description": "A gaming laptop",
"id": 3,
"views" : 0
},
{
"name": "Acer Swift ",
"quantity": 5,
"price": 35000,
"manufacturer": "Acer",
"description": "Business Laptop",
"id": 4,
"views" : 0
},
{
"name": "Acer Nitro 7",
"quantity": 4,
"price": 75000,
"manufacturer": "Acer",
"description": "A gaming laptop",
"id": 5,
"views" : 0
},
"users": [
{
"id": 1,
"email": "vi#gmail.com",
"password": "truth",
"name": {
"firstName": "Rick",
"lastName": "Garner"
},
"location": "Canada",
"mobile": "55643980"
},
{
"id": 2,
"email": "t#t.com",
"password": "123",
"name": {
"firstName": "Ram",
"lastName": "Shankar"
},
"location": "Delhi",
"mobile": "9895454860"
},
{
"email": "e#e.com",
"password": "123456789",
"name": {
"firstName": "RAGAV",
"lastName": "Shant"
},
"location": "Karnataka",
"mobile": "1234567891",
"id": 3
},
{
"email": "k#k.com",
"password": "123456789",
"name": {
"firstName": "sd",
"lastName": "dv"
},
"location": "dfv",
"mobile": "12345678231",
"id": 4
}
]
}
You may want to dispatch update products action in useEffect inside ProductDetail.jsx page.
useEffect(() => {
updateProduct({
...props.location.productName,
views: props.location.productName + 1,
});
}, []);
Of course you will also need to pass views from Product.jsx.
This will increase views every time user opens/refreshes page.
EDIT:
If you want to have separate API endpoint for incrementing view count, you can implement its increment logic on server side. In that case, it won't change anything in current reducer file ProductReducer.js.
But I think there is no need for it. You can use updateProduct API , just for this reason. No need to change reducer in this case also.
EDIT 2:
If addView API is returning product id and incremented view, then you you can write reducer as -
case actionTypes.VIEW:
return [
...state.map((product) => {
if (product.id === action.product.id) {
product.views = action.product.views;
}
return product;
})
]
So what i did was I added a useEffect() to my ProductDetail.js file and fired the Action from there.
ProductDetail.js
import React,{useEffect} from 'react';
import { Link} from 'react-router-dom';
import {withRouter} from 'react-router'
import {Button, Card} from 'react-bootstrap'
import { connect } from "react-redux";
import * as productActions from "../redux/actions/productActions";
import { bindActionCreators } from "redux";
const ProductDetail=(props)=>{
useEffect(() => {
console.log("PROPIES ",props.location.productName.id+" "+props.location.productName.views)
props.actions.addView(props.location.productName.id,props.location.productName.views)
},[props.actions,props.location.productName.id,props.location.productName.views])
const style={"display":"flex", "justifyContent":"center","alignItems":"center","minHeight":"100vh"}
return(
<div style={style}>
<Card style={{ width: "18rem","borderRadius":"30px" }} >
<Card.Body style={{style}}>
<Card.Title style={{"fontSize":"30px","fontWeight":"bold","display":"flex", "justifyContent":"center"}}> {props.location.productName.name} </Card.Title>
<Card.Text><strong>Quantity :</strong>{props.location.productName.quantity}</Card.Text>
<Card.Text><strong>Price :</strong>{props.location.productName.price}</Card.Text>
<Card.Text><strong>Manufacturer:</strong>{props.location.productName.manufacturer}</Card.Text>
<Card.Text><strong>Description :</strong>{props.location.productName.description}</Card.Text>
<div>
<Link to="/"><Button variant="primary" style={{ height: "6vh","fontWeight":"bold" }}>Back</Button></Link>
</div>
</Card.Body>
</Card>
</div>
);
}
function mapStateToProps(state, ownProps) {
return {
products: state.products,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(productActions, dispatch),
};
}
export default connect(mapStateToProps,mapDispatchToProps)(withRouter(ProductDetail));
It then fires this action
//Increase View Count of product
export function addView(id,count){
console.log("func called")
return function (dispatch){
console.log("api to be called")
return dataApi.addView(id,count)
.then((product)=>{
console.log("dispatched")
dispatch({type:actionTypes.VIEW, id: product.id})
})
}
}
So it updates the view on the server first and then in the reducer state
dataAPI.js
static addView(id,count){
return axios.patch('http://localhost:4000/products/'+id,{views:count+1})
.then(response=>response.data);
}
productReducer.js
import initialState from "./initialState";
import * as actionTypes from "../actions/actionTypes";
export default function productReducer(state = initialState.products, action) {
switch (action.type) {
case actionTypes.INIT:
return action.products;
case actionTypes.ADD:
return [...state, Object.assign({}, action.product)];
case actionTypes.DELETE:
return [...state.filter((product) => product.id !== action.id)];
case actionTypes.UPDATE:
return [
...state.filter((product) => product.id !== action.product.id),
Object.assign({}, action.product),
].sort( (a,b)=>(a.id>b.id)?1:-1 );
case actionTypes.VIEW:
let prod = [...state][action.id-1];
prod.views++;
//eslint-disable-next-line
let addView =()=>( [
...state.filter(product => product.id !== action.id),
Object.assign({}, prod)
])
return state;
default:
return state;
}
}
I had to write the ActionType.VIEW case in switch like this
case actionTypes.VIEW:
let prod = [...state][action.id-1];
prod.views++;
//eslint-disable-next-line
let addView =()=>( [
...state.filter(product => product.id !== action.id),
Object.assign({}, prod)
])
return state;
I had to put the state modification part inside a function called addView(), otherwise I saw that the function was repeatedly getting called infinitly. I'd appreciate it is someone could help me with a around that

React-Redux: Page becomes blank white screen after clicking a button and components do not appear

I am working on a React application and I am using Redux to store the state. I have the following code:
category-arrows.component.jsx:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { increaseCategoryRank, decreaseCategoryRank } from '../../redux/menu/menu.actions';
import './category-arrows.styles.scss';
class CategoryArrows extends Component {
handleClick = (id) => {
this.props.increaseCategoryRank(id);
}
render() {
const { categoryRank, categoryId, increaseCategoryRank, decreaseCategoryRank } = this.props;
return (
<div class="arrows-container">
<div class="up-arrow" onClick={this.handleClick(categoryId)}></div>
<div class="category-rank">
<p>{categoryRank}</p>
</div>
<div class="down-arrow"></div>
</div>
)
}
}
export default connect( { increaseCategoryRank, decreaseCategoryRank } )(CategoryArrows);
menu-category.component.jsx:
import React from 'react';
import { connect } from 'react-redux';
import MenuItem from '../../components/menu-item/menu-item.component';
import MenuCarousel from '../../components/menu-carousel/menu-carousel.component';
import NewItemCard from '../../components/new-item-card/new-item-card.component';
import DeleteCategory from '../../components/delete-category/delete-category.component';
import CategoryArrows from
'../../components/category-arrows/category-arrows.component';
import { editCategory } from '../../redux/menu/menu.actions';
import { MANAGER } from '../../redux/user/user.staff-types';
import './menu-category.styles.scss';
const MenuCategory = ({ currentUser, editCategory, isEditing, ...category }) => {
const isManager = currentUser && currentUser.type === MANAGER;
const editableProps = {
className: isManager ? 'category-editable' : null,
contentEditable: !!isManager,
suppressContentEditableWarning: true
};
return (
<div key={category._id} className='menu-category'>
<h2 {...editableProps} onBlur={event => editCategory({ ...category, name: event.target.innerText })}>
{category.name}
</h2>
<p {...editableProps} onBlur={event => editCategory({ ...category, description: event.target.innerText })} >
{category.description}
</p>
<MenuCarousel>
{isManager && isEditing ? <CategoryArrows className='category-rank-arrows' categoryRank={category.rank} categoryId={category._id} /> : null}
{isManager ? <NewItemCard categoryId={category._id} /> : null}
{category.items.map(menuItem => <MenuItem key={menuItem._id} categoryId={category._id} {...menuItem} />)}
{isManager ? <DeleteCategory name={category.name} categoryId={category._id} className='delete-bin' /> : null}
</MenuCarousel>
</div>
)
}
const mapStateToProps = state => ({
currentUser: state.user.currentUser
})
export default connect(mapStateToProps, { editCategory })(MenuCategory);
menu.component.jsx:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import MenuCategory from '../../components/menu-category/menu-category.component'
import NewCategoryButton from '../../components/new-category-button/new-category-button.component';
import EditMenuButton from '../../components/edit-menu-button/edit-menu-button.component';
import './menu.styles.scss';
class MenuPage extends Component {
state = {
menuEditable: false
}
toggleMenuEditable = () => this.setState({ menuEditable: !this.state.menuEditable })
render() {
return (
<div className='menu-page'>
{this.props.menu ? this.props.menu.map(category => <MenuCategory key={category._id} {...category} isEditing={this.state.menuEditable} />) : null}
<div className='edit-menu-buttons'>
<div className='menu-button'>
{this.props.currentUser ? <NewCategoryButton /> : null}
</div>
<div className='menu-button'>
{this.props.currentUser ? <EditMenuButton onClick={this.toggleMenuEditable} isEditing={this.state.menuEditable} /> : null}
</div>
</div>
</div>
)
}
}
const mapStateToProps = state => ({
currentUser: state.user.currentUser,
menu: state.menu
})
export default connect(mapStateToProps)(MenuPage);
menu.actions.js:
import { INCREASE_CATEGORY_RANK, DECREASE_CATEGORY_RANK } from './menu.types';
export const increaseCategoryRank = categoryId => dispatch => {
dispatch({ type: INCREASE_CATEGORY_RANK, payload: categoryId })
}
export const decreaseCategoryRank = categoryId => dispatch => {
dispatch({ type: DECREASE_CATEGORY_RANK, payload: categoryId })
}
menu.types.js:
export const INCREASE_CATEGORY_RANK = "INCREASE_CATEGORY_RANK";
export const DECREASE_CATEGORY_RANK = "DECREASE_CATEGORY_RANK";
menu.reducer.js:
import INITIAL_STATE from './menu.data';
import { INCREASE_CATEGORY_RANK, DECREASE_CATEGORY_RANK } from './menu.types';
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case INCREASE_CATEGORY_RANK:
console.log(action.payload._id);
return;
case DECREASE_CATEGORY_RANK:
console.log(action.payload._id);
return;
default:
return state;
}
}
menu.data.js:
export default [
{
"_id": "c0daac6ab8954a40606dd8b81d24a0ef",
"name": "Entree",
"rank": "0",
"items": [
{
"title": "Curry Puffs",
"price": 14,
"_id": "615caa7dd573bcf84781c9e4382b520d"
},
{
"title": "Spring Rolls",
"price": 12,
"_id": "542f711856b7854b71d9862797620e23"
},
{
"title": "Tandoori Cauliflower",
"price": 20,
"_id": "f0c0f2fa02e392ad4e74dfaaf6068fb1"
}
]
},
{
"_id": "934aeba1e96e6d6a4207cd5ba207b52a",
"name": "Lunch",
"rank": "1",
"items": [
{
"title": "Spaghetti",
"price": 20,
"_id": "db414e2b9951ed621fbf6fb40df36ee3"
},
{
"title": "Spaghetti",
"price": 20,
"_id": "253592733a8f7835f390d3d9ed8bda95"
},
{
"title": "Spaghetti",
"price": 20,
"_id": "a22741f27a346cda93d3cf752e371779"
}
]
}
]
In the above code, I have a CategoryArrows component that is rendered in the MenuCategory component only if the isEditing value which is passed from the MenuPage component to the MenuCategory component as a props is true. When the EditMenuButton component is clicked, isEditing is set to true, and the CategoryArrows components appear on the menu page.
When the up-arrow div is clicked I want to dispatch the action increaseCategoryRank, which takes the category id as a parameter. I have added code for this action.
However, when I click the EditMenuButton component, the CategoryArrows do not appear and the screen turns blank white. I get the following error when I inspect the page:
Uncaught Error: Invalid value of type object for mapStateToProps argument when connecting component CategoryArrows.
I am not sure what this error means and how to resolve it. Any insights are appreciated.
You have not passed correct parameter inputs to redux connect function in your code.
The connect function first argument must be the mapStateToProps function which returns the reduced state.
export default connect( null, { increaseCategoryRank, decreaseCategoryRank } )(CategoryArrows);
Pass null as the first parameter to connect since you don't seem to pass any props from the redux store.
Please update your category-arrows.component.jsx connect as given above.
Your first argument to connect is an object here:
export default connect( { increaseCategoryRank, decreaseCategoryRank } )(CategoryArrows);
The first argument to connect should be mapStateToProps which is a function. If you want to access dispatch, but don't need state, you need to pass null as first argument to connect:
connect(null, mapStateToDispatch)

ComponentDidMount does not work - props do not pass to child component

I have to pass data from MyWeather to MyWeatherData and it occurs that props does not pass into child component, when I use componentDidMount in MyWeatherData. Now I'm using componentDidUpdate in MyWeatherData and it works, but it produces an error: Can only update a mounted or mounting component. Please check the code for the ... component. Does anybody knows how to correct this?
When I pass: <MyWeatherData lat="53.0038" lon="20.0458"/> it works with ComponentDidMount.
import React, {Component} from "react";
import MyWeatherData from "./MyWeatherData";
export default class MyWeather extends Component {
constructor() {
super();
this.state = {
latitude: "",
longitude: ""
}
this.getMyLocation = this
.getMyLocation
.bind(this)
}
componentDidMount() {
this.getMyLocation()
}
getMyLocation() {
const location = window.navigator && window.navigator.geolocation
if (location) {
location.getCurrentPosition(pos => {
this.setState({latitude: pos.coords.latitude, longitude: pos.coords.longitude})
}, (error) => {
this.setState({latitude: 'err-latitude', longitude: 'err-longitude'})
})
}
}
render() {
const {latitude, longitude} = this.state;
return (
<div>
<MyWeatherData lat={latitude} lon={longitude}/>
</div>
)
}
}
import React, {Component} from "react";
import axios from "axios";
export default class MyWeatherData extends Component {
constructor() {
super();
this.state = {
descriptionMain: "",
description: "",
temperature: null,
weatherIcon: "",
name: ""
}
}
componentDidUpdate = () => {
this.getMyWeather();
}
getMyWeather = () => {
const lat = this.props.lat;
const lon = this.props.lon;
const API_KEY = "e6f4d816d3ade705ec1d8d9701b61e14";
const weatherURL = `https://api.openweathermap.org/data/2.5/weather?APPID=${API_KEY}&units=metric&lat=${lat}&lon=${lon}`;
axios
.get(weatherURL)
.then(res => {
this.setState({descriptionMain: res.data.weather[0].main, description: res.data.weather[0].description, temperature: res.data.main.temp, weatherIcon: res.data.weather[0].icon, name: res.data.name});
})
.catch(error => {
console.log(error);
});
}
render() {
const {descriptionMain, description, temperature, weatherIcon, name} = this.state;
return (
<div>
<h2>Weather for: {name}</h2>
<h4>Sky: {description}</h4>
<h5>Description: {descriptionMain}</h5>
<span className="temperature">{temperature}
°C</span>
{weatherIcon
? (<img
src={`http://openweathermap.org/img/w/${weatherIcon}.png`}
alt={`${description}`}/>)
: null}
</div>
)
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
When getting the information in componentDidMount in the childComponent, you props are passed but since the componentDidMount of the parent is not run by that time the updated props are not available, you should add that part of the code in componentWillReceiveProps which is executed whenever the props change or the parent component rerenders
import React, {Component} from "react";
import MyWeatherData from "./MyWeatherData";
export default class MyWeather extends Component {
constructor() {
super();
this.state = {
latitude: "",
longitude: ""
}
this.getMyLocation = this
.getMyLocation
.bind(this)
}
componentDidMount() {
this.getMyLocation()
}
getMyLocation() {
const location = window.navigator && window.navigator.geolocation
if (location) {
location.getCurrentPosition(pos => {
this.setState({latitude: pos.coords.latitude, longitude: pos.coords.longitude})
}, (error) => {
this.setState({latitude: 'err-latitude', longitude: 'err-longitude'})
})
}
}
render() {
const {latitude, longitude} = this.state;
return (
<div>
<MyWeatherData lat={latitude} lon={longitude}/>
</div>
)
}
}
import React, {Component} from "react";
import axios from "axios";
export default class MyWeatherData extends Component {
constructor() {
super();
this.state = {
descriptionMain: "",
description: "",
temperature: null,
weatherIcon: "",
name: ""
}
}
componentWillReceiveProps(nextProps){
if(nextProps.lat !== this.props.lat || nextProps.lon !== this.props.lon) {
this.getMyWeather(nextProps);
}
}
getMyWeather = (props) => {
const lat = props.lat;
const lon = props.lon;
const API_KEY = "e6f4d816d3ade705ec1d8d9701b61e14";
const weatherURL = `https://api.openweathermap.org/data/2.5/weather?APPID=${API_KEY}&units=metric&lat=${lat}&lon=${lon}`;
axios
.get(weatherURL)
.then(res => {
this.setState({descriptionMain: res.data.weather[0].main, description: res.data.weather[0].description, temperature: res.data.main.temp, weatherIcon: res.data.weather[0].icon, name: res.data.name});
})
.catch(error => {
console.log(error);
});
}
render() {
const {descriptionMain, description, temperature, weatherIcon, name} = this.state;
return (
<div>
<h2>Weather for: {name}</h2>
<h4>Sky: {description}</h4>
<h5>Description: {descriptionMain}</h5>
<span className="temperature">{temperature}
°C</span>
{weatherIcon
? (<img
src={`http://openweathermap.org/img/w/${weatherIcon}.png`}
alt={`${description}`}/>)
: null}
</div>
)
}
}

Categories

Resources