I have 3 files that make up a component. I am trying to calculate a bmi result which is split into several files. When I click calculate, the page changes. I have tried to find that I am new to React in general, and these are things that are probably easy but not yet obvious to me
import React, { useState } from "react";
import style from './BMI.module.css';
import Calculator from './Calculator'
import Result from './Result'
function BMI() {
const [page, setPage] = useState(0);
const checkPage = () => {
if (page === 0) {
return (
<button className={`${style.button}`}
disabled={page === 1}
onClick={() => {
setPage((currPage) => currPage + 1);
}}>Calculate</button>
)
} else {
return (
<button className={`${style.button}`} disabled={page === 0} onClick={() => {
setPage((currPage) => currPage - 1);
}}>Back</button>
)
}
}
const PageDisplay = () => {
if (page === 0) {
return <Calculator />;
} else {
return <Result />;
}
};
return (
<div className={`${style.wrapper}`}>
<div className={`${style.box}`}>
<div className={`${style.img}`}></div>
<div className={`${style.topSection}`}>
<h1 className={`${style.title}`}>{(page === 0 ? 'BMI Calculator' : 'Result')}</h1>
</div>
<div className="body">{PageDisplay()}</div>
<div>{checkPage()}</div>
</div>
</div>
)
}
export default BMI
import React, { useState } from "react";
import style from './Calculator.module.css'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faMars, faVenus } from '#fortawesome/free-solid-svg-icons';
const Input = ({ label, id, handleChange, name, type, placeholder }) => (
<>
<label className={`${style.label}`} htmlFor={id}>{label}</label>
<input className={`${style.input}`} type={type || "number"} id={id} name={name || id} placeholder={placeholder} onChange={(e) => handleChange(e.target.value)}></input>
<br />
</>
);
function Calculator() {
const [height, setHeight] = useState(0)
const [weight, setWeight] = useState(0)
const [age, setAge] = useState(0)
function removeSelected() {
const selectedBox = document.querySelectorAll('#head')
console.log(selectedBox.classList)
selectedBox.forEach(item => {
item.classList.remove(`${style.activeBox}`)
})
}
const handleToggle = (e) => {
if (!e.currentTarget.className.includes("activeBox")) {
removeSelected()
e.target.classList.add(`${style.activeBox}`)
} else {
e.target.classList.remove(`${style.activeBox}`)
}
};
return (
<>
<div className={`${style.content}`}>
<div className={`${style.middleSection}`}>
<h3 className={`${style.formTitle}`}>Choose your gender</h3>
<div className={`${style.genders}`}>
<div id="head" onClick={handleToggle} className={`${style.genderBox}`}>
<FontAwesomeIcon icon={faMars} className={`${style.genderIcon}`} />
<h3 className={`${style.genderBoxTitle}`}>Male</h3>
</div>
<div id="head" onClick={handleToggle} className={`${style.genderBox}`}>
<FontAwesomeIcon icon={faVenus} className={`${style.genderIcon}`} />
<h3 className={`${style.genderBoxTitle}`}>Female</h3>
</div>
</div>
</div>
<div className={`${style.bottomSection}`}>
<Input handleChange={setWeight} placeholder='Weight' label='Your weight (kg)'>{weight}</Input>
<Input handleChange={setHeight} placeholder='Height' label='Your height (cm)'>{height}</Input>
<Input handleChange={setAge} placeholder='Age' label='Your age'>{age}</Input>
</div>
</div>
</>
)
}
export default Calculator
import React, { useState } from 'react'
import style from './Result.module.css'
function Result() {
const [bmiScore, setBmiScore] = useState(0)
const [bmiDesc, setBmiDesc] = useState('')
return (
<div className={`${style.content}`}>
<div className={`${style.img}`}></div>
<div className={`${style.descriptions}`}>
<p className={`${style.bmiScoreDesc}`}>Your BMI is <span className={`${style.bmiScoreNumber}`}>{bmiScore}</span>, indication your weight is in the <span className={`${style.bmiScoreDesc}`}>{bmiDesc}</span> category for adults of your height</p>
<p className={`${style.descriptionBottom}`}>Maintaining a healthy weight may reduce the risk of chronic diseases associated with overweight and obesity.</p>
</div>
</div>
)
}
export default Result
So when i click Calculate which i have in the BMI.js take inputs value from Calculator.js calculate the score and put into paragraph in Result.js
What you will want to do in this case is at the top level, in BMI, that's where your bmiScore state variable should live. You can pass it to both toe Calculator (where it will be calculated) and the Result (where it will be displayed) via props, like so:
<Calculator bmi={bmiScore} /> and <Result bmi={bmiScore} />
Then the method signatures in each will look like:
function Calculator({ bmi }) and function Result({ bmi })
And you can use that bmi variable in both.
This is all because React passes variables down, not up. The only way to share across components is if a parent component holds the variable.
Related
I'm trying to pass data to the parent component Top.js using props from a child component TagsInput.js where I can add tags but
I don't understand what is causing the error...
What I want to achieve
I want to pass "tags" to the parent component Top.js from TagsInput.js in the child component with props.
I got the error like
props.setTagsinput is not a function
TagsInput.js
import React from "react";
const TagsInput = (props) => {
//1, Define the tags variable to store the entered tags. (I want to pass the value of the tags variable to the parent component Top.js)
const [tags, setTags] = React.useState([]);
//2, Put formText in the function received from the parent component and return it.
props.setTagsinput(tags);
console.log(props)
let tag_list = []
tag_list.push(tags);
const addTags = event => {
if (event.key === "Enter" && event.target.value !== "") {
setTags([...tags, event.target.value]);
event.target.value = "";
}
};
const removeTags = index => {
setTags([...tags.filter(tag => tags.indexOf(tag) !== index)]);
};
return (
<div className="tags-input">
<div className="tags_section">
{tags.map((tag, index) => (
<div className="tag tag-flex" key={index}>
<p className="tag-p">{tag}</p>
</div>
))}
</div>
<input
type="text"
onKeyUp={event => addTags(event)}
placeholder="Press enter to add tags"
/>
</div>
);
};
export default TagsInput;
Top.js
import React, {useState, useEffect} from 'react';
import axios from 'axios';
import Student from './Student';
import TagsInput from "./TagsInput";
const Top = () => {
const [ posts, setPosts] = useState([]);
const [ allPosts, setAllPosts] = useState([]);
let tag_list = []
const [searchKeyword, setSearchKeyword] = React.useState("");
const [searchTagKeyword, setTagSearchKeyword] = React.useState("");
console.log(searchKeyword)
const[tags_from_tagsinput, setTagsinput]= useState("");
console.log(tags_from_tagsinput);
useEffect(() => {
axios.get('xxx.com')
.then(result => {
setPosts(result.data.students);
setAllPosts(result.data.students);
if (searchKeyword) {
getSearchResult()
}
})},
[searchKeyword]);
const getSearchResult = () => {
console.log(searchKeyword)
const result = allPosts.filter((output, index) => {
return output.firstName.toLowerCase().includes(searchKeyword.toLowerCase())||output.lastName.toLowerCase().includes(searchKeyword.toLowerCase());
});
console.log(result)
setPosts(result);
};
const getTagSearchResult = () => {
console.log(searchTagKeyword)
const result = allPosts.filter((output, index) => {
return output.lastName.toLowerCase().includes(searchTagKeyword.toLowerCase());
});
console.log(result)
setPosts(result);
};
return (
<div>
<TagsInput setTagsinput={setTagsinput}/>
<div>
<input className="search-box" placeholder="" value={searchKeyword} onChange={(e) => setSearchKeyword(e.target.value)}/>
</div>
<div>
<input className="search-box" placeholder="" value={searchTagKeyword} onChange={(e) => setSearchKeyword(e.target.value)}/>
</div>
<div>
{searchKeyword &&
<p>{searchKeyword} Search</p>
}
{posts ?
<>
{posts.map((data, i) =>
<Student data={data} />
)}
</>
:
<div>
<p>Not Found!</p>
</div>
}
</div>
</div>
);
}
export default Top;
Student.js
import React, {useState} from 'react';
import TagsInput from './TagsInput';
const Student = (props) => {
const [show, setShow] = useState(false)
const gradesAverage = (grades) => {
let sum = 0;
grades.forEach(function(score) {
sum += Number(score);
});
let ave = sum / grades.length
return ave;
};
return (
<div className="flex">
<div className="image">
<img src={props.data.pic} className="profile" />
</div>
<div>
<p className="name">{props.data.firstName} {props.data.lastName}</p>
<button className="button" onClick={() => setShow(!show)}>
{show? <div className="button_p">-</div>:<div className="button_p">+</div>}
</button>
<div className="info">
<p>Email: {props.data.email}</p>
<p>Company: {props.data.company}</p>
<p>Skill: {props.data.skill}</p>
<p>Average Grade: {gradesAverage(props.data.grades)}%</p>
{show &&
<>
<p>Test 1: {props.data.grades[0]}%</p>
<p>Test 2: {props.data.grades[1]}%</p>
<p>Test 3: {props.data.grades[2]}%</p>
<p>Test 4: {props.data.grades[3]}%</p>
<p>Test 5: {props.data.grades[4]}%</p>
<p>Test 6: {props.data.grades[5]}%</p>
<p>Test 7: {props.data.grades[6]}%</p>
<p>Test 8: {props.data.grades[7]}%</p>
</>
}
<TagsInput />
</div>
</div>
</div>
);
}
export default Student;
You can not directly use one component hook declaration in another component, you need to have a callback function to update that state. I modified your code to use the top page setTagsinput in student tag input
Top.js
import React, { useState, useEffect } from "react";
import axios from "axios";
import Student from "./Student";
import TagsInput from "./TagsInput";
const Top = () => {
const [posts, setPosts] = useState([]);
const [allPosts, setAllPosts] = useState([]);
let tag_list = [];
const [searchKeyword, setSearchKeyword] = React.useState("");
const [searchTagKeyword, setTagSearchKeyword] = React.useState("");
console.log(searchKeyword);
const [tags_from_tagsinput, setTagsinput] = useState("");
console.log(tags_from_tagsinput);
useEffect(() => {
axios.get("xxx.com").then((result) => {
setPosts(result.data.students);
setAllPosts(result.data.students);
if (searchKeyword) {
getSearchResult();
}
});
}, [searchKeyword]);
const getSearchResult = () => {
console.log(searchKeyword);
const result = allPosts.filter((output, index) => {
return (
output.firstName.toLowerCase().includes(searchKeyword.toLowerCase()) ||
output.lastName.toLowerCase().includes(searchKeyword.toLowerCase())
);
});
console.log(result);
setPosts(result);
};
const getTagSearchResult = () => {
console.log(searchTagKeyword);
const result = allPosts.filter((output, index) => {
return output.lastName
.toLowerCase()
.includes(searchTagKeyword.toLowerCase());
});
console.log(result);
setPosts(result);
};
const setTagsFromStudent = (tags) => {
setTagsinput(tags);
};
return (
<div>
<div>
<input
className="search-box"
placeholder=""
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
/>
</div>
<div>
<input
className="search-box"
placeholder=""
value={searchTagKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
/>
</div>
<div>
{searchKeyword && <p>{searchKeyword} Search</p>}
{posts ? (
<>
{posts.map((data, i) => (
<Student data={data} setStudentTags={setTagsFromStudent} />
))}
</>
) : (
<div>
<p>Not Found!</p>
</div>
)}
</div>
</div>
);
};
export default Top;
Student.js
import React, { useState } from "react";
import TagsInput from "./TagsInput";
const Student = (props) => {
const [show, setShow] = useState(false);
const gradesAverage = (grades) => {
let sum = 0;
grades.forEach(function (score) {
sum += Number(score);
});
let ave = sum / grades.length;
return ave;
};
return (
<div className="flex">
<div className="image">
<img src={props.data.pic} className="profile" />
</div>
<div>
<p className="name">
{props.data.firstName} {props.data.lastName}
</p>
<button className="button" onClick={() => setShow(!show)}>
{show ? (
<div className="button_p">-</div>
) : (
<div className="button_p">+</div>
)}
</button>
<div className="info">
<p>Email: {props.data.email}</p>
<p>Company: {props.data.company}</p>
<p>Skill: {props.data.skill}</p>
<p>Average Grade: {gradesAverage(props.data.grades)}%</p>
{show && (
<>
<p>Test 1: {props.data.grades[0]}%</p>
<p>Test 2: {props.data.grades[1]}%</p>
<p>Test 3: {props.data.grades[2]}%</p>
<p>Test 4: {props.data.grades[3]}%</p>
<p>Test 5: {props.data.grades[4]}%</p>
<p>Test 6: {props.data.grades[5]}%</p>
<p>Test 7: {props.data.grades[6]}%</p>
<p>Test 8: {props.data.grades[7]}%</p>
</>
)}
{/*pass settag from topTag component*/}
<TagsInput setStudentTags={props.setStudentTags} />
</div>
</div>
</div>
);
};
export default Student;
TagsInput.js
import React from "react";
const TagsInput = (props) => {
const [tags, setTags] = React.useState([]);
let tag_list = [];
tag_list.push(tags);
const addTags = (event) => {
if (event.key === "Enter" && event.target.value !== "") {
setTags([...tags, event.target.value]);
// call function pass down from toptag
props.setStudentTags(tags);
event.target.value = "";
}
};
const removeTags = (index) => {
setTags([...tags.filter((tag) => tags.indexOf(tag) !== index)]);
};
return (
<div className="tags-input">
<div className="tags_section">
{tags.map((tag, index) => (
<div className="tag tag-flex" key={index}>
<p className="tag-p">{tag}</p>
</div>
))}
</div>
<input
type="text"
onKeyUp={(event) => addTags(event)}
placeholder="Press enter to add tags"
/>
</div>
);
};
export default TagsInput;
You should consider exploring React context -https://reactjs.org/docs/context.html, its built exactly for something like this.
You are getting this error because, like you mentioned, TagsInput component is used in Student component but it doesn’t pass the state setter setTagsInput function to the TagsInput component.
Now, assuming you need tags created inside Student and displayed in Top, also assuming that both are rendered in the same parent component, you can create a state for tags in the parent component. This component will pass a state setter function to Student which passes the setter to TagsInput and the state itself to Top to use the list of tags.
Something like:
const App = () => {
const [tags,setTags] = useState([]);
return (<div>
<Top tags={tags} />
<Student setTags={setTags} />
</div>);
}
Your Student component can then pass it to TagsInput like:
const Student = (props) => {
return (<div>
{/* everything else */}
<TagsInput setTagsinput={props.setTags} />
</div>)
}
In your Top component you can create a function that updates your tags_from_tagsinput hook then pass it as props to the child component
import TagsInput from "./TagsInput";
const Top = () => {
const[tags_from_tagsinput, setTagsinput]= useState("");
console.log(tags_from_tagsinput);
const getTag = (value) => {
setTagsinput(value);
};
return (
<div>
<TagsInput getTag={getTag} />
</div>
);
}
export default Top;
Now from your TagsInput component you can call this function to update tags_from_tagsinput of Top, let's suppose that you want to updated when the user click on a button
import React from "react";
const TagsInput = (props) => {
return (
<div className="tags-input">
...
<button onClick={()=>{props.getTag(tags)}}>updated parent component</button>
</div>
);
};
export default TagsInput;
I am new to ReactJS and I would like to ask how you can reset a useState when it reaches a specific condition.
In my code I basically have a dropdown selection that returns data based on its selected value.
I can display this with no problem, and was also able to add a 'load more' button to update the limit to request from the API.
My problem now is I want to reset it back to the limit of 10 as I change my dropdown value to something else.
My dropdown code looks like this
import React, { useContext, useState } from 'react';
import { BreedProvider } from './BreedContext';
import { BreedsContext } from './BreedsContext';
import Cats from './Cats';
const BreedSelection = () => {
const [breeds] = useContext(BreedsContext);
const [cat, setCat] = useState([]);
const [limit, setLimit] = useState(10);
function handleChange(e) {
setCat(e.target.value);
setLimit(limit);
}
return (
<>
<div className="row">
<div className="col-md-3 col-sm-6 col-12">
<div className="form-group">
<label className="form-label" htmlFor="breed">Breed</label>
<select className="form-control" onChange={handleChange} disabled={breeds.length === 0}>
<option value={null}>Select Breed</option>
{breeds.map(breed => (
<option key={breed.id} value={breed.id}>{breed.name}</option>
))}
</select>
</div>
</div>
</div>
<BreedProvider breed={cat} limit={limit}>
<Cats />
</BreedProvider>
</>
);
}
export default BreedSelection;
After which, this then calls my context to display the results and allow the user to load more via a button
import React, { useState, useEffect, createContext } from 'react';
import axios from '../services/axios';
import requests from '../services/requests';
export const BreedContext = createContext();
export const BreedProvider = ({ children, breed, limit }) => {
const [breeds, setBreeds] = useState([]);
const [showMore, setShowMore] = useState(limit);
useEffect(() => {
async function fetchData() {
if (breed.length !== 0) {
const request = await axios.get(requests.fetchBreed + 'page=1&limit=' + showMore + '&breed_id=' + breed);
setBreeds(request.data);
console.log('async', request.data.length);
return request;
}
}
fetchData();
}, [breed, showMore]);
return (
<BreedContext.Provider value={[breeds, setBreeds]}>
{children}
<div className="row">
<div className="col-md-3 col-sm-6 col-12">
<button type="button" className="btn btn-success" onClick={() => { setShowMore(showMore + limit) }}>Load more</button>
</div>
</div>
</BreedContext.Provider>
);
}
My problem here now is how do you reset the limit to 10 after you change your dropdown values? ReactJS is very new to me and I just learned this about 2 days ago.
hello I did the edit below to reset show More when breed is changed
import React, { useState, useEffect, createContext } from 'react';
import axios from '../services/axios';
import requests from '../services/requests';
export const BreedContext = createContext();
export const BreedProvider = ({ children, breed, limit,resetLimit}) => {
const [breeds, setBreeds] = useState([]);
const [showMore, setShowMore] = useState(limit);
useEffect(() => {
if (breed.length !== 0) {
setShowMore(limit);
}
}, [breed]);
useEffect(() => {
async function fetchData() {
if (breed.length !== 0) {
const request = await axios.get(requests.fetchBreed + 'page=1&limit=' + showMore + '&breed_id=' + breed);
setBreeds(request.data);
console.log('async', request.data.length);
return request;
}
}
fetchData();
}, [showMore,breed]);
return (
<BreedContext.Provider value={[breeds, setBreeds]}>
{children}
<div className="row">
<div className="col-md-3 col-sm-6 col-12">
<button type="button" className="btn btn-success" onClick={() => { setShowMore(showMore + limit) }}>Load more</button>
</div>
</div>
</BreedContext.Provider>
);
}
I created a component (sort of popup box) which displays a sign of horoscope, there’s an image and description. The popup box works correctly. I added a button ‘more’ to see more description, so I used a useState for it, but it doesn’t work, when I click on it doesn't show the rest of the text.
Thanks for your help !
const Modal = ({
children, visible, hide, fermer, more,
}) => {
const popup = `popup ${visible ? 'block' : 'hidden'}`;
return (
<div className={popup}>
{fermer ? null : (
<button className="close" onClick={hide} type="button">X</button>
)}
{children}
<button className="more" onClick={more} type="button">more</button>
</div>
);
};
export default Modal;
import './App.css';
import { useState } from 'react';
import Element from './Element';
import Modal from './Modal';
import Bd from './Bd';
function App() {
const bd = Bd.map((element) => (
<Element
nom={element.nom}
image={element.image}
description={element.description}
modulo={element.modulo}
/>
));
const [year, setYear] = useState('');
function handleChange(event) {
setYear(event.target.value);
}
const [signe, setSigne] = useState([]);
const [vis, setVis] = useState(false);
const [desc, setDesc] = useState(true);
function handleSubmit() {
setVis(true);
const yearModulo = Number(year) % 12;
Bd.map((element) => (
yearModulo === element.modulo ? setSigne(
[<h1>{element.nom}</h1>,
<div>{element.description.substr(0, 150)}</div>,
desc ? <div />
: <div>{element.description.substr(150, 600)}</div>,
<img src={`/images/${element.image}`} alt="" />,
],
) : false
));
}
return (
<div>
<div>
<input
className="text-center font-bold"
type="number"
id="year"
name="year"
value={year}
onChange={handleChange}
/>
<button type="submit" onClick={handleSubmit}>Valider</button>
</div>
<div className="flex flex-wrap">{bd}</div>
<Modal
visible={vis}
hide={() => setVis(false)}
more={() => setDesc(false)}
>
<div>
<div>{signe}</div>
</div>
</Modal>
</div>
);
}
export default App;
I would avoid storing in a local state a component (setSigne([<h1>{element.nom}</h1>,...). Prefer storing in the state the values that cannot be computed from other existing states, and generate the elements at rendering.
const [signe, setSigne] = useState(null);
function handleSubmit() {
setVis(true);
const yearModulo = Number(year) % 12;
setSigne(Bd.find(element => yearModulo === element.modulo));
}
// ...
<div>
{signe && <div>
<h1>{signe.nom}</h1>
<div>{signe.description.substr(0, 150)}</div>
{desc ? <div /> : <div>{signe.description.substr(150, 600)}</div>}
<img src={`/images/${signe.image}`} alt="" />
</div>}
</div>
Also, don’t forget to add a key prop when generating elements from an array:
const bd = Bd.map(element => (
<Element
key={element.nom}
// ...
I'm trying to simply pass the Id of a clicked item to display on another page under a different component ("Cart") . At the bottom of the code below, I have a button containing <Cart test={product.id} /> which extracts the Id that I want to be displayed in "Cart" when the button is clicked.
However, I am instead getting an error message of:
Objects are not valid as a React child (found: object with keys
{history, location, match, staticContext}). If you meant to render a
collection of children, use an array instead.
Is there a simple syntax error?
import React, { useState, useEffect, Cart } from 'react';
import './../App.css';
import * as ReactBootStrap from 'react-bootstrap';
function Item(props) {
const [product, setProduct] = useState([]);
const [loading, setLoading] = useState(false);
const [quantity, setQuantity] = useState(1);
const [cost, setCost] = useState([]);
useEffect(async () => {
fetchItems();
}, []);
const itemId = props.match.params.item;
const fetchItems = async () => {
const data = await fetch('https://fakestoreapi.com/products/' + itemId);
const items = await data.json();
setProduct(items);
setLoading(true);
setCost(items.price);
};
function priceUSD(change) {
return change.toFixed(2);
}
useEffect(() => {
const newCost = quantity * product.price;
setCost(priceUSD(newCost));
}, [quantity]);
return (
<div className="App">
<h2>Item</h2>
<div className="gridContainer">
{loading ? (
<div key={itemId} className="productStyle">
<img src={product.image} className="productImage"></img>
<p>{product.title}</p>
<p>{product.description}}</p>
<p>${priceUSD(product.price)}</p>
<div className="quantity">
<button
className="btn minus-btn"
type="button"
onClick={quantity > 1 ? () => setQuantity(quantity - 1) : null}
>
-
</button>
<input type="text" id="quantity" value={quantity} />
<button className="btn plus-btn" type="button" onClick={() => setQuantity(quantity + 1)}>
+
</button>
</div>
<button type="button" onClick={() => <Cart test={product.id} />}>
Add to shopping cart ${cost}
</button>
</div>
) : (
<ReactBootStrap.Spinner className="spinner" animation="border" />
)}
</div>
</div>
);
}
export default Item;
Cart
import React, { useState, Item } from 'react';
import './../App.css';
import './Item.js';
function Cart(test) {
return (
<div className="App">
<p>{test}</p>
</div>
);
}
export default Cart;
Component props are objects. You can read more about them in the official documentation.
You can either destructure the props of the Cart component:
function Cart({test}) {
return (
<div className="App">
<p>{test}</p>
</div>
);
}
or use explicitly test property of props:
function Cart(props) {
return (
<div className="App">
<p>{props.test}</p>
</div>
);
}
I'm creating a DatePicker and using useState hook to manage it's visibility. On div click I've added the event listener which changes value, but it didn't work as I expected. It works only the first time, so initial value changes to true, but on second and third clicks this value stays to true and DatePicker stays visible on click.
This is DatePicker
import React, { useState } from 'react';
import OutsideClickHandler from 'react-outside-click-handler';
import { renderInfo, getWeeksForMonth } from './utils';
import {
renderMonthAndYear,
handleBack,
handleNext,
weekdays,
} from '../../utils';
const DatePicker = ({
isOpen,
setIsOpen,
selected,
setSelected,
dayClick,
dayClass,
}) => {
const startDay = new Date().setHours(0, 0, 0, 0);
const [current, setCurrent] = useState(new Date(startDay));
const weeks = getWeeksForMonth(current.getMonth(), current.getFullYear());
function handleClick(date) {
if (date > startDay) {
setSelected(date);
setCurrent(date);
setIsOpen(false);
if (dayClick) {
dayClick(date);
}
}
}
return (
<div className="DatePicker-container">
<div
tabIndex="0"
role="button"
className="DatePicker-info"
onKeyPress={e => {
if (e.which === 13) {
setIsOpen(!isOpen);
}
}}
onClick={e => {
setIsOpen(!isOpen);
}}
>
{renderInfo(selected)}
</div>
{isOpen && (
<OutsideClickHandler onOutsideClick={() => setIsOpen(false)}>
<div className="DatePicker">
<div className="DatePicker__header">
<span
role="button"
onClick={() => handleBack(current, setCurrent)}
className="triangle triangle--left"
/>
<span className="DatePicker__title">
{renderMonthAndYear(current)}
</span>
<span
role="button"
onClick={() => handleNext(current, setCurrent)}
className="triangle triangle--right"
/>
</div>
<div className="DatePicker__weekdays">
{weekdays.map(weekday => (
<div
key={weekday}
className="DatePicker__weekday"
>
{weekday}
</div>
))}
</div>
{weeks.map((week, index) => (
<div
role="row"
key={index}
className="DatePicker__week"
>
{week.map((date, index) =>
date ? (
<div
role="cell"
key={index}
onClick={() => handleClick(date)}
className={dayClass(date)}
>
{date.getDate()}
</div>
) : (
<div
key={index}
className="DatePicker__day--empty"
/>
),
)}
</div>
))}
</div>
</OutsideClickHandler>
)}
</div>
);
};
export default DatePicker;
DateRangePicker which uses two.
import React, { useState } from 'react';
import DatePicker from './DatePicker';
import DatePickerContext from './DatePickerContext';
import './DatePicker.scss';
const DateRangePicker = () => {
const startDay = new Date().setHours(0, 0, 0, 0);
const [isOpen, setIsOpen] = useState(false);
const [isSecondOpen, setIsSecondOpen] = useState(false);
const [selected, setSelected] = useState(new Date(startDay));
const [secondSelected, setSecondSelected] = useState(new Date(startDay));
function dayClass(date) {
if (
selected.getTime() === date.getTime() ||
(date >= selected && date <= secondSelected)
) {
return 'DatePicker__day DatePicker__day--selected';
}
if (date < startDay || date < selected) {
return 'DatePicker__day DatePicker__day--disabled';
}
return 'DatePicker__day';
}
function dayClick(date) {
setSecondSelected(date);
setIsSecondOpen(true);
}
return (
<DatePickerContext.Provider>
<div className="DatePicker-wrapper">
<DatePicker
key={1}
isOpen={isOpen}
setIsOpen={setIsOpen}
selected={selected}
setSelected={setSelected}
dayClick={dayClick}
dayClass={dayClass}
/>
<DatePicker
key={2}
isOpen={isSecondOpen}
setIsOpen={setIsSecondOpen}
selected={secondSelected}
setSelected={setSecondSelected}
dayClass={dayClass}
/>
</div>
</DatePickerContext.Provider>
);
};
export default DateRangePicker;
a) It is a good practice to use functional updates to make sure to use correct "current" value when the next state is dependent on the previous (== current) state:
setIsOpen(currentIsOpen => !currentIsOpen)
b) It's very hard to reason about the next state when it gets updated by multiple handlers executed for the same event. Following 2 handlers might execute on the same click (the 1st div is "outside"):
<div ... onClick={e => setIsOpen(!isOpen)}>
<OutsideClickHandler onOutsideClick={() => setIsOpen(false)}>
If onOutsideClick executes first, then React re-renders with isOpen=false, and then onClick executes second, it would set isOpen=true as you observe - I don't see how the re-render could happen between, but maybe OutsideClickHandler is doing something nefarious or your code is more complicated than in the question ¯\_(ツ)_/¯
To enforce only 1 event handler:
<OutsideClickHandler onOutsideClick={(e) => {
e.stopPropagation();
setIsOpen(false);
}}>