Material-UI Checkbox not work with Redux store - javascript

The source can be accessed in this repo
I used Redux store to update the checkbox's check flag, and I can see that the state is perfectly changing, but things does not get applied to React components.
I think everything is fine, but checkbox is not updated when I change the state coupled with check.
Redux store used is located in src/redux/modules/menu.js, and checkbox-related action creator is checkMenuNameList function.
Also, checkbox code can be found in src/containers/MenuEditContainer.js.
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as actions from '../redux/modules/menu'
import PageWrapper from '../components/base/PageWrapper'
import MenuEditWrapper from '../components/common/templates/MenuEditWrapper'
import MenuEditContent from '../components/common/content/MenuEditContent'
const MenuEditContainer = (props) => {
const handleInputChange = name => event => {
props.actions.changeInput({
key: name,
value: event.target.value
})
props.actions.generateMenuList(event.target.value)
}
const handleMenuNameCheckbox = index => event => {
props.actions.checkMenuNameList(index)
}
return (
<>
<PageWrapper>
<MenuEditWrapper>
<MenuEditContent
menuName={props.menuName}
menuPrice={props.menuPrice}
menuNameList={props.menuNameList}
handleInputChange={handleInputChange}
handleMenuNameCheckbox={handleMenuNameCheckbox}
/>
</MenuEditWrapper>
</PageWrapper>
</>
)
}
const mapStateToProps = ({ menu }) => ({
menuId: menu.menuId,
menuName: menu.menuName,
menuPrice: menu.menuPrice,
menuNameList: menu.menuNameList,
menuNameListChosen: menu.menuNameListChosen,
})
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actions, dispatch)
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(MenuEditContainer)
import React from 'react'
import { withRouter } from 'react-router-dom'
import Paper from '#material-ui/core/Paper'
import Typography from '#material-ui/core/Typography'
import TextField from '#material-ui/core/TextField'
import FormGroup from '#material-ui/core/FormGroup'
import FormControlLabel from '#material-ui/core/FormControlLabel'
import Checkbox from '#material-ui/core/Checkbox'
import PageTitle from '../../typography/PageTitle'
import AddButtonSet from '../../button/AddButtonSet'
import EditButtonSet from '../../button/EditButtonSet'
import { makeStyles } from '#material-ui/core/styles'
const useStyles = makeStyles(theme => ({
root: {
padding: theme.spacing(3, 2)
},
title: {
fontWeight: 200,
marginBottom: '1.5rem'
},
textField: {
marginLeft: theme.spacing(0.5),
marginRight: theme.spacing(1),
width: 200,
display: 'block'
},
}))
const MenuEditContent = (props) => {
const classes = useStyles()
const { match, location, history } = props
return (
<Paper className={classes.root}>
<PageTitle>
{
(location.pathname === '/menu/edit/new')
? (`새로운 메뉴 등록`)
// 기존 메뉴 수정시
: (match.params.menuId)
? (`메뉴 수정`)
: (``)
}
</PageTitle>
<TextField
id="menuName"
label="메뉴 이름"
type="search"
className={classes.textField}
margin="normal"
variant="outlined"
autoComplete="off"
value={props.menuName}
onChange={props.handleInputChange('menuName')}
/>
<TextField
id="menuPrice"
label="가격"
type="search"
className={classes.textField}
margin="normal"
variant="outlined"
autoComplete="off"
value={props.menuPrice}
onChange={props.handleInputChange('menuPrice')}
/>
{
(props.menuNameList.length > 0) && (
<FormGroup>
{
props.menuNameList.map((item, index) => (
<FormControlLabel
key={index}
control={
<Checkbox
checked={item.checked}
value={index}
onChange={props.handleMenuNameCheckbox(index)}
/>
}
label={item.name}
/>
))
}
</FormGroup>
)
}
{
(location.pathname === '/menu/edit/new')
? (
<AddButtonSet
onClickCreate={() => alert('create button!')}
onClickCancel={() => history.push('/menu')}
/>
)
: (match.params.menuId)
? (
<EditButtonSet
onClickUpdate={() => alert('update button!')}
onClickDelete={() => alert('delete button!')}
onClickCancel={() => history.push('/menu')}
/>
)
: (<></>)
}
</Paper>
)
}
export default withRouter(MenuEditContent)

Reason why my checkbox did not work was because I re-used the previous state incorrectly.
// redux/modules/menu.js
const updateMenuNameList = createAction(UPDATE_MENU_NAME_LIST, payload => ({ menuNameList: payload.menuNameList }))
export const checkMenuNameList = (index) => (dispatch, getState) => {
const { menu: { menuNameList } } = getState()
const previousStatus = menuNameList[index].checked
menuNameList[index].checked = !previousStatus
dispatch(onCheckMenuNameList({ updatedMenuNameList: menuNameList }))
}
In above example, I fetched previous state from getState(), and extract menuNameList from it. menuNameList's structure looks like below:
[
{
name: String,
checked: Boolean
}
]
Each checkbox uses this array to display name and determine whether it is checked or not. When I click any checkbox, handler will change the clicked checkbox's checked value.
The problem arise from here: I accidentally re-used objects from the previous menuNameList, and updated it only with certain checked value changed. This is not a correct approach, because even if an inside property is changed, Redux or React has no idea what have changed! React and Redux figure out state changes with shallow comparison of object. So, even if store changed, React does not render the view, and changes does not get applied to the view!
To avoid this problem, we should make a new object for menuNameList. Maybe it is recommended to use library like Immutable.js.
// redux/modules/menu.js
export const checkMenuNameList = (idx) => (dispatch, getState) => {
const { menu: { menuNameList } } = getState()
// deep copy
const newMenuNameList = menuNameList.map((item, index) => {
if (index !== idx) {
const newMenu = {
name: item.name,
checked: item.checked
}
return newMenu
} else {
const newMenu = {
name: item.name,
checked: !item.checked
}
return newMenu
}
})
dispatch(onCheckMenuNameList({ updatedMenuNameList: newMenuNameList }))
}

