I have a component and render it conditionally with different props.
{activeNavItem === 'Concept Art' ? (
<Gallary
images={conceptArtImages}
sectionRef={sectionRef}
/>
) : (
<Gallary
images={mattePaintingImages}
sectionRef={sectionRef}
/>
)}
This component has useState(false) and useEffect hooks. useEffect determines when screen position reaches the dom element and it triggers useState to true: elementPosition < screenPosition. Then my state triggers class on dom element: state ? 'animationClass' : ''.
const Gallary = ({ images, sectionRef }) => {
const [isViewed, setIsViewed] = useState(false);
useEffect(() => {
const section = sectionRef.current;
const onScroll = () => {
const screenPosition = window.innerHeight / 2;
const sectionPosition = section.getBoundingClientRect().top;
console.log(screenPosition);
if (sectionPosition < screenPosition) setIsViewed(true);
};
onScroll();
window.addEventListener('scroll', onScroll);
return () => {
window.removeEventListener('scroll', onScroll);
};
}, [sectionRef]);
return (
<ul className="section-gallary__list">
{images.map((art, index) => (
<li
key={index}
className={`section-gallary__item ${isViewed ? 'animation--view' : ''}`}>
<img className="section-gallary__img" src={art} alt="concept art" />
</li>
))}
</ul>
);
};
Problem: it works on my first render. But when I toggle component with different props, my state iniatially is true and I haven't animation.
I notice that if I have two components(ComponentA, ComponentB) instead of one(ComponentA) it works fine.
try setting isViewed to false when your component is not in view like this:
if (sectionPosition < screenPosition && !isViewed){
setIsViewed(true);
}
else{
if(isViewed)
setIsViewed(false);
}
and you can do it like this:
if (sectionPosition < screenPosition && !isViewed){
setIsViewed(state=>!state);
}
else{
if(isViewed)
setIsViewed(state=>!state);
}
plus no need to render same component multiple times, you can change props only:
<Gallary
images={activeNavItem === 'ConceptArt'?conceptArtImages:mattePaintingImages}
sectionRef={sectionRef}
/>
Related
I have one component which I want to get ref and pass it to other component (fixed to top navbar with opacity 0) to check when the offsetTop of this ref is smaller than window.pageYOffset and if it is, change my state and set opacity of my navbar to 1. The problem is when I pass the ref, the other components gets null causing errors. How do I pass this ref when it's not null?
const galleryRef = useRef(null);
<div className="gallery py-5 border" ref={galleryRef}>
<FixedNavbar ref={galleryRef} />
</div>
FixedNavBar.js
const handleFixedNavbar = () => {
if (window.pageYOffset > ref.current.offsetTop) {
setIsFixed(true);
} else {
setIsFixed(false);
}
};
useEffect(() => {
window.addEventListener('scroll', handleFixedNavbar);
return () => {
window.removeEventListener('scroll', handleFixedNavbar);
};
}, []);
<div ref={navMenusRef} className={`${isFixed ? 'fixed-nav' : ''}`}>
{navMenus.map((item, index) => (
<Link
key={index}
to={item.type}
data-id={item.id}
>
{item.name}
</Link>
))}
</div>
You can use a Callback Ref with useState() to cause a re-render when the ref is updated:
const [galleryRef, setGalleryRef] = useState(null);
<div className="gallery py-5 border" ref={setGalleryRef}>
<FixedNavbar galleryRef={galleryRef} />
</div>
The ref is passed as a standard prop (galleryRef), and the useEffect() is dependent on it, and would be called whenever it changes:
useEffect(() => {
if(!galleryRef) return; // do nothing when ref is not set
const handleFixedNavbar = () => {
setIsFixed(window.pageYOffset > galleryRef.offsetTop); // sets true or false according to condition
};
handleFixedNavbar(); initial set
window.addEventListener('scroll', handleFixedNavbar);
return () => {
window.removeEventListener('scroll', handleFixedNavbar);
};
}, [galleryRef]);
I m having one child component which is inside a loop of parent component. when one of the child components is updating the state of parent component, it is re-rendering the all children since it is loop. How can i avoid the re-render for each iteration.
function Parent() {
const [selectedChild, setSelectedChild] = useState([]);
const onChangeHandle = (event, id) => {
const checked = event.target.checked;
let updatedArray = [...selectedChild];
if(checked){
if(!selectedChild.includes(id)){
updatedArray.push(id);
}
}
else{
var index = updatedArray.indexOf(id)
if (index !== -1) {
updatedArray.splice(index, 1);
}
}
setSelectedChild(updatedArray);
}
const dummy = (id) => {
return selectedChild.includes(id);
}
return (
<div>
<table>
<tbody>
{[1,2,3].map((value, index) => {
return (
<Child
key={index}
index={index}
value={value}
handle={onChangeHandle}
isSelected={dummy}
/>
)
})}
</tbody>
</table>
<div>
{selectedChild}
</div>
</div>)
}
function Child({index, value, handle, isSelected }) {
console.log('rendering')
return (
<tr>
<td>
<input
type="checkbox"
checked={isSelected(index)}
onChange={(event) => handle(event, index)}/>
</td>
<td>hello {index} {value}</td>
</tr>
)
}
export default function App() {
return (
<div className="App">
<Parent />
</div>
);
}
Current behaviour:
In above code, When i m clicking on the checkbox in one of the children component, it is updating the parent component state(selectedChild). So the loop is executing and all children(all table rows) are re rendering.
Expected behaviour:
Only that particular row have to go for re-render
Demo: https://codesandbox.io/s/newpro-0pezc
for that you can use React.memo that will memoize your component if props remains the same. But given your code you need to make some extra changes:
you have to apply useCallback to memoize onChangeHandle function;
to memoize properly onChangeHandle you need to refactor it. you can't pass selectedChild directly, otherwise it memoizes its value. use setSelectedChild passing as argument a function that takes selectedChild instead.
your Child should receive isSelected as boolean value instead of function. otherwise props will remain the same and Child never updates;
import React, { useState, memo, useCallback } from "react";
function Parent() {
const [selectedChild, setSelectedChild] = useState([]);
const onChangeHandle = useCallback((event, id) => {
setSelectedChild(selectedChild => {
const checked = event.target.checked;
let updatedArray = [...selectedChild];
if (checked) {
if (!selectedChild.includes(id)) {
updatedArray.push(id);
}
} else {
var index = updatedArray.indexOf(id);
if (index !== -1) {
updatedArray.splice(index, 1);
}
}
return updatedArray;
});
}, []);
const dummy = id => {
return selectedChild.includes(id);
};
const renderChildren = () =>
[1, 2, 3].map((value, index) => {
return (
<Child
key={index}
index={index}
value={value}
handle={onChangeHandle}
isSelected={dummy(index)}
/>
);
});
return (
<div>
<table>
<tbody>{renderChildren()}</tbody>
</table>
<div>{selectedChild}</div>
</div>
);
}
const Child = memo(({ index, value, handle, isSelected }) => {
console.log("rendering");
return (
<tr>
<td>
<input
type="checkbox"
checked={isSelected}
onChange={event => handle(event, index)}
/>
</td>
<td>
hello {index} {value}
</td>
</tr>
);
});
export default function App() {
return (
<div className="App">
<Parent />
</div>
);
}
https://stackblitz.com/edit/so-memo-children?file=src/App.js
The basic answer is use React.memo on Child.
const Child = memo(function Child(...) {...})
But to make memo work, the component needs to receive the same props if it shouldn't get rerendered. That means using useCallback on onChangeHandle:
const onChangeHandle = useCallback((event, id) => {...}, [])
But since onChangeHandle uses selectedChild that always changes on checkbox change, you'll also need to ref it using useRef:
const selectedChildRef = useRef();
selectedChildRef.current = selectedChild;
and use reffed version inside of onChangeHandle.
The last thing that needs to be done is to change isSelected prop from function to just a flag since it needs to be run on each checkbox change:
isSelected={selectedChild.includes(index)}
https://codesandbox.io/s/newpro-forked-wxvqs
You could implement shouldComponentUpdate (doc: https://reactjs.org/docs/react-component.html#shouldcomponentupdate) inside the definition of Child to have more control over when it rerenders. But that's only meant for cases where you have performance issues- generally you don't have to worry about it, and letting them all rerender is standard.
I need to lift up the state of my Child component to the Parent to be able to reset this within the resetTotals() function. Each child component within the map has a click counter, so this needs to be reset within the parent component onClick.
How can I pass up the state?
// Parent Functional Component
function Parent() {
const [calorieCount, setCalorieCount] = useState(0);
const [vitaminACount, setVitaminACount] = useState(0);
const [vitaminDCount, setVitaminDCount] = useState(0);
function updateTotals(calories = 0, vitaminA = 0, vitaminD = 0) {
setCalorieCount(prevCalorieCount => Math.round(prevCalorieCount + calories));
setVitaminACount(prevVitaminACount => Math.round((prevVitaminACount + vitaminA) * 100) / 100);
setVitaminDCount(prevVitaminDCount => Math.round((prevVitaminDCount + vitaminD) * 100) / 100);
}
function resetTotals() {
setCalorieCount(0);
setVitaminACount(0);
setVitaminDCount(0);
}
return (
<div className="App">
<main className="products-grid flex flex-wrap">
{FoodCards.map((item, i) => {
return <Child
key={item.id}
name={item.name}
img={item.img}
calories={item.calories}
vitamin_a={item.vitamin_a}
vitamin_d={item.vitamin_d}
updateTotals={updateTotals} />
})}
</main>
<footer>
<div
className="reset"
onClick={() => resetTotals()}
>Reset</div>
</footer>
</div>
);
}
export default App
// Child Functional Component
const Child = (props) => {
const [clickCount, setClickCount] = useState(0);
function handleUpdateTotals(calories, vitamin_a, vitamin_d) {
props.updateTotals(calories, vitamin_a, vitamin_d);
setClickCount(prevClickCount => prevClickCount + 1);
}
return (
<div
className="product"
onClick={() => handleUpdateTotals(props.calories, props.vitamin_a, props.vitamin_d)}
>
<p>{props.name}</p>
<p>{clickCount > 0 ? <p>Selected: {clickCount}</p> : <p>Not Selected</p>}</p>
<img src={props.img} alt="" />
</div>
);
}
You are already updating the parent state from the child in that code.
You are passing in a callback function as a property, then calling it by props.updateTotals(). That will then run the updateTotals function in parent.
Do the same for reset totals: pass the method in as a prop, and call it from the child.
I got a problem trying to make my own Autocomplete component in react..
I created a component which render the results and set max-height on their wrapper,
I used an onKeyDown event on the input element to track down/up key press. Right now I use it to mark the active item... but when the max-height I set is too small and there is a scroll in the side when the "active-item" go off the div's height limit the scroll doesn't go down with it... How can I fix it?
import React, { useState, useEffect } from "react"
const Autocomplete = ({ options }) => {
const [activeOption, setActiveOption] = useState(4)
const [filteredOptions, setFilteredOptions] = useState([])
const [showOptions, setShowOptions] = useState(false)
const [userInput, setUserInput] = useState("")
useEffect(() => {
setShowOptions(userInput)
setFilteredOptions([
...options.filter(
option => option.toLowerCase().indexOf(userInput.toLowerCase()) > -1
)
])
setActiveOption(0)
}, [userInput])
const handleKeyDown = e => {
if (e.key === "ArrowDown") {
if (activeOption === filteredOptions.length - 1) return
setActiveOption(activeOption + 1)
}
if (e.key === "ArrowUp") {
if (activeOption === 0) return
setActiveOption(activeOption - 1)
}
}
return (
<>
<div className="search">
<input
type="text"
className="search-box"
value={userInput}
onChange={e => setUserInput(e.target.value)}
onKeyDown={handleKeyDown}
/>
<ul className="options">
{showOptions &&
filteredOptions.map((option, i) => (
<li className={activeOption === i ? `option-active` : ``} key={i}>
{option}
</li>
))}
</ul>
</div>
</>
)
}
export default Autocomplete
function App() {
return (
<div className="App">
<Autocomplete
options={[
"Alligator",
"Bask",
"Crocodilian",
"Death Roll",
"Eggs",
"Jaws",
"Reptile",
"Solitary",
"Tail",
"Wetlands"
]}
/>
</div>
)
}
.option-active {
font-weight: bold;
background: cornflowerblue;
}
.options {
height: 100px;
overflow: overlay;
}
Here's a picture to explain better my problem:
when second item is active:
when the sixth is active:
As you can see the scroll stays the same and doesn't go down with the li element...
Thanks by heart!
I think you could maintain an array of element refs, in order to call scrollIntoView method of that element, when active option changes.
Untested code:
// Use a ref container (we won't use `current`)
const elmRefs = useRef()
// Instead we build a custom ref object in each key of the ref for each option
useEffect(() => {
options.forEach(opt => elmRefs[opt] = {current: null}
}, [options])
// Effect that scrolls active element when it changes
useLayoutEffect(() => {
// This is what makes element visible
elmRefs[options[activeOption]].current.scrollIntoView()
}, [options, activeOption])
// In the "render" section, connect each option to elmRefs
<li className={activeOption === i ? `option-active` : ``} ref={elmRefs[option]} key={i}>
{option}
</li>
Let me know what you think!
Edit:
If the elmRefs need to be initialized imediately, you could do:
// Outside component
const initRefs = options => Object.fromEntries(options.map(o => [o, {current: null}]))
// Then in the component, replace useRef by:
const elmRefs = useRef(initRef(options))
And replace elmRefs by elmRefs.current in the rest of the code...
i have this breadcrump component that map over props and renders a list of chip components like this:
class BreadCrumb extends React.Component {
render () {
const {
steps,
activeIndex
} = this.props;
const chips = steps
.map((step,index) => {
return <Chip
key={index}
title={step.category}
onClick = {()=> this.props.selectChip(index)} // this should be passed only if
// active == true
active={activeIndex >= index} />
})
return (
<div className="chip-container">
{chips}
</div>
)
}
}
i need to click on chips only if his active prop is true,
this is the chip component
class Chip extends React.Component {
render(){
const {
active,
title
} = this.props;
const activeClassName = active ? 'chip active' : 'chip';
return (
<div
className = {activeClassName}
onClick = {() => this.props.onClick()} >
<span>{title}</span>
</div>
)
}
}
how can i make chip clickable only if the active prop is true?
For further information selectChip() function sets the state of a component App, parent of Breadcrump component, so it is binded to App component.
You could e.g. make that onClick function as a class method and use a simple condition inside:
class Chip extends React.Component {
handleClick = () => {
if (this.props.active) {
this.props.onClick(); // call only if active props is true
}
}
render() {
const { active, title } = this.props;
const activeClassName = active ? 'chip active' : 'chip';
return (
<div
className = {activeClassName}
onClick = {this.handleClick}
>
<span>{title}</span>
</div>
)
}
}
Either execute the handler or an empty function
onClick = {isActive ? this.props.onClick : () =>{} } >
You can do it like this:-
// If chip component expects a function all the time
<Chip
key={index}
title={step.category}
onClick = {step.active ? ()=> this.props.selectChip(index) : () => {}}
active={activeIndex >= index} />
// If onClick is an optional prop to chip component
<Chip
key={index}
title={step.category}
onClick = {step.active ? ()=> this.props.selectChip(index) : undefined}
active={activeIndex >= index} />
// of onClick handler is optional, possibly an alternative solution
type ChipProps = {
title: string;
active: boolean;
onClick?: ()=>void;
}
<Chip
key={index}
title={step.category}
active={activeIndex >= index}
{...(step.active ? {onClick:()=> this.props.selectChip(index)} : {})}
/>