Something that used to be simple is quite complicated when you don't know the React way.
I'm trying to create a component to act like a sticky header or footer when it reaches the top of the page.
At the moment I'm quite content in adding the below:
componentDidMount() {
window.addEventListener('scroll', this.onScroll, false);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.onScroll, false);
}
However I've hit a wall to how I get the scroll top position of a styled component. So lets say I had a styled component called Container and that outputted a form I usually just add a data attribute and do something like the below:
const container = document.getElementbyTag("data-sticky-container")
const position = container.offsetTop
How would I go about doing this in React?
Update
Now using ref. I've fallen into a problem where current isn't defined:
constructor(props) {
super(props);
this.optionBox = React.createRef();
}
componentDidMount() {
window.addEventListener('scroll', this.onScroll, false);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.onScroll, false);
}
onScroll() {
console.log(this.optionBox.current.offsetTop);
}
In react you would use a reference for your component: https://reactjs.org/docs/refs-and-the-dom.html
You would have something like this:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
getOffset = () => {
console.log(this.myRef.current.offsetTop);
}
render() {
return <Container ref={this.myRef} onClick={this.getOffset} />;
}
}
And now you can access the container offsetTop by this.myRef.current.offsetTop inside of your onScroll function like in getOffset
Another option is to use an innerRef. This can be better because it will give you a reference to the DOM node and not just a wrapper. See this answer.
You would then have more direct access to properties like scrollTop.
Unlike ref, innerRef uses a callback:
In your case it would look like:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef;
}
getOffset = () => {
console.log(this.myRef.offsetTop);
}
render() {
return <Container
innerRef={element => this.textInput = element}
onClick={this.getOffset} />;
}
}
You can add a ref
to the component for which u want an offsetTop. This ref will have all the computed css values of that component.
Related
I am trying to change toolbar background color as #fff color when body scrolled.Otherwise it will be transparent.
Here is sample toolbar component:
export default class ToolBar extends React.Component {
constructor(props){
super(props)
this.state = {
scrolled:false
}
}
render() {
const { children, className,scrolled, ...other } = this.props
return (
<nav style={{backgroundColor:this.state.scrolled?'#fff':'transparent'}}>
{children}
</nav>
)
}
}
How can we do this with react?
Just add an event listener to window object.
componentDidMount() {
window.addEventListener('scroll', this.checkScroll);
}
checkScroll = () => {
this.setState({ scrolled: window.scrollY > 0 });
};
Note: You would probably also need some debounce to avoid rapid and multiple set states.
And remember to disconnect the listener on component destroy.
componentWillUnmount() {
window.removeEventListener('scroll', this.checkScroll);
}
I'm making a simple scroll-to-top component and I thought that React will only re-render a component if something in it changes. Since I have a conditional tied to state in my render, shouldn't React only render it if the state changes? Instead, I'm seeing it re-render with every little scroll.
Also, if I left it as-is, are there any downsides to it re-rendering so much?
import React from 'react';
import './scroll-to-top.css';
export default class extends React.Component {
state = {
shouldShowButton: false
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
handleScroll = () => {
this.setState({
shouldShowButton: window.scrollY > 250 ? true : false
});
}
render () {
{console.log("i have rendered!")}
return (
this.state.shouldShowButton ? <a className="scroll-to-top" href="#">Return to Top</a> : null
);
};
};
Welcome to Stack Overflow :)
Let's think through your code.
When the component loads, you're attaching a listener to the scroll event:
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
}
This fires handleScroll when the user scrolls. handleScroll sets the state of the component, regardless of whether or not the ternary condition resolves as true or false:
handleScroll = () => {
this.setState({
shouldShowButton: window.scrollY > 250 ? true : false
});
}
Whenever we use setState, React triggers render. Hence, render is triggering with every little scroll.
Downsides - you should be really careful of attaching anything to scroll, as it can affect performance. You might consider debouncing the event if you really, really need to do so. (Where debouncing is the technique of rate-limiting how many times a function can be called.)
This happens, because you are calling handleScroll function every time scroll event is fired. To fix this, setState only in condition:
import React from 'react';
import './scroll-to-top.css';
export default class extends React.Component {
state = {
shouldShowButton: false
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
handleScroll = () => {
const {shouldShowButton} = this.state;
if (!shouldShowButton && window.scrollY > 250) {
this.setState({
shouldShowButton: true
});
} else if (shouldShowButton && window.scrollY <= 250) {
this.setState({
shouldShowButton: false
});
}
}
render () {
{console.log("i have rendered!")}
return (
this.state.shouldShowButton ? <a className="scroll-to-top" href="#">Return to Top</a> : null
);
};
};
No, it's typical for Component. It's re-rendered(not in DOM but in virtual DOM) each time .setState is called, props are changes or parent element is re-rendered.
Just an example how re-rendering parent also fires re-rendering for child:
import React from "react";
import ReactDOM from "react-dom";
class Child extends React.Component {
render() {
console.log('child re-rendered');
return 'test';
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {a: 1};
setInterval(() => this.setState(oldState => ({...oldState, a: oldState.a + 1})), 1000);
}
render() {
return (
<div className="App">
<Child />
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Here you can check that child is re-rendered in the line as parent's .setState is called.
But it is not 100% to be issue for performance. Virtual DOM is much faster than browser DOM.
But if you want to avoid such a behavior you may use React.PureComponent instead of React.Component and then it will not be re-rendered on parent's update. Also PureComponent handles case when .setState does not actually changes value.
So there will be less re-rendering.
Official docs are good enough but here is also fine article at Medium
in react when I get the offsetTop of the element in the componentDidMount is different than the offsetTop value of the same element when called in componentDidUpdate(). Why is that? I thought componentDidMount is called after render so the DOM elements are placed in the page so the offsetTop value should be correct.
class Index extends React.Component {
...
render() {
return (
<div className="site-wrapper">
<TopHeader />
<IntroSection />
</div>
);
}
}
class TopHeader extends React.Component {
...
componentDidMount() {
var rect = ReactDOM.findDOMNode(this).offsetTop;
console.log(rect);
}
}
class IntroSection extends React.Component {
...
componentDidMount() {
var rect = ReactDOM.findDOMNode(this).offsetTop;
console.log(rect);
}
}
componentDidMount is called once and only one . However, componentDidUpdate is called for each update that component received , even through state or props.
Then , we recommend to persist the offsetTop in the component state for both lifecycle methods .
I have built a React component which is suppose to call the function on window scroll event.
I would like to call the function, "getCards('default'), when the user scroll to 90% of the scroll height.
The component looks as shown in below:
class Home extends React.Component {
constructor(props) {
super(props);
this.handleScroll = this.handleScroll.bind(this);
}
componentDidMount() {
// test
this.props.getCards('default');
window.addEventListener('scroll', this.handleScroll);
}
handleScroll(event) {
console.log('scroll event');
}
Could anyone help me achieve this?
Thank you.
You have to work with your component, please refer to this code:
class Home extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
// test
this.props.getCards('default');
}
render() {
return (<div onScroll={this.handleScroll}>...</div>)
}
handleScroll(event) {
var heightBound = window.height * 0.8
if (heightBound > window.scrollY) {
// Probably you want to load new cards?
this.props.getCards(...);
}
}
The scroll event must be placed inside your component and binded using onScroll evt. The handler will check for percentage of used screen size and will load others elements if the lower bound limit is reached.
I hope this can help :)
I've got a similar problem and nothing with onScroll work to me.
constructor(props) {
super(props);
window.addEventListener('scroll', this.handleScroll, true);
}
handleScroll = (event) => {
// Your code
}
https://gist.github.com/koistya/934a4e452b61017ad611
React says we should not use refs where possible and I noticed that you can't use shallow rendering testing with refs so I have tried to remove refs where possible. I have a child component like this:
class Child extends React.Component {
play = () => {
//play the media
},
pause = () => {
//pause the media
},
setMedia = (newMedia) => {
//set the new media
}
}
I then have a parent component that needs to call these methods. For the setMedia I can just use props with the componentWillReceiveProps and call setMedia when the new props come in to the child.
With the play and pause functions I cannot do this.
Ben Alpert replied to this post and said:
In general, data should be passed down the tree via props. There are a few exceptions to this (such as calling .focus() or triggering a one-time animation that doesn't really "change" the state) but any time you're exposing a method called "set", props are usually a better choice. Try to make it so that the inner input component worries about its size and appearance so that none of its ancestors do.
Which is the best way to call a child function?
play() and pause() methods can be called from refs as they do not change the state just like focus() and use props for the other functions that have arguments.
Call the child functions by passing the method name in although this just seems hacky and a lot more complex:
class Child extends React.Component {
play = () => {
//play the media
},
pause = () => {
//pause the media
},
setMedia = (newMedia) => {
//set the new media
},
_callFunctions = (functions) => {
if (!functions.length) {
return;
}
//call each new function
functions.forEach((func) => this[func]());
//Empty the functions as they have been called
this.props.updateFunctions({functions: []});
}
componentWillReceiveProps(nextProps) {
this._callFunctions(nextProps.functions);
}
}
class Parent extends React.Component {
updateFunctions = (newFunctions) => this.setState({functions: newFunctions});
differentPlayMethod = () => {
//...Do other stuff
this.updateFunctions("play");
}
render() {
return (
<Child updateFunctions={this.updateFunctions}/>
);
}
}
Do this in the child component: this.props.updateFunctions({play: this.play});
The problem with this is that we are exposing(copying) a method to another component that shouldn't really know about it...
Which is the best way to do this?
I am using method number 2 at the moment and I don't really like it.
To override child functions I have also done something similar to above. Should I just use refs instead?
Rather than call child functions, try to pass data and functions down from the parent. Alongside your component, you can export a wrapper or higher order function that provides the necessary state / functions.
let withMedia = Wrapped => {
return class extends React.Component {
state = { playing: false }
play() { ... }
render() {
return (
<Wrapped
{...this.state}
{...this.props}
play={this.play}
/>
)
}
}
}
Then in your parent component:
import { Media, withMedia } from 'your-library'
let Parent = props =>
<div>
<button onClick={props.play}>Play</button>
<Media playing={props.playing} />
</div>
export default withMedia(Parent)
Keep the state as localized as you can, but don't spread it over multiple components. If you need the information whether it is currently playing in both the parent and the child, keep the state in the parent.
This leaves you with a much cleaner state tree and props:
class Child extends React.Component {
render() {
return (
<div>
<button onClick={this.props.togglePlay}>Child: Play/Pause</button>
<p>Playing: {this.props.playing ? 'Yes' : 'No'}</p>
</div>
);
}
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.togglePlay = this.togglePlay.bind(this);
this.state = {
playing: false
};
}
togglePlay() {
this.setState({
playing: !this.state.playing
});
}
render() {
return (
<div>
<button onClick={this.togglePlay}>Parent: Play/Pause</button>
<Child togglePlay={this.togglePlay} playing={this.state.playing} />
</div>
);
}
}
ReactDOM.render(
<Parent />,
document.getElementById('app')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id='app'></div>