Related

How to add a check icon with MUI or Tailwind CSS to all the selected value in the dropdown list?

I want to add a small check/tick icon beside the value eg: Operations ✓ if user selects operations in the TopicList dropdownlist. The TopicList is a class component to get the data from the database which consist of:
Operations, Array, Repetition, Function, Selection. If user selects 2 different values then eg:
Operations ✓
Array ✓
Repetition
Function
Selection.
How can I modify this code to solve this issue?
Here is an image of how I would like it to be if user selects Array.
import axios from "axios";
import React, { useState } from "react";
import TopicList from "../Components/topicList";
import CheckIcon from '#mui/icons-material/Check';
function CreateEvent(success, message) {
const navigate = useNavigate();
const [selectedValues, setSelectedValues] = useState([]); // array to store selected values
const getDataFromTopicList = (val) => {
if (val !== "" && !selectedValues.includes(val)) {
setSelectedValues([...selectedValues, val]);
}
};
const handleSubmit = (event) => {
event.preventDefault();
console.log(selectedValues); // selectedValues array contains all selected values from all TopicList components
axios({
method: "POST",
url: BASE_URL + "events/submitEvent",
data: {
topicId: selectedValues,
},
headers: { "Content-Type": "application/json" },
})
.then((response) => {
if (response.status === 200) {
toast.success("Successfully Created", {
position: toast.POSITION.TOP_CENTER,
});
} else {
toast.error(response.data.message, {
position: toast.POSITION.TOP_CENTER,
});
}
})
.catch((err) => {
if (err.response) {
toast.error(err.response.data.message, {
position: toast.POSITION.TOP_CENTER,
});
} else {
toast.error("Failed to Create", {
position: toast.POSITION.TOP_CENTER,
});
}
});
};
return (
<div className="">
<div>
<form
onSubmit={handleSubmit}
>
<h1>
Create an Event
</h1>
<div>
<div>
<TopicList
selectedValues={selectedValues}
getDataFromTopicList={getDataFromTopicList}
/>
</div>
</div>
</form>
</div>
</div>
);
}
export default CreateEvent;
import axios from "axios";
import React from "react";
import CheckIcon from "#mui/icons-material/Check";
const BASE_URL = process.env.REACT_APP_BASE_URL;
export default class TopicList extends React.Component {
state = {
topics: [],
};
componentDidMount() {
axios.get(BASE_URL + `events/topic`).then((res) => {
const topics = res.data;
this.setState({ topics });
});
}
render() {
return (
<select required onChange={(val) => this.getCat(val.target.value)}>
{this.state.topics.map((topic) => (
<option value={topic.topicId}>
{topic.topic}
{this.props.selectedValues.includes(topic.topicId) && <CheckIcon style={{ color: "green" }} />}
</option>
))}
</select>
);
}
}
It seems that the posted code has MUI but is not using its components in TopicList.
Here is a basic example that makes use of the MUI Select, and with implementation of the needed CheckIcon on multiple selected values.
Minimized demo of below example: stackblitz
The example omitted the data fetching part for simplicity, but it keeps the same topics state and get props for selectedValues in TopicList.
The update function for selectedValues also changed for MUI component but the state value itself remained to be an array of selected topicId, so hopefully it could be wired up for the data fetching without large refectoring of existing code.
Imports for MUI components in TopicList:
import OutlinedInput from '#mui/material/OutlinedInput';
import InputLabel from '#mui/material/InputLabel';
import MenuItem from '#mui/material/MenuItem';
import FormControl from '#mui/material/FormControl';
import ListItemText from '#mui/material/ListItemText';
import ListItemIcon from '#mui/material/ListItemIcon';
import Select from '#mui/material/Select';
import CheckIcon from '#mui/icons-material/Check';
The output of modified TopicList:
<FormControl sx={{ m: 3, width: 300 }}>
<InputLabel id="topic-form-input-label">Topic</InputLabel>
<Select
labelId="topic-form-label"
id="multiple-topic-select"
multiple
value={this.props.selectedValues}
onChange={this.props.onChange}
input={<OutlinedInput label="Topic" />}
renderValue={(selected) =>
selected
.map((topicId) => {
const selectedTopic = this.state.topics.find(
(item) => item.topicId === topicId
);
return selectedTopic ? selectedTopic.topic : topicId;
})
.join(", ")
}
>
{this.state.topics.map((topic) => {
const selected = this.props.selectedValues.includes(topic.topicId);
return (
<MenuItem key={topic.topicId} value={topic.topicId}>
{selected && (
<ListItemIcon>
<CheckIcon style={{ color: "green" }} />
</ListItemIcon>
)}
<ListItemText inset={!selected} primary={topic.topic} />
</MenuItem>
);
})}
</Select>
</FormControl>
Selected values passed from the parent component:
const [selectedValues, setSelectedValues] = useState([]);
const handleChange = (event) => {
const {
target: { value },
} = event;
setSelectedValues(typeof value === "string" ? value.split(",") : value);
};
<TopicList
selectedValues={selectedValues}
onChange={handleChange}
/>
Here you can see an example of a select with checkbox inside it.
to see the code just click the link.

