I'm trying to play a gsap animation on component did mount in a Gatsby site but my refs aren't being applied.
const PricingList = ({ classes }) => {
let pricingCard = useRef([]);
useEffect(() => {
console.log('start Animation', pricingCard.current);
TweenMax.staggerFrom(pricingCard.current, 0.4, { opacity: 0, y: 100 }, 0.5);
}, []);
return (
<StaticQuery
query={graphql`
{
Prices: contentfulConfig {
pricing {
priceBand {
title
price
}
priceBand2 {
price
title
}
priceBand3 {
price
title
}
}
}
}
`}
render={(data) => (
<Fragment>
<div className={classes.Container}>
<PricingItem
ref={(el) => {
pricingCard.current[0] = el;
}}
/>
<PricingItem
ref={(el) => {
pricingCard.current[1] = el;
}}
/>
<PricingItem
ref={(el) => {
pricingCard.current[2] = el;
}}
/>
</div>
</Fragment>
)}
/>
);
};
I have tried -
pricingCard.current.push(el);
without any luck, I just get an empty array in console.
I have also tried -
useEffect(() => {
console.log('start Animation', pricingCard.current);
TweenMax.staggerFrom(pricingCard.current, 0.4, { opacity: 0, y: 100 }, 0.5);
}, [pricingCard]);
Thinking it might need to wait to be updated after the component mounted, but no luck.
Any help would be appreciated.
Thanks in advance.
For animation you might want to use useLayoutEffect instead of useEffect.
Is it going to be always 3 PricingItems? In this case you can create 3 separate refs for each (const pricingItem1 = useRef(null)) and in render <PricingItem ref={pricingItem1} />.
Also can you confirm that the function from the render prop has been called?
Related
I'm creating a snackbar system then use a function addSnackbar() to display a component in the screen, i'm use context and provider to save the list of snackbars.
OBS:
More than one snackbar can be display per time
The problem is, when any value of any element in the list change, all f my list is re-renderd and the close function fail. How I can solve this?
function Snackbar(props:SnackBarProps) {
useEffect(() => {
if(props.active) {
setTimeout(() => {
props.onClose();
}, props.autoHideDuration ?? 3000);
}
}, [props.active])
return(
<article className={`${styles.snackbar} ${props.active ? styles.active : ''}`}>
<div className={styles.column}>
<Heading4>{props.title}</Heading4>
<Paragraph>{props.children}</Paragraph>
</div>
<FaTimes onClick={props.onClose} className={styles.close} />
</article>
)
}
export function SnackbarGroup () {
const { snackbars, removeSnackbar } = useContext(SnackbarContext);
console.log("Snackbars", snackbars);
const onClose = useCallback((id:string) => {
removeSnackbar(id);
} , [removeSnackbar]);
const snackbarList = useMemo(() => {
return snackbars.map(snackbar => {
return (
<Snackbar
key={snackbar.id}
title={snackbar.title}
active={snackbar.active}
onClose={() => onClose(snackbar.id)}
autoHideDuration={snackbar.autoHideDuration}
>
{snackbar.message}
</Snackbar>
)
})
}, [snackbars])
return (
<div className={styles.snackbarGroup}>
{snackbarList}
</div>
)
}
I am using react-spring for animation and all the animations start once the page is loaded. I want to control the start of the animation. The desired outcome is to let the components down in the screen start the animation once they are in view (i.e the user scrolled down). The code follows something like this :
const cols = [
/*Components here that will be animated ..*/
{component: <div><p>A<p></div> , key:1},
{component: <div><p>B<p></div> , key:2},
{component: <div><p>C<p></div> , key:3},
]
export default function foocomponent(){
const [items, setItems] = React.useState(cols);
const [appear, setAppear] = React.useState(false); // Should trigger when the component is in view
const transitions = useTransition(items, (item) => item.key, {
from: { opacity: 0, transform: 'translateY(70px) scale(0.5)', borderRadius: '0px' },
enter: { opacity: 1, transform: 'translateY(0px) scale(1)', borderRadius: '20px', border: '1px solid #00b8d8' },
// leave: { opacity: 1, },
delay: 200,
config: config.molasses,
})
React.useEffect(() => {
if (items.length === 0) {
setTimeout(() => {
setItems(cols)
}, 2000)
}
}, [cols]);
return (
<Container>
<Row>
{appear && transitions.map(({ item, props, key }) => (
<Col className="feature-item">
<animated.div key={key} style={props} >
{item.component}
</animated.div>
</Col>
))}
</Row>
</Container>
);
}
I tried using appear && transitions.map(...) but unfortunately that doesn't work. Any idea how should I control the start of the animation based on a condition?
I use https://github.com/civiccc/react-waypoint for this type of problems.
If you place this hidden component just before your animation. You can switch the appear state with it. Something like this:
<Waypoint
onEnter={() => setAppear(true) }
/>
You can even specify an offset with it. To finetune the experience.
If you wish to have various sections fade in, scroll in, whatever on enter, it's actually very simple to create a custom wrapper. Since this question is regarding React Spring, here's an example but you could also refactor this a little to use pure CSS.
// React
import { useState } from "react";
// Libs
import { Waypoint } from "react-waypoint";
import { useSpring, animated } from "react-spring";
const FadeIn = ({ children }) => {
const [inView, setInview] = useState(false);
const transition = useSpring({
delay: 500,
to: {
y: !inView ? 24 : 0,
opacity: !inView ? 0 : 1,
},
});
return (
<Waypoint onEnter={() => setInview(true)}>
<animated.div style={transition}>
{children}
</animated.div>
</Waypoint>
);
};
export default FadeIn;
You can then wrap any component you want to fade in on view in this FadeIn component as such:
<FadeIn>
<Clients />
</FadeIn>
Or write your own html:
<FadeIn>
<div>
<h1>I will fade in on enter</h1>
</div>
</FadeIn>
I am trying to conditionally render part of an object (user comment) onClick of button.
The objects are being pulled from a Firebase Database.
I have multiple objects and want to only render comments for the Result component I click on.
The user comment is stored in the same object as all the other information such as name, date and ratings.
My original approach was to set a boolean value of false to each Result component and try to change this value to false but cannot seem to get it working.
Code and images attached below, any help would be greatly appreciated.
{
accumRating: 3.7
adheranceRating: 4
cleanRating: 2
date: "2020-10-10"
place: "PYGMALIAN"
staffRating: 5
timestamp: t {seconds: 1603315308, nanoseconds: 772000000}
userComment: "Bad"
viewComment: false
}
const results = props.data.map((item, index) => {
return (
<div className='Results' key={index}>
<span>{item.place}</span>
<span>{item.date}</span>
<Rating
name={'read-only'}
value={item.accumRating}
style={{
width: 'auto',
alignItems: 'center',
}}
/>
<button>i</button>
{/* <span>{item.userComment}</span> */}
</div >
)
})
You have to track individual state of each button toggle in that case.
The solution I think of is not the best but you could create a click handler for the button and adding a classname for the span then check if that class exists. If it exists then, just hide the comment.
Just make sure that the next sibling of the button is the target you want to hide/show
const toggleComment = (e) => {
const sibling = e.target.nextElementSibling;
sibling.classList.toggle('is-visible');
if (sibling.classList.contains('is-visible')) {
sibling.style.display = 'none'; // or set visibility to hidden
} else {
sibling.style.display = 'inline-block'; // or set visibility to visible
}
}
<button onClick={toggleComment}>i</button>
<span>{item.userComment}</span>
You can try like this:
const [backendData, setBackendData] = useState([]);
...
const showCommentsHandler = (viewComment, index) => {
let clonedBackendData = [...this.state.backendData];
clonedBackendData[index].viewComment = !viewComment;
setBackendData(clonedBackendData);
}
....
return(
<div>
....
<button onClick={() => showCommentsHandler(item.viewComment, index)}>i</button>
{item.viewComment && item.userComment}
<div>
You can store an array with that places which are clicked, for example:
const [ selectedItems, setSelectedItems] = React.useState([]);
const onClick = (el) => {
if (selectedItems.includes(el.place)) {
setSelectedItems(selectedItems.filter(e => e.place !== el.place));
} else {
setSelectedItems(selectedItems.concat(el));
}
}
and in your render function
const results = props.data.map((item, index) => {
return (
<div className='Results' key={index}>
<span>{item.place}</span>
<span>{item.date}</span>
<Rating
name={'read-only'}
value={item.accumRating}
style={{
width: 'auto',
alignItems: 'center',
}}
/>
<button onClick={() => onClick(item)}>i</button>
{ /* HERE */ }
{ selectedItems.includes(item.place) && <span>{item.userComment}</span> }
</div >
)
})
You need to use useState or your component won't update even if you change the property from false to true.
In order to do so you need an id since you might have more than one post.
(Actually you have a timestamp already, you can use that instead of an id.)
const [posts, setPosts] = useState([
{
id: 1,
accumRating: 3.7,
adheranceRating: 4,
cleanRating: 2,
date: "2020-10-10",
place: "PYGMALIAN",
staffRating: 5,
timestamp: { seconds: 1603315308, nanoseconds: 772000000 },
userComment: "Bad",
viewComment: false
}
]);
Create a function that updates the single property and then updates the state.
const handleClick = (id) => {
const singlePost = posts.findIndex((post) => post.id === id);
const newPosts = [...posts];
newPosts[singlePost] = {
...newPosts[singlePost],
viewComment: !newPosts[singlePost].viewComment
};
setPosts(newPosts);
};
Then you can conditionally render the comment.
return (
<div className="Results" key={index}>
<span>{item.place}</span>
<span>{item.date}</span>
<Rating
name={"read-only"}
value={item.accumRating}
style={{
width: "auto",
alignItems: "center"
}}
/>
<button onClick={() => handleClick(item.id)}>i</button>
{item.viewComment && <span>{item.userComment}</span>}
</div>
);
Check this codesandbox to see how it works.
I have an intersectionObserver that watches some sections and highlights the corresponding navigation item. But I've only managed to get the "main sections Microsoft, Amazon working, but not the subsections Define, Branding, Design, Deduction. As seen in the gif below:
The reason why I want it structured this way is so that I can highlight the "main" sections if the subsections are in view.
Semi working demo: https://codesandbox.io/s/intersection-with-hooks-fri5jun1344-fe03x
It might seems that I might be able to copy and paste the same functionality with the subsections as well. But I'm having a hard time wrapping my head around how to deal with nested data + useRef + reducer. I was wondering if someone could give me a pointer in the right direction.
Here is an gif of the desired effect. Notice the main title (Loupe, Canon) are still highlighted if one of the subsections are in view:
It all starts with an data array
const data = [
{
title: "Microsoft",
id: "microsoft",
color: "#fcf6f5",
year: "2020",
sections: ["define", "branding", "design", "deduction"]
},
{
title: "Amazon",
id: "amazon",
color: "#FFE2DD",
year: "2018",
sections: ["define", "design", "develop", "deduction"]
},
{
title: "Apple",
id: "apple",
color: "#000",
year: "2020",
sections: ["about", "process", "deduction"]
}
];
App.js padding data object into reduce to create Refs
const refs = data.reduce((refsObj, Case) => {
refsObj[Case.id] = React.createRef();
return refsObj;
}, {});
My components passing in the props
<Navigation
data={data}
handleClick={handleClick}
activeCase={activeCase}
/>
{data.map(item => (
<Case
key={item.id}
activeCase={activeCase}
setActiveCase={setActiveCase}
refs={refs}
data={item}
/>
))}
Case.js
export function Case({ data, refs, activeCase, setActiveCase }) {
const components = {
amazon: Amazon,
apple: Apple,
microsoft: Microsoft
};
class DefaultError extends Component {
render() {
return <div>Error, no page found</div>;
}
}
const Tag = components[data.id] || DefaultError;
useEffect(() => {
const observerConfig = {
rootMargin: "-50% 0px -50% 0px",
threshold: 0
};
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.target.id !== activeCase && entry.isIntersecting) {
setActiveCase(entry.target.id);
}
});
}, observerConfig);
observer.observe(refs[data.id].current);
return () => observer.disconnect(); // Clenaup the observer if unmount
}, [activeCase, setActiveCase, refs, data]);
return (
<React.Fragment>
<section
ref={refs[data.id]}
id={data.id}
className="section"
style={{ marginBottom: 400 }}
>
<Tag data={data} />
</section>
</React.Fragment>
);
}
I've tried mapping the subsections like this but I get stuck at this part:
const subRefs = data.map((refsObj, Case) => {
refsObj[Case] = React.createRef();
return refsObj;
}, {});
Working Example
I've found a solution while trying to keep most of your logic intact. Firstly what you need to do is to store the subrefs (the sections ref) in the same object as your Case ref. So you will need an extra reduce function to create those inside the refs object:
App.js
const refs = data.reduce((refsObj, Case) => { // Put this outside the render
const subRefs = Case.sections.reduce((subrefsObj, Section) => {
subrefsObj[Section] = React.createRef();
return subrefsObj;
}, {});
refsObj[Case.id] = {
self: React.createRef(), // self is the Case ref, like Apple, Microsoft...
subRefs // This is going to be the subrefs
};
return refsObj;
}, {});
Then you add an extra state to handle which sub section is active, like const [activeSection, setActiveSection] = React.useState(); And you put it anywhere you also use the activeCase. You need that because you said that the Case and Sections need to work independently. (Both active at the same time).
Case.js
You will need to pass along the subrefs to the child components, so you do:
<Tag data={data} subRefs={refs[data.id].subRefs} />
And you will also need the intersection observer for each of the subrefs. So your useEffect will look like:
useEffect(() => {
const observerConfig = {
rootMargin: "-50% 0px -50% 0px",
threshold: 0
};
const observerCallback = (entries, isCase) => {
const activeEntry = entries.find(entry => entry.isIntersecting);
if (activeEntry) {
if (isCase) setActiveCase(activeEntry.target.id);
else setActiveSection(activeEntry.target.id);
} else if (isCase) {
setActiveCase(null);
setActiveSection(null);
}
};
const caseObserver = new IntersectionObserver(
entries => observerCallback(entries, true),
observerConfig
);
caseObserver.observe(refs[data.id].self.current);
const sectionObserver = new IntersectionObserver(
entries => observerCallback(entries, false),
observerConfig
);
Object.values(refs[data.id].subRefs).forEach(subRef => {
sectionObserver.observe(subRef.current);
});
return () => {
caseObserver.disconnect();
sectionObserver.disconnect();
}; // Clenaup the observer if unmount
}, [refs, data]);
Then in your amazon/index.js ,microsoft/index.js and apple/index.js files. You pass along the ref again:
<Template
data={this.props.data}
caseSections={caseSections}
subRefs={this.props.subRefs}
/>
Finally, in your template.js file you will have the following so you can assign the right ref:
const Template = props => {
return (
<React.Fragment>
<div
sx={{
background: "#eee",
transition: "background ease 0.5s"
}}
>
{props.data.sections &&
props.data.sections.map(subItem => (
<Container
ref={props.subRefs && props.subRefs[subItem]}
id={`${props.data.id}--${subItem}`}
key={subItem}
className="article"
>
<Section sectionId={subItem} caseSections={props.caseSections} />
</Container>
))}
</div>
</React.Fragment>
);
};
I believe most of it is covered in the post. You can check your forked working repo here
You can simplify your code. You don't really need refs or intersectionObservers for your use case. You can simply scrollIntoView using document.getElementById (you already have ids to your navs.
You can do setActiveCase very well in handleClick.
Working demo
Modify handleClick like this
const handleClick = (subTabId, mainTabName) => {
//console.log("subTabName, mainTabName", subTabId, mainTabName);
setActiveCase({ mainTab: mainTabName, subTab: subTabId.split("--")[1] }); //use this for active tab styling etc
document.getElementById(subTabId).scrollIntoView({
behavior: "smooth",
block: "start"
});
};
Navigation.js Call handleClick like this.
{item.sections &&
item.sections.map(subItem => (
<div
className={`${styles.anchor}`}
key={`#${item.title}--${subItem}`}
sx={{ marginRight: 3, fontSize: 0, color: "text" }}
href={`#${item.title}--${subItem}`}
onClick={e => {
handleClick(`${item.id}--${subItem}`, item.id);
e.stopPropagation();
}}
>
{toTitleCase(subItem)}
</div>
))}
This might refer to other relevant general questions like how to update a child component from the parent, though I'd like to hear any fair judgement of my design solution to the following scenario.
I have a parent class where I store css attributes for 2 children objects.
import React from 'react'
import Item from './item/Item'
class Small_gallery extends React.Component {
constructor(props) {
super(props);
this.state = {
chosenVal: 0,
};
this.listObjParams = [
// Style 1
{
left: 300,
zIndex: 0
},
//Style 2
{
left: 320,
zIndex: 1
}
];
this.handleClick = this.handleClick.bind(this);
this.calculateShift = this.applyNewStyle.bind(this);
this.listItems = this.listObjParams.map((objStyle, i) =>
<Item
key={i}
id={i}
objStyle={objStyle}
onClick={this.handleClick}
/>
);
}
handleClick = (indexFromChild) => {
this.setState({chosenVal: indexFromChild});
this.applyNewStyle(indexFromChild)
};
applyNewStyle = (clickedIndex) => {
if (clickedIndex === 0) {
// somehow I want to apply new css style 2 to the clicked? <Item> child
};
render() {
return (
<div>
{this.listItems}
</div>
)
}
Child component is rather trivial:
class Item extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<div
onClick={(e) => {
e.preventDefault();
this.props.onClick(this.props.id)
}}
style={{
left: this.props.objStyle.left,
zIndex: this.props.objStyle.zIndex
}}
>
</div>
);
}
}
The question is: how can I apply style 1 or 2 to the clicked Item component(depending on the index I am returning)? I've read about getDerivedStateFromProps instead of using deprecated componentWillReceiveProps here https://hackernoon.com/replacing-componentwillreceiveprops-with-getderivedstatefromprops-c3956f7ce607 but it's not a solution for me.
I expect number of created Items to grow in the future to 10-20, so it makes no sense to populate state of Item with this.listObjParams when creating it, or am I wrong here?
I have a working example below, so to cover what I did:
Create a prop that takes an array of items, more items more looped <Item />'s will appear.
Styles are either activeStyles || inactiveStyles it is based on the currentId matching the id from object (from array prop = items).
import React from "react";
const inactiveStyles = {
left: 300,
zIndex: 0,
backgroundColor: "#E9573F"
};
const activeStyles = {
left: 320,
zIndex: 1,
backgroundColor: "#00B1E1"
};
const inboundItems = [
{
id: 0
},
{
id: 1
},
{
id: 2
}
];
// Note - added to show it working not needed
const defaultStyles = {
display: "block",
border: "1px solid black",
width: 50,
height: 50
};
export const Item = ({ id, onClick, style }) => (
<>
<pre>{JSON.stringify({ styles: style }, null, 2)}</pre>
<div
{...{ id }}
style={{ ...defaultStyles, ...style }}
onClick={e => {
e.preventDefault();
onClick(id);
}}
/>
</>
);
export const SmallGallery = ({ items = inboundItems }) => {
const [currentId, setCurrentId] = React.useState(null);
const getStyles = selectedId => {
return currentId === selectedId ? activeStyles : inactiveStyles;
};
return items.map(({ id, ...item }) => (
<Item
key={id}
{...{ id }}
{...item}
style={getStyles(id)}
onClick={selectedId => setCurrentId(selectedId)}
/>
));
};
export default SmallGallery;
Let me know what you think, I added a screenshot to show styles being added.
For <Item/> you can use simple functional component. Optimal for simple, not so complex use cases.
E.g
const Item = ({ id, clickHandler, objStyle }) => (
<div
onClick={e => {
e.preventDefault();
clickHandler(id);
}}
style={...objStyle}
/>
);
PureComponent will be updated on props change, too.
In full class component you can use shouldComponentUpdate() to force rerendering on props change. No need to duplicate data (into state) using getDerivedStateFromProps (depends on use case).
Search for some tutorials (f.e. typical todo examples) since you have no idea about state management, updating etc.
Placing listObjParams outside of state won't force rerendering on update. BTW it looks more like a style pool - maybe you should have a child params array... you can combine it with style index array or keep them (and pass as props) separately.
constructor(props) {
super(props);
this.state = {
// chosenVal: 0, // temporary handler param? probably no need to store in the state
listObjStyles: [0, 1] // style indexes
};
this.stylePool = [
// Style 1
{
left: 300,
zIndex: 0
},
//Style 2
{
left: 320,
zIndex: 1
}
];
usage:
this.listItems = this.state.listObjStyles.map((styleIndex, i) => <Item
key={i}
id={i}
objStyle={this.stylePool[ styleIndex ]}
clickHandler={this.handleClick}
/>
Updating listObjStyles (setState()) will force rerendering, updating this.stylePool won't (move to the state if rerendering required).
Of course stylePool can contain more than 2 styles for different item 'states'. You can make styles for selected, liked, unliked - by storing indexes in an array you can mix any of them with custom logic (f.e. only one selected, many liked).
10-20 items is not the case where you need special optimizations (other than avoiding unnecessary rerenderings).
Just to sum up what I've done to make it all work based on two answers (still a rather toy example):
Parent:
import Item from './item/Item'
class Small_gallery extends React.Component {
constructor(props) {
super(props);
this.state = {
listObjStyles: [0, 1]
};
this.stylePool = [
{
position: 'absolute',
width: 600,
left: 300,
height: 100,
backgroundColor: '#000',
zIndex: 0,
transition: 'all 1s ease'
},
{
position: 'absolute',
width: 600,
left: 720,
height: 350,
backgroundColor: '#ccc',
zIndex: 1,
transition: 'all 2s ease'
}]
}
handleClick = (indexFromChild) => {
console.log(indexFromChild)
if (indexFromChild === 0) {
this.setState({
listObjStyles: [1, 0]
})
} else if (indexFromChild === 1) {
this.setState({
listObjStyles: [0, 1]
})
}
}
render() {
return (
<>
<div style={{display: 'flex', margin: 40}}>
{this.state.listObjStyles.map((styleIndex, i) =>
<Item
key={i}
id={i}
objStyle={this.stylePool[styleIndex]}
onClick={this.handleClick}
/>
)}
</div>
</>)
}
}
Child:
const Item = ({id, onClick, objStyle}) => (
<div
onClick={e => {
e.preventDefault();
onClick(id)
}}
style={{...objStyle}}
/>
);
export default Item