I wonder if there is not a better way to manage the open and close of Dialogs in a functional component? You can find an example below:
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import EditDialog from './EditDialog';
import DeleteDialog from './DeleteDialog';
const ContactCard = ({ contact }) => {
const [editOpen, setEditOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const handleEditOpen = () => {
setEditOpen(true);
};
const handleEditClose = () => {
setEditOpen(false);
};
const handleDeleteOpen = () => {
setDeleteOpen(true);
};
const handleDeleteClose = () => {
setDeleteOpen(false);
};
const { type, firstName, lastName, phoneNumber, mail } = contact;
return (
<>
<div className={classes.main}>
{/* All my contact informations */}
</div>
<EditDialog handleClose={handleEditClose} open={editOpen} />
<DeleteDialog handleClose={handleDeleteClose} open={deleteOpen} />
</>
);
};
ContactCard.propTypes = {
contact: PropTypes.object.isRequired
};
export default ContactCard;
I think this is super redundant but I cannot find a nicer way to manage several different dialogs.
const handleEditOpen = () => {
setEditOpen(true);
};
const handleEditClose = () => {
setEditOpen(false);
};
const handleDeleteOpen = () => {
setDeleteOpen(true);
};
const handleDeleteClose = () => {
setDeleteOpen(false);
};
Many thanks for your time and advice!
To reduce some of the redundancy of your code, you could set the open/close in one function, by essentially toggling the current state. I did mine inline, but you could still create a handleEdit function and toggle the state there.
import React, {useState} from "react";
import ReactDOM from "react-dom";
function App() {
const [editCard, setEditCard] = useState(false)
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<button onClick={() => setEditCard(!editCard)}>Toggle Edit</button>
{editCard && <div>Card is open for editing</div>}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Here is another example with your code. I didn't run it, but it should look something like this.
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import EditDialog from './EditDialog';
import DeleteDialog from './DeleteDialog';
const ContactCard = ({ contact }) => {
const [editOpen, setEditOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const handleEdit = () => {
setEditOpen(!editOpen);
};
const handleDelete = () => {
setDeleteOpen(!deleteOpen);
};
const { type, firstName, lastName, phoneNumber, mail } = contact;
return (
<>
<div className={classes.main}>
{/* All my contact informations */}
</div>
{
editOpen && <EditDialog handleEdit={handleEdit} />
}
{
deleteOpen && <DeleteDialog handleClose={handleClose} />
}
</>
);
};
ContactCard.propTypes = {
contact: PropTypes.object.isRequired
};
export default ContactCard;
The responsibility of open the dialog should be of the main component. This way the modal is only rendered if the state property is true.
Another tip is use <React.Fragment> insted <>
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import EditDialog from './EditDialog';
import DeleteDialog from './DeleteDialog';
const ContactCard = ({ contact }) => {
const [editOpen, setEditOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const handleEditOpen = () => {
setEditOpen(!editOpen);
};
const handleDeleteOpen = () => {
setDeleteOpen(!deleteOpen);
};
const { type, firstName, lastName, phoneNumber, mail } = contact;
return (
<React.Fragment>
<div className={classes.main}>
{/* All my contact informations */}
</div>
{
editOpen && <EditDialog handleClose={handleEditOpen} />
}
{
deleteOpen && <DeleteDialog handleClose={handleDeleteOpen} />
}
</React.Fragment>
);
};
ContactCard.propTypes = {
contact: PropTypes.object.isRequired
};
export default ContactCard;
To incapsulate logic of changing dialog opening state, I'd recommend to create separate hook:
const useToggle = (defaultValue) => {
return useReducer((value) => !value, !!defaultValue)
}
This hook is basically useState but setState function isn't waiting for argument to update state, it updates state with the inverse of current state.
This might be useful while working with dialogs:
const ContactCard = () => {
const [editOpen, toggleEditOpen] = useToggle(false);
const [deleteOpen, toggleDeleteOpen] = usetoggle(false);
return (
<>
<div className={classes.main}>
{/* All my contact informations */}
</div>
{editOpen && <EditDialog handleEdit={toggleEditOpen} />}
{deleteOpen && <DeleteDialog handleClose={toggleDeleteOpen} />}
</>
);
};
Related
I have a context api in my application. At the moment I only keep the categories and i have 1 initial category. I print the categories in App.js with the map function. I have defined a function called addCategoryHandler in context api and I want to update my state by calling it in AddCategory component. But when I click the button state.categories returns undefined. I guess I'm missing something about lifecyle but I couldn't quite understand. Can you help?
Here is the codesandbox link: https://codesandbox.io/s/hungry-zeh-kwolr8
App.js
import AvailableProducts from './components/AvailableProducts.js';
import Category from './components/Category.js';
import Review from './components/Review.js';
import AddCategory from './components/AddCategory';
import { useAppContext } from './context/appContext';
import './assets/styles/App.scss';
export default function App() {
const { categories } = useAppContext();
return (
<main>
<h1>Initial Screen</h1>
<div className='container'>
<div className='container__left-side'>
<AvailableProducts />
<Review />
</div>
<div className='container__right-side'>
{categories.map((category) => (
<Category
key={category.id}
id={category.id}
title={category.title}
/>
))}
<AddCategory />
</div>
</div>
</main>
);
}
Context Api
import React, { useContext, useState } from "react";
import generateCategoryTitle from "../utils/GenerateCategoryTitle";
const AppContext = React.createContext();
const initialState = {
categories: [{ id: 1, title: "Category 1", products: [] }]
};
const AppProvider = ({ children }) => {
const [state, setState] = useState(initialState);
console.log(state);
const addCategoryHandler = () => {
// const { newId, newCategoryTitle } = generateCategoryTitle(state.categories);
// // const newCategory = [{ id: newId, title: newCategoryTitle, products: [] }];
// setState((prevState) => {
// console.log([...prevState.categories,...newCategory]);
// });
console.log("add category clicked");
};
return (
<AppContext.Provider value={{ ...state, addCategoryHandler }}>
{children}
</AppContext.Provider>
);
};
const useAppContext = () => useContext(AppContext);
export { AppProvider, useAppContext };
Add Category Component
import "../assets/styles/AddCategory.scss";
import { useAppContext } from "../context/appContext";
const AddCategory = () => {
const { addCategoryHandler } = useAppContext();
return (
<button
className="add-categoryn-btn"
type="button"
onClick={addCategoryHandler}
>
Add Category
</button>
);
};
export default AddCategory;
So I have a context.
import React from "react";
const CartContext = React.createContext({
numberOfMeals: 0,
meals: [],
});
export default CartContext;
I then import this into my App.JS
import React from "react";
import MealsBase from "./Components/Meals/MealsBase";
import CartContext from "./Context/CartContext";
function App() {
return (
<CartContext.Provider value={{ numberOfMeals: 0, meals: [] }}>
<MealsBase/>
</CartContext.Provider>
);
}
export default App;
I use this context within two components.
Component 1 :
import React, {useContext} from "react";
import "./CartIcon.css";
import CartContext from "../../Context/CartContext";
const CartIcon = () => {
const cartContext = useContext(CartContext);
return (
<React.Fragment>
<button className="border">
<h3>Your Cart : {cartContext.numberOfMeals}</h3>
</button>
</React.Fragment>
);
};
export default CartIcon;
Component 2 :
import React,{ useState, useContext } from "react";
import CartContext from "../../Context/CartContext";
const MealForm = () => {
const [amount, setAmount] = useState(0);
const mealContext = useContext(CartContext);
const amountHandler = (event) => {
setAmount(event.target.valueAsNumber);
};
const formHandler = (event) => {
mealContext.numberOfMeals = mealContext.numberOfMeals + amount;
console.log(mealContext.numberOfMeals);
event.preventDefault();
}
return(
<React.Fragment>
<form onSubmit={formHandler}>
<label>Amount</label>
<input type="number" step={1} min={-1} max={50} value={amount} onChange={amountHandler}/>
<button type="submit"> + Add </button>
</form>
</React.Fragment>
);
};
export default MealForm
The context seems to update in component two but not in component one.
I have an understanding that if useContexts values change then it causes a component to re-render. So I am struggling to understand why the CartIcon.js file is not being re-rendered.
You aren't really updating the value the React way, you are mutating it. Move the context value into state and provide that out.
const CartContext = React.createContext({
numberOfMeals: 0,
setNumberOfMeals: () => {},
meals: [],
setMeals: () => {},
});
...
function App() {
const [numberOfMeals, setNumberOfMeals] = useState(0);
const [meals, setMeals] = useState([]);
return (
<CartContext.Provider value={{ numberOfMeals, setNumberOfMeals, meals, setMeals }}>
<MealsBase/>
</CartContext.Provider>
);
}
...
const MealForm = () => {
const [amount, setAmount] = useState(0);
const { setNumberOfMeals } = useContext(CartContext);
const amountHandler = (event) => {
setAmount(event.target.valueAsNumber);
};
const formHandler = (event) => {
event.preventDefault();
setNumberOfMeals(count => count + amount);
}
return(
<React.Fragment>
<form onSubmit={formHandler}>
<label>Amount</label>
<input type="number" step={1} min={-1} max={50} value={amount} onChange={amountHandler}/>
<button type="submit"> + Add </button>
</form>
</React.Fragment>
);
};
I'm currently struggling with React Context. I'd like to pass functions allowing the show / hide cart logic in the context, instead of using props between components.
I dont understand why when clicking on the button in HeaderCartButton component, it doesn't trigger the **onClick={ctx.onShowCart}** that is in my context, even though when I console log the cartCtx.state it is properly updated, which should then add the component in the App.js
//App.js
import { useContext } from "react";
import Header from "./components/Layout/Header";
import Meals from "./components/Meals/Meals";
import Cart from "./components/Cart/Cart";
import CartProvider from "./store/CartProvider";
import CartContext from "./store/cart-context";
function App() {
const ctx = useContext(CartContext);
return (
<CartProvider>
{ctx.state && <Cart />}
<Header />
<main>
<Meals />
</main>
</CartProvider>
);
}
export default App;
//cart-context.js
import React from "react";
const CartContext = React.createContext({
state: false,
onShowCart: () => {},
onHideCart: () => {},
items: [],
totalAmount: 0,
addItem: (item) => {},
removeItem: (id) => {},
});
export default CartContext;
//CartProvider.js
import CartContext from "./cart-context";
import { useState } from "react";
const CartProvider = (props) => {
const [cartIsShown, setCartIsShown] = useState(false);
const showCartHandler = () => {
setCartIsShown(true);
};
const hideCartHandler = () => {
setCartIsShown(false);
};
const handleAddItem = (item) => {};
const handleRemoveItem = (id) => {};
const cartCtx = {
state: cartIsShown,
onShowCart: showCartHandler,
onHideCart: hideCartHandler,
items: [],
totalAmount: 0,
addItem: handleAddItem,
removeItem: handleRemoveItem,
};
return (
<CartContext.Provider value={cartCtx}>
{props.children}
</CartContext.Provider>
);
};
export default CartProvider;
//Header.js
import { Fragment } from "react";
import HeaderCartButton from "./HeaderCartButton";
import mealsImage from "../../assets/meals.jpg";
import classes from "./Header.module.css";
const Header = (props) => {
return (
<Fragment>
<header className={classes.header}>
<h1>ReactMeals</h1>
<HeaderCartButton />
</header>
<div className={classes["main-image"]}>
<img src={mealsImage} alt="A table full of delicious food!" />
</div>
</Fragment>
);
};
export default Header;
//HeaderCartButton.js
import CartIcon from "../Cart/CartIcon";
import { useContext } from "react";
import classes from "./HeaderCartButton.module.css";
import CartContext from "../../store/cart-context";
const HeaderCartButton = (props) => {
const ctx = useContext(CartContext);
const numberOfCartItems = ctx.items.reduce((accumulator, item) => {
return accumulator + item.amount;
}, 0);
return (
<button className={classes.button} onClick={ctx.onShowCart}>
<span className={classes.icon}>
<CartIcon />
</span>
<span>Your Cart</span>
<span className={classes.badge}>{numberOfCartItems}</span>
</button>
);
};
export default HeaderCartButton;
Thanks for your help
If you look at your App component, you are using CartContext outside the provider.
function App() {
const ctx = useContext(CartContext);
return (
<CartProvider>
{ctx.state && <Cart />}
<Header />
<main>
<Meals />
</main>
</CartProvider>
);
}
You should modify it so that it is similar to the following, where you are using the context inside the provider.
const Main = () => {
return <CartProvider><App /></CartProvider>
}
function App() {
const ctx = useContext(CartContext);
return (
<>
{ctx.state && <Cart />}
<Header />
<main>
<Meals />
</main>
</>
);
}
I have filtered the products and on submitting the search term, am showing the results in a new page using history.push() property.
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { IoIosSearch } from 'react-icons/io';
import { useHistory } from "react-router-dom";
import './style.css';
/**
* #author
* #function Search
*/
const Search = (props) => {
const product = useSelector(state => state.product);
let { products , filteredProducts } = product;
const [searchTerm, setSearchTerm] = useState('');
const onChangeSearch = (e) => {
setSearchTerm(e.currentTarget.value);
}
const isEmpty = searchTerm.match(/^\s*$/);
if(!isEmpty) {
filteredProducts = products.filter( function(prod) {
return prod.name.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase().trim())
})
}
const history = useHistory();
const display = !isEmpty
const handleSubmit =(e) => {
e.preventDefault();
if( !isEmpty ) {
history.push(`/search/search_term=${searchTerm}/`, { filteredProducts })
}
setSearchTerm('');
}
return (
<div>
<form onSubmit={handleSubmit}>
<div className="searchInputContainer">
<input
className="searchInput"
placeholder={'What are you looking for...'}
value={searchTerm}
onChange={onChangeSearch}
/>
<div className="searchIconContainer">
<IoIosSearch
style={{
color: 'black',
fontSize: '22px'
}}
onClick={handleSubmit}
/>
</div>
</div>
</form>
{
display && <div className="searchResultsCont">
{filteredProducts.map((prod, index) => (<div key={index}>{prod.name}</div>))}
</div>
}
</div>
);
}
export default Search
On the new page this is the code :
import React from 'react';
import Layout from '../../components/Layout';
const SearchScreen = ({location}) => {
const products = location.state.filteredProducts;
const show = products.length > 0
return (
<Layout>
<div>
{
show ? products.map((prod, index) => (<div key={index}>{prod.name}</div>)) : <div>No items found</div>
}
</div>
</Layout>
)
}
export default SearchScreen
The problem comes when I copy and paste the URL to another new page, or like when I email others the URL the error becomes " Cannot read property 'filteredProducts' of undefined ". Using this method I understand that the results (filtered products) have not been pushed through the function history.push() that's why it is undefined, how can I make this possible?
I changed the whole aspect to filtering the products from the back-end..
It worked
Hello I got stuck during creating app using hooks
I do not why but my Component does not download a state from my Context Component or maybe my initial state does not update correctly. Does somebody have any idea what's going on?
Context Component:
import React, { createContext, useState } from 'react';
export const WeatherDataContext = createContext();
const WeatherDataContextProvider = (props) => {
const [weather, setWeather] = useState(
{
city: null,
temp: null
}
)
const addWeather = (city, temp) => {
setWeather({
city,
temp
})
}
return (
<WeatherDataContext.Provider value={{weather, addWeather}}>
{props.children}
</WeatherDataContext.Provider>
)
}
export default WeatherDataContextProvider
Form - axios - Component:
import React, {useContext, useState} from 'react';
import { WeatherDataContext } from '../context/WeatherDataContext';
import axios from 'axios'
import {Link} from 'react-router-dom'
const WeatherForm = () => {
const {addWeather} = useContext(WeatherDataContext);
const [value, setValue] = useState('')
const handleChange = (e) => {
e.preventDefault();
axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${value}&appid=${KEY}&units=metric`)
.then(res => {
addWeather(res.data.name, res.data.main.temp)
})
}
return (
<div class='weather-form'>
<form onSubmit={handleChange}>
<input placeholder='City' onChange={(e) => setValue(e.target.value)} value={value} required/>
<Link to='/weather'><button>Search</button></Link>
</form>
</div>
)
}
export default WeatherForm
And final component where I want to use my update state
import React, {useContext, useState} from 'react';
import { WeatherDataContext } from '../context/WeatherDataContext';
const WeatherFront = () => {
const {weather} = useContext(WeatherDataContext)
console.log(weather)
return (
<div class='weather-front'>
<h1>City: {weather.city}, Temperatura: {weather.temp}</h1>
</div>
)
}
export default WeatherFront
Your button is not submitting the form - it navigates away from the page instead.
So handleChange is not being called.
You can call it from buttons onClick instead of forms onSubmit. Be sure to omit e.preventDefault() then, so that parent Link can still navigate.
const WeatherForm = () => {
const { addWeather } = useContext(WeatherDataContext)
const [value, setValue] = useState('')
const handleChange = (e) => {
axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${value}&appid=${KEY}&units=metric`)
.then(res => {
addWeather(res.data.name, res.data.main.temp)
})
}
return (
<div class="weather-form">
<form >
<input
placeholder="City"
onChange={(e) => setValue(e.target.value)}
value={value}
required
/>
<Link to="/weather">
<button onClick={handleChange}>Search</button>
</Link>
</form>
</div>
)
}
Be sure to wrap both pages inside the same context:
<WeatherDataContextProvider>
<Router>
<Switch>
<Route path="/weather">
<WeatherFront></WeatherFront>
</Route>
<Route path="/">
<WeatherForm></WeatherForm>
</Route>
</Switch>
</Router>
</WeatherDataContextProvider>