How to show values from array in content of SweetAlert2 [duplicate]

My context is as follows:
import React, {createContext, useEffect, useState} from "react";
export const CartContext = createContext();
const CartContextProvider = (props) => {
const [cart, setCart] = useState(JSON.parse(localStorage.getItem('cart')) || []);
useEffect(() => {
localStorage.setItem('cart', JSON.stringify(cart));
}, [cart]);
const updateCart = (productId, op) => {
let updatedCart = [...cart];
if (updatedCart.find(item => item.id === productId)) {
let objIndex = updatedCart.findIndex((item => item.id === productId));
if (op === '-' && updatedCart[objIndex].qty > 1) {
updatedCart[objIndex].qty -= 1;
} else if (op === '+') {
updatedCart[objIndex].qty += 1;
}
} else {
updatedCart.push({id: productId, qty: 1})
}
setCart(updatedCart);
}
const removeItem = (id) => {
setCart(cart.filter(item => item.id !== id));
};
return (
<CartContext.Provider value={{cart, updateCart, removeItem}}>
{props.children}
</CartContext.Provider>
)
};
export default CartContextProvider;
App.js:
import React from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import NavigationBar from "./components/layout/navigationBar/NavigationBar";
import Homepage from "./pages/homepage/Homepage";
import AboutUsPage from "./pages/aboutUs/AboutUsPage";
import ContactPage from "./pages/contact/ContactPage";
import SearchPage from "./pages/search/SearchPage";
import ShoppingCart from "./components/layout/shoppingCart/ShoppingCart";
import CartContextProvider from "./context/CartContext";
function App() {
return (
<div>
<CartContextProvider>
<Router>
<NavigationBar/>
<ShoppingCart/>
<Routes>
<Route exact path="/" element={<Homepage/>}/>
<Route path="/a-propos" element={<AboutUsPage/>} />
<Route path="/contact" element={<ContactPage/>}/>
<Route path="/recherche" element={<SearchPage/>}/>
</Routes>
</Router>
</CartContextProvider>
</div>
);
}
export default App;
In the component ShoppingCart I am using another component ShoppingCartQuantity which in turn makes use of the context. It works as it should.
Here's the ShoppingCartQuantity component:
import React, {useContext} from "react";
import {CartContext} from "../../../context/CartContext";
import styles from './ShoppingCartQuantity.module.css'
const ShoppingCartQuantity = ({productId}) => {
const {cart, updateCart} = useContext(CartContext);
let qty = 0;
if (cart.find((item => item.id === productId))) {
let objIndex = cart.findIndex((item => item.id === productId));
qty = cart[objIndex].qty;
}
return (
<div>
<span>
<span className={`${styles.op} ${styles.decrementBtn}`} onClick={() => updateCart(productId, '-')}>-</span>
<span className={styles.qty}>{qty}</span>
<span className={`${styles.op} ${styles.incrementBtn}`} onClick={() => updateCart(productId, '+')}>+</span>
</span>
</div>
)
}
export default ShoppingCartQuantity;
Now I am trying to use the ShoppingCartQuantity component in the Homepage component which is a route element (refer to App.js) but getting the error Uncaught TypeError: Cannot destructure property 'cart' of '(0 , react__WEBPACK_IMPORTED_MODULE_0__.useContext)(...)' as it is undefined.
So the context is working for components outside the router but not for those inside it. If I have wrapped the router within the provider, shouldn't all the route elements get access to the context or am I missing something?
UPDATE
As user Build Though suggested in the comments, I tried using the ShoppingCartQuantity component in another route element and it works fine; so the problem is not with the router!
Below is the code of how I am using the ShoppingCartQuantity component in the Homepage component:
import React, { useState, useEffect, useRef } from "react";
import { Responsive, WidthProvider } from "react-grid-layout";
import Subcat from "../../components/subcat/Subcat";
import CategoryService from "../../services/api/Category";
import SubCategoryService from "../../services/api/SubCategory";
import CategoriesLayout from "../../utils/CategoriesLayout";
import CategoryCard from "../../components/category/CategoryCard";
import { Triangle } from 'react-loader-spinner'
import ScrollIntoView from 'react-scroll-into-view'
import ProductService from "../../services/api/Product";
import Swal from 'sweetalert2'
import withReactContent from 'sweetalert2-react-content';
import YouTube from 'react-youtube';
import FavoriteBtn from "../../components/favorite/FavoriteBtn";
import ShoppingCartQuantity from "../../components/layout/shoppingCart/ShoppingCartQuantity";
import "./Homepage.css";
import "../../components/product/ProductModal.css"
import "react-loader-spinner";
import modalStyles from "../../components/product/ProductModal.module.css"
function Homepage() {
const [categories, setCategories] = useState([]);
const [subCats, setSubCats] = useState([]);
const [loader, setLoader] = useState(false);
const ResponsiveGridLayout = WidthProvider(Responsive);
const scrollRef = useRef();
const productModal = withReactContent(Swal);
const opts = {
// height: '390',
// width: '640',
playerVars: {
autoplay: 1,
}
};
useEffect(() => {
CategoryService.get().then((response) => {
setCategories(response);
});
}, []);
function showSubCatsHandler(catId) {
setLoader(true);
setSubCats([]);
SubCategoryService.get(catId).then((response) => {
setSubCats(response.data);
setLoader(false);
scrollRef.current.scrollIntoView({ behavior: "smooth" });
});
}
function showProductPopupHandler(productId) {
ProductService.get(productId).then((response) => {
const product = response.data;
return productModal.fire({
html:
<div>
<h3 className={modalStyles.header}>{product.AMP_Title}</h3>
<h4 className={`${modalStyles.price} ${modalStyles.header}`}>{"CHf " + product.AMP_Price}</h4>
<img className={modalStyles.image} src={process.env.REACT_APP_BACKEND_BASE_URL + 'images/products/' + product.AMP_Image} />
{
product.descriptions.map((desc, _) => (
<div key={desc.AMPD_GUID}>
{
desc.AMPD_Title === '1' && <h4 className={modalStyles.header}>{product.AMP_Title}</h4>
}
{
desc.AMPD_Image !== '' && <img src={process.env.REACT_APP_BACKEND_BASE_URL + 'images/descriptions/' + desc.AMPD_Image} className={desc.AMPD_Alignment === 'left' ? modalStyles.descImageLeft : modalStyles.descImageRight} />
}
<p className={modalStyles.description}>{desc.AMPD_Description}</p>
</div>
))
}
<br/>
<div>
<FavoriteBtn productId={product.AMP_GUID}/>
<ShoppingCartQuantity productId={product.AMP_GUID} />
</div>
<br/>
{
product.AMP_VideoId !== '' &&
<YouTube
videoId={product.AMP_VideoId}
opts={opts}
/>
}
</div>,
showConfirmButton: false,
showCloseButton: true
});
});
}
return (
<div>
<div className="categories-container">
<ResponsiveGridLayout
className="layout"
layouts={ CategoriesLayout }
breakpoints={ { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 } }
cols={ { lg: 8, md: 8, sm: 6, xs: 4, xxs: 2 } }
isDraggable={ false }
>
{
categories.map((cat, index) => (
<div key={index}>
<CategoryCard
category_id = {cat.AMC_GUID}
image = {cat.AMC_Image}
showSubCatsHandler = {showSubCatsHandler}
/>
</div>
))
}
</ResponsiveGridLayout>
{
loader &&
<Triangle
height="100"
width="100"
color='#bcad70'
ariaLabel='loading'
wrapperClass="loader"
/>
}
<div ref={scrollRef}>
{
Object.keys(subCats).map((keyName, _) => (
<Subcat
key={subCats[keyName].AMSC_GUID}
title={ subCats[keyName].AMSC_Title }
products={ subCats[keyName].products }
showProductPopupHandler = {showProductPopupHandler}
/>
))
}
</div>
</div>
</div>
);
}
export default Homepage;
I am using the component in a SweetAlert popup. I guess it's the SweetAlert component that is not getting access to the context. Does anyone have an idea how to pass the context to the SweetAlert component?
UPDATE 2
The accepted solution works great except for 1 small issue: the ShoppingCartQuantity component was not re-rendering inside the SweetAlert popup and the qty would not change visually.
I updated the component by using the qty as a state.
const ShoppingCartQuantity = ({ qty, productId, updateCart }) => {
const [quantity, setQuantity] = useState(qty);
const updateCartHandler = (productId, amount) => {
updateCart(productId, amount);
setQuantity(Math.max(quantity + amount, 1));
}
return (
<div>
<span>
<span
className={`${styles.op} ${styles.decrementBtn}`}
onClick={() => updateCartHandler(productId, -1)}
>
-
</span>
<span className={styles.qty}>{quantity}</span>
<span
className={`${styles.op} ${styles.incrementBtn}`}
onClick={() => updateCartHandler(productId, 1)}
>
+
</span>
</span>
</div>
)
}
Issue
It's very likely that the sweet alert component is rendered outside your app, and thus, outside the CartContextProvider provider. I just searched the repo docs if there is a way to specify a root element, but this doesn't seem possible since this sweet alert code isn't React specific.
See this other similar issue regarding accessing a Redux context in the alert.
Solution
It doesn't seem possible ATM to access the context value from within the modal, so IMHO a workaround could be to refactor your ShoppingCartQuantity component into a wrapper container component to access the context and a presentation component to receive the context values and any callbacks.
I suggest also just passing the amount you want to increment/decrement the quantity by to updateCart instead of passing a "+"/"-" string and operator comparison.
Example:
export const withShoppingCartContext = Component => props => {
const { cart, removeItem, updateCart } = useContext(CartContext);
return <Component {...props} {...{ cart, removeItem, updateCart }} />;
}
const ShoppingCartQuantity = ({ cart, productId, updateCart }) => {
const qty = cart.find(item => item.id === productId)?.qty ?? 0;
return (
<div>
<span>
<span
className={`${styles.op} ${styles.decrementBtn}`}
onClick={() => updateCart(productId, -1)}
>
-
</span>
<span className={styles.qty}>{qty}</span>
<span
className={`${styles.op} ${styles.incrementBtn}`}
onClick={() => updateCart(productId, 1)}
>
+
</span>
</span>
</div>
)
}
export default ShoppingCartQuantity;
In places in your app where ShoppingCartQuantity component is used within the CartContextProvider decorate it with the withShoppingCartContext HOC and use normally.
ShoppingCart
import ShoppingCartQuantityBase, {
withShoppingCartContext
} from "../../components/layout/shoppingCart/ShoppingCartQuantity";
const ShoppingCartQuantity = withShoppingCartContext(ShoppingCartQuantityBase);
const ShoppingCart = (props) => {
...
return (
...
<ShoppingCartQuantity productId={....} />
...
);
};
In places where ShoppingCartQuantity component is used outside the context, like in the sweet modal, access the context within the React code and pass in the context values and callbacks.
...
import ShoppingCartQuantity from "../../components/layout/shoppingCart/ShoppingCartQuantity";
...
function Homepage() {
...
const { cart, updateCart } = useContext(CartContext);
const productModal = withReactContent(Swal);
...
function showProductPopupHandler(productId) {
ProductService.get(productId)
.then((response) => {
const product = response.data;
return productModal.fire({
html:
<div>
...
<div>
<FavoriteBtn productId={product.AMP_GUID}/>
<ShoppingCartQuantity
productId={product.AMP_GUID}
{...{ cart, updateCart }}
/>
</div>
...
</div>,
showConfirmButton: false,
showCloseButton: true
});
});
}
return (...);
}
export default Homepage;
Additional Issues
Your context provider is mutating state when updating quantities. When updating nested state you should still create a shallow copy of the array elements that are being updated.
Example:
const CartContextProvider = (props) => {
...
const updateCart = (productId, amount) => {
// only update if item in cart
if (cart.some(item => item.id === productId)) {
// use functional state update to update from previous state
// cart.map creates shallow copy of previous state
setCart(cart => cart.map(item => item.id === productId
? {
...item, // copy item being updated into new object reference
qty: Math.max(item.qty + amount, 1), // minimum quantity is 1
}
: item
));
}
}
const removeItem = (id) => {
setCart(cart => cart.filter(item => item.id !== id));
};
return (
<CartContext.Provider value={{ cart, updateCart, removeItem }}>
{props.children}
</CartContext.Provider>
);
};
You did't show where you are using the ShoppingCart component or the ShoppingCartQuantity component.
Anyway, when you declare a route, you must pass the component, not the root element. So, this line:
<Route exact path="/" element={<Homepage/>}/>
must be
<Route exact path="/" component={Homepage}/>

