React component does not update automatically in UI but only in array - javascript

I'm a newbie to react and I have a problem. When I try to add a new item to my list, the component updates but "closes", in fact to see the new item added to the list, I have to click again on the router link. At that point it updates. I have this problem in both the 'todos' component and the 'subtodos' component.
How can I do?
Data structure:
const datas = [
{
categoryTitle: "oggi",
categoryId: "1",
todos: [
{
todoName: "pulire",
todoId: "a",
completed: false,
subTodos: [
{ subTodoName: "pulire bagno", subTodoId: "a1", completed: false },
{ subTodoName: "pulire cucina", subTodoId: "a2", completed: false },
],
},
{
todoName: "cucinare",
todoId: "b",
completed: false,
subTodos: [
{ subTodoName: "cucinare primo", subTodoId: "a3", completed: false },
{
subTodoName: "cucinare secondo",
subTodoId: "a4",
completed: false,
},
],
},
],
},
{
categoryTitle: "pianificato",
categoryId: "2",
todos: [
{
todoName: "studiare",
todoId: "c",
completed: false,
subTodos: [
{
subTodoName: "studiare geometria",
subTodoId: "a1",
completed: false,
},
{
subTodoName: "studiare matematica",
subTodoId: "a2",
completed: false,
},
],
},
{
todoName: "spesa",
todoId: "d",
completed: false,
subTodos: [
{ subTodoName: "zucchine", subTodoId: "a3", completed: false },
{ subTodoName: "pasta", subTodoId: "a4", completed: false },
],
},
],
},
];
export default datas;
app.js
import "./App.css";
import {
Routes,
Router,
Route,
NavLink,
createBrowserRouter,
createRoutesFromElements,
RouterProvider,
} from "react-router-dom";
import { createContext, useState } from "react";
import CategoryLayout from "./layouts/CategoryLayout";
import Todos from "./pages/Todos";
import datas from "./datas/datas";
import SubTodos from "./pages/SubTodos";
export const DataContext = createContext();
function App() {
const [todoDatas, setTodoDatas] = useState(datas);
const [selectedItem, setSelectedItem] = useState();
const [todoId, setTodoId] = useState();
const [selectedTodo, setSelectedTodo] = useState();
return (
<DataContext.Provider
value={{
todoDatas,
setTodoDatas,
selectedItem,
setSelectedItem,
todoId,
setTodoId,
selectedTodo,
setSelectedTodo,
}}
>
<Routes>
<Route path="/" element={<CategoryLayout />}>
<Route path="todos" element={<Todos />}>
<Route path="subtodos" element={<SubTodos />} />
</Route>
</Route>
</Routes>
</DataContext.Provider>
);
}
export default App;
category layout
import React, { useContext, useState } from "react";
import { Outlet, Link } from "react-router-dom";
import { DataContext } from "../App";
import uniqid from "uniqid";
function CategoryLayout() {
const { todoDatas, setTodoDatas, setSelectedItem } = useContext(DataContext);
const [categoryInput, setcategoryInput] = useState(null);
const catchCategory = (value) => {
setcategoryInput(value);
};
const resetCategory = () => {
setcategoryInput("");
};
const addCategory = () => {
if (
categoryInput !== null &&
categoryInput !== "" &&
categoryInput !== undefined
) {
setTodoDatas((prev) => {
return [
...prev,
{
categoryTitle: categoryInput,
categoryId: uniqid(),
todos: [],
},
];
});
}
};
return (
<div className="app_wrap">
<div className="categories_wrap">
<h5>jessicamoretti#gmail.com</h5>
<input type="text" placeholder="search"></input>
{todoDatas?.map((category) => {
return (
<Link
to="todos"
key={category.categoryId}
id={category.categoryId}
onClick={() => {
setSelectedItem(
todoDatas.find((cat) => {
return cat.categoryId === category.categoryId;
})
);
}}
>
{category.categoryTitle}
</Link>
);
})}
<form
onClick={(e) => {
e.preventDefault();
addCategory();
}}
>
<input
type="text"
onChange={(e) => {
catchCategory(e.target.value);
}}
></input>
<button onClick={resetCategory}>Nuovo elenco</button>
</form>
</div>
<section>
<Outlet />
</section>
</div>
);
}
export default CategoryLayout;
todos
import React, { useEffect } from "react";
import { useParams } from "react-router-dom";
import { DataContext } from "../App";
import { useContext, useState } from "react";
import { Link, Outlet } from "react-router-dom";
import uniqid from "uniqid";
function Todos() {
const { setSelectedItem, selectedItem, todoId, setTodoId } =
useContext(DataContext);
const [todoInput, setTodoInput] = useState();
const catchTodo = (value) => {
setTodoInput(value);
};
const resetTodo = () => {
setTodoInput("");
};
const addTodo = (obj) => {
if (todoInput !== null && todoInput !== "" && todoInput !== undefined) {
setSelectedItem(
selectedItem?.todos?.push({
todoName: todoInput,
todoId: uniqid(),
completed: false,
subTodos: [],
})
);
}
};
console.log("todoid!", todoId);
return (
<div className="todos_subtodos_wrap">
<div className="todos_wrap">
{selectedItem?.todos?.map((todo) => {
return (
<div className="todo" key={todo.todoId}>
<Link
to="subtodos"
id={todo.todoId}
onClick={() => {
setTodoId(todo.todoId);
}}
>
{todo.todoName}
</Link>
<p>subtodo: 0 of {todo.subTodos?.length} </p>
</div>
);
})}
<form
className="todos_form"
onClick={(e) => {
e.preventDefault();
addTodo();
}}
>
<input
type="text"
className="todos_input"
placeholder="add todos"
onChange={(e) => {
catchTodo(e.target.value);
}}
></input>
<button className="add_todos" onClick={resetTodo}>
add todos
</button>
</form>
</div>
<Outlet />
</div>
);
}
export default Todos;
subtodos
import React, { useEffect } from "react";
import { useContext, useState } from "react";
import { DataContext } from "../App";
import uniqid from "uniqid";
function SubTodos() {
const {
setSelectedItem,
selectedItem,
todoId,
setTodoId,
selectedTodo,
setSelectedTodo,
} = useContext(DataContext);
const [subTodoInput, setSubTodoInput] = useState();
const [subTodos, setSubTodos] = useState(selectedTodo);
let selected = selectedItem?.todos?.find((el) => {
return el.todoId === todoId;
});
console.log(selectedItem, "selected item");
useEffect(() => {
setSelectedTodo(selected); // contiene il todo selezionato
}, [selected, setSelectedTodo]);
const catchSubTodo = (value) => {
setSubTodoInput(value);
};
const addSub = () => {
if (
subTodoInput !== null &&
subTodoInput !== "" &&
subTodoInput !== undefined
) {
setSelectedTodo(
selected?.subTodos?.push({
subTodoName: subTodoInput,
subTodoId: uniqid(),
completed: false,
})
);
}
};
return (
<div>
<div>
{selectedTodo?.subTodos?.map((el) => {
return (
<div key={el.subTodoId}>
<p> {el.subTodoName}</p>
</div>
);
})}
<form
onClick={(e) => {
e.preventDefault();
addSub();
}}
>
<input
type="text"
placeholder="+ Aggiungi sottoattività"
onChange={(e) => {
e.preventDefault();
catchSubTodo(e.target.value);
}}
></input>
<button
onClick={(e) => {
e.preventDefault();
}}
></button>
</form>
</div>
</div>
);
}
export default SubTodos;

