I am building a gallery app where I need to create multiple HTTP requests to pull gallery entries(images & videos).
As gallery will be auto scrolling entries, I am trying to prevent re-rendering component when I make subsequent HTTP requests and update the state.
Thanks
Here's an example of only re-rendering when a particular condition is fulfilled (e.g. finished fetching).
For example, here we only re-render if the value reaches 3.
import React, { Component } from 'react';
import { render } from 'react-dom';
class App extends React.Component {
state = {
value: 0,
}
add = () => {
this.setState({ value: this.state.value + 1});
}
shouldComponentUpdate(nextProps, nextState) {
if (nextState.value !== 3) {
return false;
}
return true;
}
render() {
return (
<React.Fragment>
<p>Value is: {this.state.value}</p>
<button onClick={this.add}>add</button>
</React.Fragment>
)
}
}
render(<App />, document.getElementById('root'));
Live example here.
All data types
useState returns a pair - an array with two elements. The first element is the current value and the second is a function that allows us to update it. If we update the current value, then no rendering is called. If we use a function, then the rendering is called.
const stateVariable = React.useState("value");
stateVariable[0]="newValue"; //update without rendering
stateVariable[1]("newValue");//update with rendering
Object
If a state variable is declared as an object, then we can change its first element. In this case, rendering is not called.
const [myVariable, setMyVariable] = React.useState({ key1: "value" });
myVariable.key1 = "newValue"; //update without rendering
setMyVariable({ key1:"newValue"}); //update with rendering
Array
If a state variable is declared as an array, then we can change its first element. In this case, rendering is not called.
const [myVariable, setMyVariable] = React.useState(["value"]);
myVariable[0] = "newValue"; //update without rendering
setMyVariable(["newValue"]); //update with rendering
None of the answers work for TypeScript, so I'll add this. One method is to instead use the useRef hook and edit the value directly by accessing the 'current' property. See here:
const [myState, setMyState] = useState<string>("");
becomes
let myState = useRef<string>("");
and you can access it via:
myState.current = "foobar";
So far I'm not seeing any drawbacks. However, if this is to prevent a child component from updating, you should consider using the useMemo hook instead for readability. The useMemo hook is essentially a component that's given an explicit dependency array.
It's as easy as using this.state.stateName = value. This will change the state without re-rendering, unlike using this.setState({stateName:value}), which will re-render. For example;
class Button extends React.Component {
constructor( props ){
super(props);
this.state = {
message:"Hello World!"
};
this.method = this.method.bind(this);
}
method(e){
e.preventDefault();
this.state.message = "This message would be stored but not rendered";
}
render() {
return (
<div >
{this.state.message}
<form onSubmit={this.method}>
<button type="submit">change state</button>
</form>
</div>
)
}
}
ReactDOM.render(<Button />, document.getElementById('myDiv'));
If you just need a container to store the values, try useRef. Changing the value of ref.current doesn't lead to re-rendering.
const [ loading,setLoading] = useState(false)
loading=true //does not rerender
setLoading(true) //will rerender
In functional component refer above code, for class use componentShouldUpdate lifecycle
Related
I am facing issue while using useState hook with array. I checked various resources on stackoverflow, but could not fix it.
my basic code snippet looks like :
const [users, setUsers] = useState([]);
function addNewContact(user) {
const newUsers = [...users,user];
console.log(newUsers);
setUsers(newUsers);
}
<CardContainer users={users}></CardContainer>
class CardContainer extends Component {
constructor(props) {
super(props);
console.log("this -> ");
console.log(this.props.users);
this.state = {
users: this.props.users
}
}
render() {
//console.log(this.state.users)
return (
<div class="row row-cols-1 row-cols-md-2 g-4">
{
this.state.users.map(user => {
return <Card id={user.phone} title={user.name} email={user.email} phone={user.phone}></Card>
})
}
</div>
)
}
}
export default CardContainer;
I am able to see updated array in the console, but the component using it is not rendering again. Can anyone please help me on this.
The issue is due to you're storing the prop in the state of the child component, which is assigned on component initialization and component initialization/constructor only run one, until its remounted. After that, whenever, the state changes in the parent component, the child component is not re-rendering, because it uses its own state for map.
This below code only runs once on the component initialization.
this.state = {
users: this.props.users
}
In the child component, you can directly use the props and the child component will always re-render on change in the parent component. Instead of this.state.users.map you can directly map the array from props like this this.props.users.map. This way,the component will re-render on state change in the parent compoenent.
As #Junaid said, constructor is only called once before component mounting. If you really need to set a separate state inside the child component, then you can use componentDidUpdate(prevProps) react life cycle method. Make sure to compare previous and current props in order to avoid infinite loop of re-rendering.
componentDidUpdate(prevProps) {
if (this.props.users !== prevProps.users) {
this.setState({ users: this.props.users });
}
};
I'm trying out using React Hooks where in a component that I previously have as class based. The component looks like this:
class A extends Component {
constructor(props) {
super(props)
this.mapRef = createRef()
this.map = new Map({ ... })
}
componentDidMount() {
this.map.setTarget(this.mapRef.current)
}
// Also shouldComponentUpdate & componentWillUnmount
render() {
return (
<div>
<div ref={this.mapRef}></div>
{this.props.children({map: this.map})}
</div>
)
}
}
My understanding of the open-layers library is that when I create an instance of the Map object, I need to pass it a reference to a DOM element since the library requires direct control over a DOM element. I'm doing this in the componentDidMount function via a ref.
Trying to change this code to React Hooks (out of interest), I have tried this:
function A (props) {
var map
const mapRef = useRef(null)
useEffect(() => {
map = new Map()
map.setTarget(mapRef.current)
})
return (
<div>
<div ref={mapRef}></div>
{props.children({map})}
</div>
)
}
Which just errors (because the props.children function gets null for the map object). I tried moving the map object initialization out of the function, which seems to work:
const map = new Map({ ... })
function A (props) {
const mapRef = useRef(null)
useEffect(() => {
map.setTarget(mapRef.current)
// Then adjust the map however necessary depending on props
})
return (
<div>
<div ref={mapRef}></div>
{props.children({map})}
</div>
)
}
This somewhat works... although it seems that the useEffect callback fires far more often then necessary. And I have to figure out how to implement shouldComponentUpdate.
Is this the 'correct' approach to using React Hooks? I can't help feeling that in this case either a class component makes a lot more sense, or I'm not using Hooks correctly (probably the latter).
In this case I'm not actually using a class component for state at all, but rather for the ability to use lifecycle methods to update the map instance due to DOM changes.
This was a useful question for me when I was trying to create my own Map component with OpenLayers. I used a slightly different approach:
The olMap.setTarget method accepts either an HTML element or the ID of one. So I construct the initial OlMap object and give the target key a value of undefined. The target is then set in a useEffect to the div's id.
To make sure this effect only runs when the component mounts and not on each render, an empty array is passed as the second parameter to useEffect. The effect returns a function that sets the target back to undefined when the component unmounts. This part is necessary if the map is in a component that is only rendered on certain routes. The map will not re-render if you navigate away and then back if you don't set the target again.
import React, { useEffect } from 'react';
import { Map, View } from 'ol';
const olMap = new Map({
target: undefined,
layers: [
new TileLayer({
source: new OSM()
})
],
view: new View({
center: [-6005420.749222653, 6000508.181331601],
zoom: 9
})
});
export default function OlMap() {
useEffect(() => {
olMap.setTarget('map')
return () => olMap.setTarget(undefined);
}, []);
return (
<div id='map'>
</div>
)
}
If you are using a library that needs a HTMLElement you can use useRef and ref instead:
export default function OlMap() {
let mapDiv = useRef(null);
useEffect(() => {
olMap.setTarget(mapDiv.current)
return () => olMap.setTarget(undefined);
}, []);
return (
<div id='map' ref={mapDiv}>
</div>
)
}
I've left the second parameter as an empty array again, but you could pass mapDiv in that array if you wanted to call the effect again should the ref ever change. I don't think this is needed for OpenLayers, but another library might make changes other than just appending to the target HTMLElement.
If you want the useEffect hook to fire only when needed you can put an array with the properties that would trigger the hook as a second argument.
useEffect(() => {map.setTarget(...)}, [mapRef.current])
In my react project, the componentWillReceiveProps() function seems to be called twice, but not sure what the problem is.
Here is the code.
import React, { Component, PropTypes } from 'react';
...
class MessagesList extends Component {
constructor(props) {
super(props);
this.state = {
message: '',
messages: []
};
...
componentWillMount() {
this.props.init_message();
};
componentWillReceiveProps(nextProps) {
this.setState({
messages: this.props.user.messages
});
var msgs = this.props.user.messages;
var total_group = [];
var msg_group = [];
var group_date = 0;
setTimeout(() => {
if (typeof msgs != 'undefined') {
for(var i = 0; i < msgs.length; i++) {
...
}
}
}, 100);
};
render() {
return (
<div className="row">
<div className="col-12">
<div className="messages">
{this.state.messages.map(message => {
return (
<div>{message.user}: {message.message} : {message.date}</div>
)
})}
</div>
</div>
...
</div>
);
}
}
I was going to read the msgs.length in the componentWillReceiveProps(), I got the following issue.
msgs.length is undefiend
After that I got the values of array, so I think the componentWillReceiveProps() seems to be called twice. So in the first call, can't read the value and then in the second call, read the value at least.
Please help me.
componentWillReceiveProps is invoked before a mounted component receives new props. If you need to update the state in response to prop changes (for example, to reset it), you may compare this.props and nextProps and perform state transitions using this.setState() in this method.
Note that if a parent component causes your component to re-render, this method will be called even if props have not changed. Make sure to compare the current and next values if you only want to handle changes.
You will get the details from react docs.
ComponentWillReceiveProps is a method from the growth/update phase of the React lifecycle.
The Growth phase is triggered in three different ways: changing of props, changing of state or calling forceUpdate().
The value you are referring to in componentWillReceiveProps, this.props.user.messages, is the current value not the nextProps value.
Also something to consider is that the setState method is actually an asynchronous function. So when that setting of state takes place, it will cause another rerender.
I suspect, but I cannot be sure without more of your code, that setState is called once with your original value from props which triggers another update cycle. During this next update cycle the setState method now sets state to the new prop values.
Are you perhaps meaning to use nextProps.user.messages instead of this.props.user.messages?
I think this question has been answer several time but I can't find my specific case.
https://codesandbox.io/s/jjy9l3003
So basically I have an App component that trigger an action that change a state call "isSmall" to true if the screen is resized and less than 500px (and false if it is higher)
class App extends React.Component {
...
resizeHandeler(e) {
const { window, dispatch } = this.props;
if (window.innerWidth < 500 && !this.state.isSmall) {
dispatch(isSmallAction(true));
this.setState({ isSmall: true });
} else if (window.innerWidth >= 500 && this.state.isSmall) {
dispatch(isSmallAction(false));
console.log(isSmallAction(false));
this.setState({ isSmall: false })
}
};
componentDidMount() {
const { window } = this.props;
window.addEventListener('resize', this.resizeHandeler.bind(this));
}
...
I have an other component called HeaderContainer who is a child of App and connected to the Store and the state "isSmall", I want this component to rerender when the "isSmall" change state... but it is not
class Header extends React.Component {
constructor(props) {
super(props);
this.isSmall = props.isSmall;
this.isHome = props.isHome;
}
...
render() {
return (
<div>
{
this.isSmall
?
(<div>Is small</div>)
:
(<div>is BIG</div>)
}
</div>
);
}
...
even if I can see through the console that redux is actually updating the store the Header component is not re-rendering.
Can someone point out what I am missing ?
Am I misunderstanding the "connect()" redux-react function ?
Looking at your code on the link you posted your component is connected to the redux store via connect
const mapStateToProps = (state, ownProps) => {
return {
isHome: ownProps.isHome,
isSmall: state.get('isSmall')
}
}
export const HeaderContainer = connect(mapStateToProps)(Header);
That means that the props you are accessing in your mapStateToProps function (isHome and isSmall) are taken from the redux store and passed as props into your components.
To have React re-render your component you have to use 'this.props' inside the render function (as render is called every time a prop change):
render() {
return (
<div>
{
this.props.isSmall
?
(<div>Is small</div>)
:
(<div>is BIG</div>)
}
</div>
);
}
You are doing it well in the constructor but the constructor is only called once before the component is mounted. You should have a look at react lifecycle methods: https://reactjs.org/docs/react-component.html#constructor
You could remove entirely the constructor in your Header.js file.
You should also avoid using public class properties (e.g. this.isSmall = props.isSmall; ) in react when possible and make use of the React local state when your component needs it: https://reactjs.org/docs/state-and-lifecycle.html#adding-local-state-to-a-class
A component is only mounted once and then only being updated by getting passed new props. You constructor is therefore only being called once before mount. That means that the instance properties you set there will never change during the lifetime of your mounted component. You have to directly Access this.props in your render() function to make updating work. You can remove the constructor as he doesn't do anything useful in this case.
I am trying to force a child component to re-render. I have tried this.forceUpdate();, but it does not work. I put console.log statements in my <PostList /> component, and none of them are ever called--not componentDidMount, nor componentWillMount, componentWillReceiveProps, none of them. It's as if the <PostList /> component is never initialized. I am sure it is though, because I know for a fact items.count retrieves my items. Here is my render method:
render() {
const items = this.state.posts;
const postList = items.count > 0 ? (<PostList comingFromSearch={true} xyz={items} />) : (<div></div>)
const navBar = <NavigationBar />
return (
<div><br/>{navBar}
<div className="container">
<h3>Search Results for {this.state.searchTerm}</h3>
<div className="row">
<div className="col-x-12">{postList}</div>
</div>
</div>
</div>
)
}
And here is my api call:
retrieveSearch(term) {
Helpers.searchWithTerm(term).then((terms) => {
const postsWithTermsInTitle = terms.titleResults
this.setState({posts: postsWithTermsInTitle})
this.forceUpdate();
}).catch((error) => {
console.log("error searching: " + error);
})
}
I should note, on my previous page, i had another ` component, and maybe react is using that one instead of this one? I want to force it to use this instance.
If this.forceUpdate(); does not make the whole DOM re-render, how can I do that?
thanks
your PostList and NavigationBar Components might not update because they only update when their props are changed (shallow compare).
PostList might not update when changing the inner content of the array, because the component will shallow compare the new state with the previous one. Shallow comparing an array will basically checked against its length property. which does not change in this case.
Quick Solution
Sometimes you need to update a List, without changing any of its props or the length of the list. To achieve this, just pass a prop to the component and keep incrementing it instead of calling force update.
retrieveSearch(term) {
Helpers.searchWithTerm(term).then((terms) => {
const postsWithTermsInTitle = terms.titleResults
this.setState((curState) => ({posts: postsWithTermsInTitle, refreshCycle: curState.refreshCycle+1}))
this.forceUpdate();
}).catch((error) => {
console.log("error searching: " + error);
})
}
render() {
...
<PostList
...
refreshCycle={this.state.refreshCycle}
/>
...
}
Right solution
The right solution is to provide an itemRenderer which you is a function that returns the an individual item from the list. This function is passed as a prop to the component.
This way you have control over how the items inside the list will appear, also changes inside the itemRenderer function will cause a component update.
itemRenderer(itemIndex) {
return <div>{this.props.item[itemIndex]}</div>;
}
render() {
...
<PostList
itemRenderer={this.itemRenderer.bind(this)}
itemsLength={items.length}
/>
...
}
The itemRenderer will be called inside the PostList in a loop (of length itemsLength). each loop will be passed the index of the current iteration, so you can know which item of the list to return from the function.
This way you can also make your list more scalable and more accommodating.
You can check an implementation of such solution on a list package like this one: https://www.npmjs.com/package/react-list
You can force a re-render of a component and all its children by changing the state of the component. In the constructor add a state object:
constructor(props) {
super(props)
this.state = {
someComponentState: 'someValue'
}
}
Now whenever you do:
this.setState(someComponentState, 'newValue')
It will re-render the component and all its children.
This of course assumes your component is a class based component, not a functional component. However, if your component is a functional component you can easily transform it to a class based component as follows:
class ComponentName {
constructor() {
// constructor code
}
render() {
// render code
}
}
export default ComponentName
Understand that componenet level state is not the same as redux state but is exposed only inside the component itself.