How can I create an instance of an Object in React..?

I am very new to react and javascript, but I am trying to build a simple ToDo App. It wasn't complicated until I wanted to read data from a file and to display that data on the screen. The problem is that I don't know how to create a new Todo object to pass it as parameter for addTodo function.. Thaaank you all and hope you can help me!!
I will let the code here (please see the -loadFromFile- function, there is the problematic place:
import React, { useState } from 'react';
import TodoForm from './TodoForm';
import Todo from './Todo';
import data from './data/data.json'
function TodoList() {
const [todos, setTodos] = useState([]);
const loadFromFile = data.map( ( data) => {
const newTodo = addTodo(new Todo(data.id,data.text));
return ( {newTodo} )});
const addTodo = todo => {
if (!todo.text || /^\s*$/.test(todo.text)) {
return;
}
const newTodos = [todo, ...todos];
setTodos(newTodos);
console.log(...todos);
};
const updateTodo = (todoId, newValue) => {
if (!newValue.text || /^\s*$/.test(newValue.text)) {
return;
}
setTodos(prev => prev.map(item => (item.id === todoId ? newValue : item)));
};
const removeTodo = id => {
const removedArr = [...todos].filter(todo => todo.id !== id);
setTodos(removedArr);
};
const completeTodo = id => {
let updatedTodos = todos.map(todo => {
if (todo.id === id) {
todo.isComplete = !todo.isComplete;
}
return todo;
});
setTodos(updatedTodos);
};
return (
<>
<TodoForm onSubmit={addTodo} />
{loadFromFile}
<Todo
todos={todos}
completeTodo={completeTodo}
removeTodo={removeTodo}
updateTodo={updateTodo}
/>
</>
);
}
export default TodoList;
I want to create new instance of Todo object. I tried many times, many different forms, but still doesn't work. I have an id and a text from the data.json file. I want to create that instance of Todo object with these two values. But how?
import React, { useState } from 'react';
import TodoForm from './TodoForm';
import EditIcon from '#material-ui/icons/Edit';
import DeleteIcon from '#material-ui/icons/Delete';
const Todo = ({ todos, completeTodo, removeTodo, updateTodo }) => {
const [edit, setEdit] = useState({
id: null,
value: ''
});
const submitUpdate = value => {
updateTodo(edit.id, value);
setEdit({
id: null,
value: ''
});
};
if (edit.id) {
return <TodoForm edit={edit} onSubmit={submitUpdate} />;
}
return todos.map((todo, index) => (
<div
className={todo.isComplete ? 'todo-row complete' : 'todo-row'}
key={index}
>
<p> <div key={todo.id} onClick={() => completeTodo(todo.id)}>
{todo.text}
</div>
</p>
<div className='icons'>
<DeleteIcon fontSize="small"
onClick={() => removeTodo(todo.id)}
className='delete-icon'
/>
<EditIcon
onClick={() => setEdit({ id: todo.id, value: todo.text })}
className='edit-icon'
/>
</div>
</div>
));
};
export default Todo;
import React, { useState, useEffect, useRef } from 'react';
import { Fab, IconButton } from "#material-ui/core";
import AddIcon from '#material-ui/icons/Add';
function TodoForm(props) {
const [input, setInput] = useState(props.edit ? props.edit.value : '');
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
});
const handleChange = e => {
setInput(e.target.value);
};
const handleSubmit = e => {
e.preventDefault();
props.onSubmit({
id: Math.floor(Math.random() * 10000),
text: input
});
setInput('');
};
return (
<form onSubmit={handleSubmit} className='todo-form'>
{props.edit ? (
<>
<textarea cols="10"
placeholder='Update item'
value={input}
onChange={handleChange}
name='text'
ref={inputRef}
className='todo-input edit'
/>
<button onClick={handleSubmit} className='todo-button edit'>
Save
</button>
</>
) : (
<>
<input
placeholder='Add item'
value={input}
onChange={handleChange}
name='text'
className='todo-input'
ref={inputRef}
/>
<Fab color="primary" aria-label="add">
< AddIcon onClick={handleSubmit} fontSize="small" />
</Fab>
</>
)}
</form>
);
}
export default TodoForm;
Issue
Ah, I see what you are getting at now, you are wanting to load some list of todos from an external file. The main issue I see in your code is that you are attempting to call/construct a Todo React component manually and this simply isn't how React works. You render data/state/props into JSX and pass this to React and React handles instantiating the components and computing the rendered DOM.
const loadFromFile = data.map((data) => {
const newTodo = addTodo(new Todo(data.id, data.text));
return ({newTodo});
});
Todo shouldn't be invoked directly, React handles this.
Solution
Since it appears the data is already an array of objects with the id and text properties, it conveniently matches what you store in state. You can simply pass data as the initial todos state value.
const [todos, setTodos] = useState(data);
If the data wasn't readily consumable you could create an initialization function to take the data and transform/map it to the object shape your code needs.
const initializeState = () => data.map(item => ({
id: item.itemId,
text: item.dataPayload,
}));
const [todos, setTodos]= useState(initializeState);
Running Example:
import data from "./data.json";
function TodoList() {
const [todos, setTodos] = useState(data); // <-- initial state
const addTodo = (text) => {
if (!text || /^\s*$/.test(text)) {
return;
}
setTodos((todos) => [todo, ...todos]);
};
const updateTodo = (id, newTodo) => {
if (!newTodo.text || /^\s*$/.test(newTodo.text)) {
return;
}
setTodos((todos) => todos.map((todo) => (todo.id === id ? newTodo : todo)));
};
const removeTodo = (id) => {
setTodos((todos) => todos.filter((todo) => todo.id !== id));
};
const completeTodo = (id) => {
setTodos((todos) =>
todos.map((todo) =>
todo.id === id
? {
...todo,
isComplete: !todo.isComplete
}
: todo
)
);
};
return (
<>
<TodoForm onSubmit={addTodo} />
<Todo
todos={todos}
completeTodo={completeTodo}
removeTodo={removeTodo}
updateTodo={updateTodo}
/>
</>
);
}