Related

User preferred language saved but the page content language didn't change only after I reload

I'm using ant design pro.
The idea is In the platform we have 2 languages to choose from Fr(French) and En(English),
I want the user when he logs in and change the language to English for example when he logs out and log in again the language should be saved to English so he would be able to see the content in English, I managed to do it, when i login in the backend the preferedLanguage = en, the language toggle in the also changes to en, the only problem the web page content stays in French it's only change in English when i reload the page.
I think the issue is related to the login page, the login page is set to French as default , let's say my preferred language now is English, if i login from the login page. The page content loaded in French only changes when i reload.
-This is the umijs documentation : https://umijs.org/docs/max/i18n#setlocale-%E8%AE%BE%E7%BD%AE%E8%AF%AD%E8%A8%80
-LanguageDropdown (the toggle where you select the language (Fr or En)
import type { FC } from 'react';
import React, { useState, useEffect } from 'react';
import { Menu, Dropdown } from 'antd';
import { CaretDownOutlined, CaretUpOutlined, GlobalOutlined } from '#ant-design/icons';
import styles from './index.less';
import { setLocale, getLocale, getAllLocales } from 'umi';
interface Props {
setUpdateLang: any;
currentLang: string;
}
const LanguageDropdown: FC<Props> = ({ currentLang, setUpdateLang }) => {
const [langvisible, setLangVisible] = useState<boolean>(false);
const [localesList, setLocalesList] = useState<any[]>([]);
const [currentLocale, setCurrentLocale] = useState<string>('');
// useEffect(() => {
// if (currentLang) {
// setLocalesList(getAllLocales());
// setCurrentLocale(getLocale());
// setLocale(currentLang === 'fr' ? 'fr-FR' : 'en-US');
// }
// alert(currentLang);
// alert(getLocale());
// }, [currentLang]);
useEffect(() => {
setLocalesList(getAllLocales());
setCurrentLocale(getLocale());
if (currentLang) {
// alert(222);
const selectedLang = currentLang === 'fr' ? 'fr-FR' : 'en-US';
// setNewLang(selectedLang);
setLocale(selectedLang, false);
setCurrentLocale(getLocale());
}
}, [currentLang]);
const onLangVisibleChange = (visibleLang: boolean) => {
setLangVisible(visibleLang);
};
const langNameHandler = (lang: string) => {
if (lang === 'en-US') return 'EN';
else if (lang === 'fr-FR') return 'FR';
return 'FR';
};
const setNewLang = (lang: string) => {
setUpdateLang({ lang: langNameHandler(lang).toLocaleLowerCase(), updated: true });
setLocale(lang);
};
const Langmenu = (
<Menu>
{localesList?.map((lang: any) => (
<Menu.Item key={lang}>
<a onClick={() => setNewLang(lang)}>{langNameHandler(lang)}</a>
</Menu.Item>
))}
</Menu>
);
return (
<div className={styles.profileDropdownContainer}>
<Dropdown
overlay={Langmenu}
placement="bottomLeft"
trigger={['click']}
onVisibleChange={onLangVisibleChange}
className={styles.dropdown}
>
<div className={styles.langContainer}>
<span>
<GlobalOutlined /> {langNameHandler(currentLocale)}
</span>
{!langvisible ? <CaretDownOutlined /> : <CaretUpOutlined />}
</div>
</Dropdown>
</div>
);
};
export default LanguageDropdown;
-RightContext
import { Tag } from 'antd';
import type { Settings as ProSettings } from '#ant-design/pro-layout';
import React, { useEffect, useState } from 'react';
import type { ConnectProps } from 'umi';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import type { ConnectState } from '#/models/connect';
import Avatar from './AvatarDropdown';
import styles from './index.less';
import LanguageDropdown from '../languageDropdown';
import moment from 'moment';
export type GlobalHeaderRightProps = {
dispatch: Dispatch;
theme?: ProSettings['navTheme'] | 'realDark';
auth: any;
users: any;
platformLanguage: any;
data: any;
} & Partial<ConnectProps> &
Partial<ProSettings>;
const ENVTagColor = {
dev: 'orange',
test: 'green',
pre: '#87d068',
};
const GlobalHeaderRight: React.FC<GlobalHeaderRightProps> = (props) => {
const [updateLang, setUpdateLang] = useState<{ lang: string; updated: boolean }>({
lang: '',
updated: false,
});
const [currentLang, setCurrentLang] = useState<any>(null);
const { theme, layout, auth, platformLanguage, data, dispatch } = props;
let className = styles.right;
useEffect(() => setCurrentLang(platformLanguage), [platformLanguage]);
useEffect(() => {
if (updateLang.updated) {
const {
organization,
roles,
email,
deleted,
department,
createdById,
organizationId,
...rest
} = data;
const birthdate = moment(rest.birthdate).format('YYYY-MM-DD');
const workversary = moment(rest.workversary).format('YYYY-MM-DD');
dispatch({
type: 'users/updateMe',
payload: {
data: { ...rest, birthdate, workversary, platformLanguage: updateLang.lang },
userId: auth?.currentUser.id,
},
});
setUpdateLang({ ...updateLang, updated: false });
setCurrentLang(updateLang.lang);
}
}, [updateLang]);
if (theme === 'dark' && layout === 'top') {
className = `${styles.right} ${styles.dark}`;
}
return (
<div className={className}>
{currentLang ? (
<LanguageDropdown currentLang={currentLang} setUpdateLang={setUpdateLang} />
) : null}
<Avatar />
{REACT_APP_ENV && (
<span>
<Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
</span>
)}
</div>
);
};
export default connect(({ settings, auth, users }: ConnectState) => ({
theme: settings.navTheme,
layout: settings.layout,
auth,
users,
platformLanguage: auth?.currentUser?.membership?.platformLanguage,
data: auth?.currentUser?.membership,
}))(GlobalHeaderRight);
-LoginLayout
import React, { useEffect, useState } from 'react';
import type { ConnectState } from '#/models/connect';
import type { MenuDataItem } from '#ant-design/pro-layout';
import { getMenuData, getPageTitle } from '#ant-design/pro-layout';
import { useIntl, connect } from 'umi';
import type { ConnectProps } from 'umi';
import { Col, Row } from 'antd';
import { Helmet, HelmetProvider } from 'react-helmet-async';
import LoginImage from '../assets/loginImage.png';
import SignUpAdminImage from '../assets/adminSignup.svg';
import styles from './LoginLayout.less';
import LanguageDropdown from '#/components/languageDropdown';
import SignupSideText from '#/components/SignupSideText';
export type UserLayoutProps = {
breadcrumbNameMap: Record<string, MenuDataItem>;
} & Partial<ConnectProps>;
const LoginLayout: React.FC<UserLayoutProps> = (props) => {
const [layoutImage, setLayoutImage] = useState(LoginImage);
const [updateLang, setUpdateLang] = useState<{ lang: string; updated: boolean }>({
lang: '',
updated: false,
});
const [currentLang, setCurrentLang] = useState<any>('');
useEffect(() => {
if (updateLang.updated) {
setUpdateLang({ ...updateLang, updated: false });
setCurrentLang(updateLang.lang);
}
}, [updateLang]);
useEffect(() => {
if (window.location.pathname === '/user/adminSignup/step1') {
setLayoutImage(SignUpAdminImage);
} else if (window.location.pathname === '/user/adminSignup/step2') {
setLayoutImage('TextSignup');
} else setLayoutImage(LoginImage);
}, [window.location.pathname]);
const {
route = {
routes: [],
},
} = props;
const { routes = [] } = route;
const {
children,
location = {
pathname: '',
},
} = props;
const { formatMessage } = useIntl();
const { breadcrumb } = getMenuData(routes);
const title = getPageTitle({
pathname: location.pathname,
formatMessage,
breadcrumb,
...props,
});
return (
<>
<HelmetProvider>
<Helmet>
<title>{title}</title>
<meta name="description" content={title} />
</Helmet>
<div className={styles.container}>
<Col
xl={{ span: 12, order: 1 }}
xs={{ span: 0, order: 2 }}
md={{ span: 0, order: 2 }}
lg={{ span: 0, order: 2 }}
style={{ backgroundColor: '#00bfa5' }}
>
{layoutImage === 'TextSignup' ? (
<SignupSideText />
) : (
<img alt="logo" width="100%" height="100%" src={layoutImage} />
)}
</Col>
<Col
xl={{ span: 12, order: 2 }}
lg={{ span: 24, order: 2 }}
sm={{ span: 24, order: 1 }}
xs={{ span: 24, order: 1 }}
>
{' '}
<Row justify="end" className="languageRow">
<LanguageDropdown currentLang={currentLang} setUpdateLang={setUpdateLang} />
</Row>
{children}
</Col>
</div>
</HelmetProvider>
</>
);
};
export default connect(({ settings }: ConnectState) => ({ ...settings }))(LoginLayout);

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)