TypeError: Cannot assign to read only property '0' of object '[object Array]'

Im really rellied about this issue, the case im trying to use a Material UI slider with redux.
here the slider component:
import { Slider } from '#material-ui/core'
const RangeSlider = ({handleRange, range}) => {
const handleChange = (event, newValues) => {
handleRange(newValues)
}
return (
<Slider
value={range}
onChange={handleChange}
valueLabelDisplay="auto"
aria-labelledby="range-slider"
min={0}
max={100}
/>
)
}
export default RangeSlider
here the filter component with redux logic:
import {
Button,
Typography as Tp,
Select,
MenuItem,
Accordion,
AccordionDetails,
AccordionSummary,
} from "#material-ui/core";
import { ExpandMore } from "#material-ui/icons";
import { RangeSlider } from "../components";
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { filterUpdate } from "../redux/actions/filter";
const Filter = ({ models }) => {
const [range, setRange] = useState([0,60]);
const [modelSelected, setModelSelected] = useState("None");
const handleRange = (newValue) => setRange(newValue);
const handleModel = (event) => setModelSelected(event.target.value);
const dispatch = useDispatch();
const handleSubmit = () => {
let filterValues = {
range,
modelSelected,
};
dispatch(filterUpdate(filterValues));
};
return (
<div className="container mt-3 ">
<Accordion>
<AccordionSummary
expandIcon={<ExpandMore />}
aria-controls="panel-filter-content"
id="panel-filter-header"
>
<Tp>Sort your search</Tp>
</AccordionSummary>
<AccordionDetails className='d-flex align-items-center bg-light pb-3'>
<div className='col-md-6'>
<Tp className="mt-3">Pick up a price range</Tp>
<RangeSlider handleRange={handleRange} range={range} />
</div>
<div className="col">
<div className="col text-center pb-3">
<Tp className="mt-3">Sort by model</Tp>
<Select value={modelSelected} onChange={handleModel}>
{models.map((model) => {
return (
<MenuItem key={model} value={model}>
{model}
</MenuItem>
);
})}
</Select>
</div>
</div>
<div className=" col mt-5 text-center">
<Button onClick={handleSubmit} variant="outlined" color="primary">
Submit
</Button>
</div>
</AccordionDetails>
</Accordion>
</div>
);
};
export default Filter;
Redux action logic:
import { createAction } from '#reduxjs/toolkit'
import types from '../types'
export const filterUpdate = createAction(types.FILTER_UPDATE)
Here Reducer:
import { createReducer } from '#reduxjs/toolkit'
import { filterUpdate } from '../actions/filter'
const reducer = createReducer({
range: [0,60],
model: 'None'
}, {
[filterUpdate]: (state, action) => {
state.range = action.payload.range
state.model = action.payload.modelSelected
}
})
export default reducer
The error appear when i include the redux logic, when i tested with a console.log was fine, but with the dispatch, appear this error, also, its a random error because i can test to dispatch 2-3 times without error, but the next this error appear
What can i do to deal with that?
Thanks in advance.
In my case, it fails to sort function inside the Slider.
That because the value that I passed into the component was a read-only (immutable).
So to avoid this error I should to pass in the value prop the copy of range array. Like this:
<Slider
value={[...range]}
// ....other props
/>
I am not sure what is the problem, but your reducer is definitely wrong. You do:
const reducer = createReducer({
range: [0,60],
model: 'None'
}, {
[filterUpdate]: (state, action) => {
state.range = action.payload.range
state.model = action.payload.modelSelected
}
})
You probably meant to do this:
const reducer = createReducer({
range: [0,60],
model: 'None'
}, {
[filterUpdate]: (state, action) => ({
range: action.payload.range,
model: action.payload.modelSelected,
})
})
Also you are not actually pulling state from redux store. You are importing useSelector and not calling it anywhere. Whatever useState and useDispatch return are independent from each other in your code.

React view only gets fresh data on page reload?

I'm building a simple campgrounds CRUD app to get some practice with the MERN stack and Redux.
Adding a campground is working fine. I'm routing to the campgrounds list page after adding a campground. But unless I reload the page, fresh data isn't retrieved.
I figured it has something to do with React's lifecycle methods.
Code:
manageCampground.js
import React, { Component } from 'react';
import TextField from '#material-ui/core/TextField';
import Card from '#material-ui/core/Card';
import Button from '#material-ui/core/Button';
import '../../styles/addCampground.css';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import {
actionAddCampground,
getCampgroundDetails,
actionUpdateCampground
} from './actions/campgroundActions';
class AddCampground extends Component {
constructor(props) {
super(props);
this.state = {
name: '',
description: '',
cost: ''
};
}
componentDidMount() {
const campground = this.props.campground;
if (campground._id) {
this.props.getCampgroundDetails(campground._id);
this.setState({
name: campground.name,
description: campground.description,
cost: campground.cost
});
}
}
handleChange = e => {
const { name, value } = e.target;
this.setState({
[name]: value
});
};
addCampground = () => {
const name = this.state.name;
const description = this.state.description;
const cost = this.state.cost;
this.props.actionAddCampground({
name,
description,
cost
});
this.props.history.push('/home');
console.log('Campground added successfully');
};
updateCampground = () => {
const name = this.state.name;
const description = this.state.description;
const cost = this.state.cost;
this.props.actionUpdateCampground({
name,
description,
cost
});
this.props.history.push('/home');
console.log('Updated successfully');
};
render() {
console.log(this.props);
return (
<Card className="add-campground-card">
<TextField
name="name"
className="textfield"
label="Campground name"
variant="outlined"
value={this.state.name}
onChange={e => this.handleChange(e)}
/>
<TextField
name="description"
className="textfield"
label="Campground description"
variant="outlined"
value={this.state.description}
onChange={e => this.handleChange(e)}
/>
<TextField
name="cost"
className="textfield"
type="number"
label="Campground cost"
variant="outlined"
value={this.state.cost}
onChange={e => this.handleChange(e)}
/>
{!this.props.campground._id ? (
<Button
variant="contained"
color="primary"
onClick={this.addCampground}>
Add Campground
</Button>
) : (
<Button
variant="contained"
color="primary"
className="update-campground-btn"
onClick={this.updateCampground}>
Update Campground
</Button>
)}
</Card>
);
}
}
const mapStateToProps = state => {
return {
campground: state.campgroundList.singleCampground || ''
};
};
const mapDispatchToProps = dispatch => {
return bindActionCreators(
{
actionAddCampground,
getCampgroundDetails,
actionUpdateCampground
},
dispatch
);
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(AddCampground);
campgroundList.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getAllCampgrounds } from './actions/campgroundActions';
import Header from '../common/Header';
import Card from '#material-ui/core/Card';
import CardActionArea from '#material-ui/core/CardActionArea';
import CardActions from '#material-ui/core/CardActions';
import CardContent from '#material-ui/core/CardContent';
import Button from '#material-ui/core/Button';
import Typography from '#material-ui/core/Typography';
import { Link } from 'react-router-dom';
import { bindActionCreators } from 'redux';
import '../../styles/landingPage.css';
class CampgroundLanding extends Component {
componentDidMount() {
this.props.getAllCampgrounds();
console.log('From component did mount');
}
render() {
const { campgrounds } = this.props;
return (
<>
<Header />
{campgrounds.map(campground => (
<Card className="campground-card" key={campground._id}>
<CardActionArea>
<CardContent>
<Typography gutterBottom variant="h5"
component="h2">
{campground.name}
</Typography>
<Typography variant="body2" color="textSecondary"
component="p">
{campground.description}
</Typography>
</CardContent>
</CardActionArea>
<CardActions>
<Link
style={{ textDecoration: 'none', color: 'white' }}
to={`/campgrounds/${campground._id}`}>
<Button size="small" color="primary">
View Details
</Button>
</Link>
<Button size="small" color="primary">
Learn More
</Button>
</CardActions>
</Card>
))}
<Link
style={{ textDecoration: 'none', color: 'white' }}
to="/campgrounds/add">
<Button color="primary">Add Campground</Button>
</Link>
</>
);
}
}
const mapStateToProps = state => {
return {
campgrounds: state.campgroundList.campgrounds
};
};
const mapDispatchToProps = dispatch => {
return bindActionCreators(
{
getAllCampgrounds
},
dispatch
);
};
export default connect(
mapStateToProps,
null
)(CampgroundLanding);
campgroundActions.js
import {
GET_ALL_CAMPGROUNDS,
ADD_CAMPGROUND,
GET_CAMPGROUND_DETAILS,
EDIT_CAMPGROUND
} from '../actionTypes/types';
import axios from 'axios';
const API_URL = `http://localhost:5000/api`;
export const getAllCampgrounds = () => {
return dispatch => {
axios
.get(`${API_URL}/campgrounds`)
.then(res => {
dispatch({
type: GET_ALL_CAMPGROUNDS,
payload: res
});
})
.catch(err => console.log(err));
};
};
export const actionAddCampground = campground => {
return dispatch => {
axios
.post(`${API_URL}/campgrounds`, campground)
.then(res => {
console.log(res);
dispatch({
type: ADD_CAMPGROUND,
payload: res
});
})
.catch(err => console.log(err));
};
};
export const getCampgroundDetails = id => {
return dispatch => {
axios
.get(`${API_URL}/campgrounds/${id}`)
.then(res => {
dispatch({
type: GET_CAMPGROUND_DETAILS,
payload: res
});
})
.catch(err => console.log(err));
};
};
export const actionUpdateCampground = id => {
return dispatch => {
axios
.put(`${API_URL}/campgrounds/${id}`)
.then(res => {
console.log(res);
dispatch({
type: EDIT_CAMPGROUND,
payload: res
});
})
.catch(err => console.log(err));
};
};
campgroundReducers.js
import {
GET_ALL_CAMPGROUNDS,
ADD_CAMPGROUND,
GET_CAMPGROUND_DETAILS,
EDIT_CAMPGROUND
} from '../actionTypes/types';
const initialState = {
campgrounds: []
};
export default (state = initialState, action) => {
switch (action.type) {
case GET_ALL_CAMPGROUNDS:
const { campgroundList } = action.payload.data;
state.campgrounds = campgroundList;
return {
...state
};
case ADD_CAMPGROUND:
const { campground } = action.payload.data;
return {
...state,
campground
};
case GET_CAMPGROUND_DETAILS:
const { singleCampground } = action.payload.data;
return { ...state, singleCampground };
case EDIT_CAMPGROUND:
const { editedCampground } = action.payload.data;
return { ...state, editedCampground };
default:
return state;
}
};
If I'm using componentDidUpdate, it's leading to infinite loop.
componentDidUpdate(prevProps) {
if (prevProps.campgrounds !== this.props.campgrounds) {
this.props.getAllCampgrounds();
}
}
I know I'm going wrong somewhere, but I can't figure out where.
You have to fix your componentDidUpdate method to avoid this infinity loop. Your are trying to compare objects via === method which will always fail according to this example:
const a = {key: 1}
const b = {key: 1}
console.log(a === b); // false
You could use for example isEqual method from lodash module to compare objects like you want https://lodash.com/docs/4.17.15#isEqual
console.log(_.isEqual({key: 1}, {key: 1})) // true
console.log({key: 1} === {key: 1}) // false
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.js"></script>
Actually why do you want refresh data in this case? When you add object with success into the store this will refresh your component so you don't have to request for fresh data.

Categories

Resources