make like "is this page useful " by reaction icons react

i need to make like reaction icon , when someone clicks in one icon the counter increase one and when he click to other icons it will decrease the previous icons then increase what wh click on .
so this is my code
it's will look like emojis with conuter for each one and you need to click to one of these emoji , then increase the count one .
import logo from './logo.svg';
import React, { useState } from 'react';
import './App.css';
let emojis = [
{
"id":"0",
"reactionName": "disLike",
"pic": "logo",
"PreCounter":20,
},
{
"id":"1",
"reactionName": "like",
"pic": "logo",
"PreCounter":2,
},
{
"id":"2",
"reactionName": "disLike",
"pic": "logo",
"PreCounter":0,
},
{
"id":"3",
"reactionName": "like",
"pic": "logo",
"PreCounter":20,
},]
function App() {
return (
<div className="App">
{
emojis.map(({id,reactionName, pic,PreCounter}) => {
return <Emoji
key={id}
reactionName={reactionName}
pic={pic}
PreCounter={PreCounter}
/>
})
}
</div>
);
}
export default App;
function Emoji (props){
const { key,reactionName, pic,PreCounter } = props;
const [count, setCounter] = useState(PreCounter);
const [selectedName, setSelectedName] = useState("noReaction");
const [selected, setSelected] = useState(false);
const handleClick = (e) => {
setSelectedName(e.target.getAttribute("name"));
if (selected) {
setCounter(count - 1);
setSelected(false);
}else {
setCounter(count + 1);
setSelected(true);
}
};
return(
<button onClick={handleClick} name={reactionName} value={count} id={key}>
<img src={pic} alt="logo" width="20"/>
{count}
</button>
);
}
I couldn't know how I can change the value of the previous click ,
I don't know if this is what you want
function Emoji(props) {
const { id, reactionName, pic, PreCounter, handleClick } = props;
return (
<button onClick={handleClick} name={reactionName+id} value={PreCounter} id={id}>
{/* <img src={pic} alt="logo" width="20"/> */}
{PreCounter}
</button>
);
}
let emojis = [
{
id: '0',
reactionName: 'disLike',
pic: 'logo',
PreCounter: 20,
},
{
id: '1',
reactionName: 'like',
pic: 'logo',
PreCounter: 2,
},
{
id: '2',
reactionName: 'disLike',
pic: 'logo',
PreCounter: 0,
},
{
id: '3',
reactionName: 'like',
pic: 'logo',
PreCounter: 20,
},
];
function useAppCount() {
const [list, listCallBack] = useState(emojis)
return [list, listCallBack]
}
function App() {
const [list, listCallBack] = useAppCount()
const handleClick = e => {
const id = e.target.getAttribute('id')
const data = list.map(r => {
if (r.isClick) {
r.isClick = false
r.PreCounter -= 1
}
if (r.id === id) {
r.isClick = true
r.PreCounter += 1
}
return r
})
listCallBack(() => data)
}
return (
<div className="App">
{list.map(({ id, reactionName, pic, PreCounter }) => {
return <Emoji key={id} id={id} reactionName={reactionName} pic={pic} PreCounter={PreCounter} handleClick={handleClick} />;
})}
</div>
);
}
export default App;

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

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

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: {},
};

Categories

Resources