I have a component a table with many rows (50-100). There is an ability to edit lines and type there text. All lines are stored in redux store. I update store on every keypress and it takes about 1-1.5sec before the typed character is appeared in the textarea. Every line is a separate component.
The approximate structure is bellow (but of course it's much more complex)
#connect(store => {
return {
phrases: store.phrases
};
})
export default class TableContainer extends React.Component {
handlePhraseUpdate = value => {
// update redux store here
};
render() {
return this.props.phrases.map((phrase, index) => {
return (
<Phrase
phrase={phrase}
onPhraseUpdate={this.handlePhraseUpdate}
/>
)
})
}
}
How can I refactor this code?
When dealing with large collections in Redux, it's best to factor you code so that the fewest components re-render when an item in the collection changes.
In your current implementation, when a Phrase is updated, the TableContainer receives a whole new array of phrases and will re-render all of them, even though only one has changed.
By connecting the Phrase component and looking up the individual phrase for each one will result in just the single row updating when the phrase is updated.
It is best to use an id of some kind on the values, for example:
#connect((store, ownProps) => {
return {
phrase: store.phrases[ownProps.id]
}
})
export default class Phrase extends React.Component {
handlePhraseUpdate = value => {
// update redux store here
}
render() {
const { phrase } = this.props
// render phrase
}
}
#connect(store => {
return {
// this could also be stored rather than calculated each time
phrases: store.phrases.map(p => p.id)
};
})
export default class TableContainer extends React.Component {
render() {
return this.props.phrases.map((id) => {
return (
<Phrase key={id} id={id} />
);
});
}
}
If you don't have an id you can use the index in the array:
#connect((store, ownProps) => {
return {
phrase: store.phrases[ownProps.index]
}
})
export default class Phrase extends React.Component {
handlePhraseUpdate = value => {
// update redux store here
}
render() {
const { phrase } = this.props
// render phrase
}
}
#connect(store => {
return {
phraseCount: store.phrases.length
};
})
export default class TableContainer extends React.Component {
render() {
const phrases = []
let index = 0
for (let index = 0; index < this.props.phraseCount; index++) {
phrases.push(<Phrase key={index} index={index} />)
}
return (
<div>
{ phrases }
</div>
)
}
}
Hope this helps.
Most likely your performance issues are not related to redux, but to ReactJS itself.
This is because whenever you update your global object, all your rows are re-rendered. You should re-render only the row that the user is updating.
You can have control over a component render, using the shouldComponentUpdate method.
For example, you want to update the component only if its internal state has changed.
Related
I'm new to React and have to work on a specific assignment where all the logic of my app is in a single parent component and the child component only receives a few props. However, in the child there shouldn't be any logic or almost no logic.
I have a grid (parent component) made of 25 cell and each cell (child component ) can be either on or off. Imagine each cell as a light which is on or off.
From my parent component I'm rendering the cell component 25 times. Every time each cell has:
a key
an id
a status (on or off randomly assigned)
a click event
In my child component when the click event is triggered, the child component return to the parent its id and its status(on or off)
What I want to achieve:
In my parent component I want to be able to detect which child has been clicked and only change the status of the clicked child.
What I get so far:
Despite the parent receive the id and the status of the child that has been clicked, when I change the state via setState, all the children are affected.
Here is a snippet of my parent component:
import React, { Component } from 'react';
import GridSquare from './GridSquare';
import { randomlyLit, cellIdentifier } from '../helpers/helperFuns.js';
let nums = []
cellIdentifier(nums, 25)
class GridContainer extends Component {
static defaultProps = {
gridSize: 25
};
constructor(props) {
super();
this.state = {
cellID: nums,
hasWon: false,
lightStatus: Array.from({ length: 25 }, () => randomlyLit()),
};
this.changeValue = this.changeValue.bind(this);
}
changeValue(id, value) {
console.log(id, value);
this.setState(st => ({
// let result = st.cellID.filter(c => c===id)
// if(result){
// st.value = !value;
// }
lightStatus : !value
})
)
}
render() {
return (
<div>
<h1 className="neon">
Light <span className="flux">Out</span>
</h1>
<div className="GridContainer">
{this.state.cellID.map((el, i) =>(
<GridSquare key={this.state.cellID[i]} id={this.state.cellID[i]} lit={this.state.lightStatus[i]} click={this.changeValue}/>
))}
</div>
</div>
);
}
}
export default GridContainer;
Here is a snippet of my child component:
import React, {Component} from 'react';
class GridSquare extends Component {
constructor(props){
super(props);
this.handleClick= this.handleClick.bind(this)
}
handleClick(){
this.props.click( this.props.id, this.props.lit);
}
render() {
const squareClasses = {
'GridSquareOn': this.props.lit === true,
'GridSquareOff': this.props.lit === false,
'GridSquare': true,
}
function classNames(squareClasses) {
return Object.entries(squareClasses)
.filter(([key, value]) => value)
.map(([key, value]) => key)
.join(' ');
}
const myClassName = classNames(squareClasses)
return(
<div className={myClassName} onClick={this.handleClick}>
</div>
)
}
}
export default GridSquare;
My app.js only renders the parent component and nothing else:
import GridContainer from './components/GridContainer.jsx'
import './style/App.css';
import './style/GridContainer.css';
import './style/GridSquare.css';
function App() {
return (
<div className="App">
<GridContainer />
</div>
);
}
export default App;
Thank you in advance for any help!
changeValue(id, value) {
console.log(id, value);
this.setState(st => ({
// let result = st.cellID.filter(c => c===id)
// if(result){
// st.value = !value;
// }
lightStatus : !value
})
Your change value function is updating the state with the wrong value.
Initial value of your lightStatus is an array of booleans ( assuming randomlyLit function will return a boolean.
When you click on one of the cells, the lightStatus gets updated with the value of false instead of an array.
To fix this, search through the entire lightStatus array by index for which the cell was clicked and update the boolean at that particular index using Array.slice.
How to Optimise
Instead of traversing the whole array every time to update the lightStatus of the cell. You can save the the value in an Object.
What if I could update the status in changeValue like this ?
this.setState((st) => {
return {
...st,
lightStatus: { ...st.lightStatus, [id]: value } // Direct update without traversing through array
}
});
lightStatus can be used to form a "mapping" between cell ids in cellID and their corresponding status booleans.
Prefacing this with a thought; I think I might require a recursive component but that's beyond my current ability with native js and React so I feel like I have Swiss cheese understanding of React at this point.
The problem:
I have an array of metafields containing metafield objects with the following structure:
{
metafields: [
{ 0:
{ namespace: "namespaceVal",
key: "keyVal",
val: [
0: "val1",
1: "val2",
2: "val3"
]
}
},
...
]
}
My code maps metafields into Cards and within each card lives a component <MetafieldInput metafields={metafields['value']} /> and within that component the value array gets mapped to input fields. Overall it looks like:
// App
render() {
const metafields = this.state.metafields;
return (
{metafields.map(metafield) => (
<MetafieldInputs metafields={metafield['value']} />
)}
)
}
//MetafieldInputs
this.state = { metafields: this.props.metafields}
render() {
const metafields = this.state;
return (
{metafields.map((meta, i) => (
<TextField
value={meta}
changeKey={meta}
onChange={(val) => {
this.setState(prevState => {
return { metafields: prevState.metafields.map((field, j) => {
if(j === i) { field = val; }
return field;
})};
});
}}
/>
))}
)
}
Up to this point everything displays correctly and I can change the inputs! However the change happens one at a time, as in I hit a key then I have to click back into the input to add another character. It seems like everything gets re-rendered which is why I have to click back into the input to make another change.
Am I able to use components in this way? It feels like I'm working my way into nesting components but everything I've read says not to nest components. Am I overcomplicating this issue? The only solution I have is to rip out the React portion and take it to pure javascript.
guidance would be much appreciated!
My suggestion is that to out source the onChange handler, and the code can be understood a little bit more easier.
Mainly React does not update state right after setState() is called, it does a batch job. Therefore it can happen that several setState calls are accessing one reference point. If you directly mutate the state, it can cause chaos as other state can use the updated state while doing the batch job.
Also, if you out source onChange handler in the App level, you can change MetafieldInputs into a functional component rather than a class-bases component. Functional based component costs less than class based component and can boost the performance.
Below are updated code, tested. I assume you use Material UI's TextField, but onChangeHandler should also work in your own component.
// Full App.js
import React, { Component } from 'react';
import MetafieldInputs from './MetafieldInputs';
class App extends Component {
state = {
metafields: [
{
metafield:
{
namespace: "namespaceVal",
key: "keyVal",
val: [
{ '0': "val1" },
{ '1': "val2" },
{ '2': "val3" }
]
}
},
]
}
// will never be triggered as from React point of view, the state never changes
componentDidUpdate() {
console.log('componentDidUpdate')
}
render() {
const metafields = this.state.metafields;
const metafieldsKeys = Object.keys(metafields);
const renderInputs = metafieldsKeys.map(key => {
const metafield = metafields[key];
return <MetafieldInputs metafields={metafield.metafield.val} key={metafield.metafield.key} />;
})
return (
<div>
{renderInputs}
</div>
)
}
}
export default App;
// full MetafieldInputs
import React, { Component } from 'react'
import TextField from '#material-ui/core/TextField';
class MetafieldInputs extends Component {
state = {
metafields: this.props.metafields
}
onChangeHandler = (e, index) => {
const value = e.target.value;
this.setState(prevState => {
const updateMetafields = [...prevState.metafields];
const updatedFields = { ...updateMetafields[index] }
updatedFields[index] = value
updateMetafields[index] = updatedFields;
return { metafields: updateMetafields }
})
}
render() {
const { metafields } = this.state;
// will always remain the same
console.log('this.props', this.props)
return (
<div>
{metafields.map((meta, i) => {
return (
<TextField
value={meta[i]}
changekey={meta}
onChange={(e) => this.onChangeHandler(e, i)}
// generally it is not a good idea to use index as a key.
key={i}
/>
)
}
)}
</div>
)
}
}
export default MetafieldInputs
Again, IF you out source the onChangeHandler to App class, MetafieldInputs can be a pure functional component, and all the state management can be done in the App class.
On the other hand, if you want to keep a pure and clean App class, you can also store metafields into MetafieldInputs class in case you might need some other logic in your application.
For instance, your application renders more components than the example does, and MetafieldInputs should not be rendered until something happened. If you fetch data from server end, it is better to fetch the data when it is needed rather than fetching all the data in the App component.
You need to do the onChange at the app level. You should just pass the onChange function into MetaFieldsInput and always use this.props.metafields when rendering
Problem
I've parent class which contains list of items and renders component for each item of the list. When some item has been changed (even only one), all items in the list are being rerendered.
So I've tried to implement shouldComponentUpdate(). I am using console.log() to see if it is called but I can't see the log. I've found question shouldComponentUpdate is not never called and tried to return return (JSON.stringify(this.props) !=JSON.stringify(nextProps)); but component still renders itself again. So I've tried just to return false (like do not ever update) but it still does. As the last try I've used PureComponent but it is still being rerendered.
Question
How can I stop children re-rendering if the parent list changes and why is ShouldComponentUpdate never called?
Edit
I've noticed something what I didn't mention in question, I'm sorry for that. I am using context. If I don't use context -> it's ok. Is there any chance to stop re-render while using context? (I'm not using context on updated item - values of context didn't change).
Example
I've parent class which iterates list and renders TaskPreview component for each item of list:
class Dashboard extends React.Component
{
constructor(props) {
this.state = {
tasks: {},
};
}
onTaskUpdate=(task)=>
this.setState(prevState => ({
tasks: {...prevState.tasks, [task._id]: task}
}));
// ... some code
render() {
return (
<div>
{(!Object.entries(this.props.tasks).length)
? null
: this.props.tasks.map((task,index) =>
<TaskPreview key={task._id} task={task} onChange={this.onTaskUpdate}/>
})}
</div>
)
}
}
and I've children TaskPreview class:
class TaskPreview extends React.Component
{
shouldComponentUpdate(nextProps) {
console.log('This log is never shown in console');
return false; // just never!
}
render() {
console.log('task rendered:',this.props.task._id); // indicates rerender
return(<div>Something from props</div>);
}
}
TaskPreview.contextType = TasksContext;
export default TaskPreview;
As #Nicolae Maties suggested I've tried to use Object.keys for iteration instead of direct map but it still doesn't call "shouldComponentUpdate" and still being re-rendered even if there is no changes.
Updated code:
render() {
return (
<div>
{(!Object.entries(this.props.tasks).length)
? null
: Object.keys(this.props.tasks).map((key,index) => {
let task = this.props.tasks[key];
<TaskPreview key={task._id} task={task}/>
}
})}
</div>
)
}
Component is being re-rendered because of .contextType.
TaskPreview.contextType = TasksContext;
Also as is mentioned in documentation:
The propagation from Provider to its descendant consumers (including .contextType and useContext) is not subject to the shouldComponentUpdate method, so the consumer is updated even when an ancestor component skips an update. Source: reactjs.org/docs/context
You have to use context somehow else or do not use it at all.
You can use Context.Consumer which won't force re-render of current component but it might force re-render of its children.
<TasksContext.Consumer>
{value => /* render something based on the context value */}
</TasksContext.Consumer>
Instead of return (JSON.stringify(this.props) != JSON.stringify(nextProps)); in your shouldComponentUpdate() life cycle, try specifying tasks object like this return (JSON.stringify(this.props.tasks) != JSON.stringify(nextProps.tasks));
Maybe react is creating new instances of your component and replaces the old instances with them. That's why you're probably not getting your lifecycle method invoked. That can happen if the key property you're assigning in the map always changes.
use from pureComponent and array as state:
class Dashboard extends React.PureComponent
{
constructor(props) {
this.state = {
tasks: this.props.tasks
}
}
onTaskUpdate=(task)=>
this.setState(prevState => ({
tasks: [...prevState.tasks, task] // render only new task
}));
render() {
const {tasks} = this.state
return (
<div>
{tasks.map(task => <TaskPreview key={task._id} task={task} />)}
</div>
)
}
}
class TaskPreview extends React.PureComponent
{
render() {
console.log('task rendered:',this.props.task._id); // indicates rerender
return(<div>Something from props</div>);
}
}
In the shouldComponentUpdate() method of your TaskPreview component, you should check if the next props have changes in comparison to the current props. Then if there are changes, return true to update the component, otherwise false.
The following example compares all the fields of props object with the new props object. But you can only check the props you are interested in.
shouldComponentUpdate(nextProps) {
return !!(Object.keys(nextProps).find(key => nextProps[key] !== this.props[key]));
}
I tried with below code snippet, shouldComponentUpdate worked as I expected. Could you share your Dashboard initial props ?
class Dashboard extends React.Component {
constructor(props) {
this.state = {
tasks: {}
};
}
onTaskUpdate = task =>
this.setState(prevState => ({
tasks: { ...prevState.tasks, [task._id]: task }
}));
// ... some code
render() {
return (
<div>
{!Object.entries(this.props.tasks).length
? null
: Object.keys(this.props.tasks).map((key, index) => {
let task = this.props.tasks[key];
return (
<TaskPreview
key={task._id}
task={task}
onChange={this.onTaskUpdate}
/>
);
})}
</div>
);
}
}
class TaskPreview extends React.Component {
shouldComponentUpdate(nextProps) {
console.log("This log is never shown in console");
return nextProps.task._id != this.props.task._id;
}
render() {
console.log("task rendered:", this.props.task); // indicates rerender
return (
<button onClick={() => this.props.onChange(this.props.task)}>
Something from props
</button>
);
}
}
my initial props for Dashboard component is :
<Dashboard tasks={{test:{_id:'myId', description:'some description'}}}/>
Newbie question.
I have an array that i need to add to and I am using slice to do this. I am using gatsby/react. The problem I have is each time my page/component rerenders the object I am adding to my array gets added again
Here is my code
class IndexPage extends PureComponent {
render() {
const data = this.props.data;
const hostels = data.featuredHostel.edges;
const hopimage = data.hop.childImageSharp.fluid;
hostels.splice(8, 0, {
node: {
featuredImage: {
alt: 'Bedhopper Image',
fluid: hopimage
},
id: 'bedhopper',
slug: '/deals/bed-hopper',
title: 'For travel adicts who want to stay everywhere'
}
});
return (....
Been stuck on this for a while now. Any help appreciated
You should make any calculation on constructor or componentDidMount.
class IndexPage extends PureComponent {
constructor(props) {
super(props);
this.state = {
hostels: props.data.featuredHostel.edges.concat(...)
}
}
componentDidMount() {
}
render() {
const { hostels } = this.state;
return (
...
)
Probably, your case can works too (I didn't see whole code). I guess you use array index as key for render
hostels.map((hostel, hostelIndex) => (<SomeComponent key={hostelIndex} />))
You can change key to hostel.id for example for more unique block.
I have a React component with a prop 'total' that changes every time the component is updated:
function MyView(props) {
const total = props.data.loading ? 0 : props.data.total;
return (
<p> total </p>
);
}
The first time the component mounts the total is say 10. Every time the component is updated because of a prop change the total goes up.
Is there a way I can display the original total (in this example 10)?
I have tried setting it in this.total inside componentDidMount, but props.data.total is not yet available when componentDidMount is called. Same with the constructor. The total only becomes available when props.data.loading is false.
In order to get access to lifecycle features, you must move from function, stateless component, to a class component.
in the below example, InitialTotal is initialized in the construstor lifecycle method and it never changes.
currentTotal, is incremented each time the render function is called - when the component is re-rendered (because of props change or state changes)
it should look something like that:
class MyView extends React.Component {
constructor(props) {
super(props)
this.initialTotal = 10;
this.currentTotal = 10;
}
render() {
this.currentTotal+=1;
return (
<p>InitialToal: {this.initialTotal}</p>
<p>Current Total: {this.currentTotal}</p>
);
}
}
You could create a stateful component and store the initial total in the component state.
Example
class MyView extends React.Component {
state = {
initialTotal: this.props.total
};
render() {
const { total } = this.props;
const { initialTotal } = this.state;
return (
<div>
<p> Total: {total} </p>
<p> Initial total: {initialTotal} </p>
</div>
);
}
}
class App extends React.Component {
state = {
total: 10
};
componentDidMount() {
this.interval = setInterval(() => {
this.setState(({ total }) => {
return { total: total + 1 };
});
}, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return <MyView total={this.state.total} />;
}
}
If I understand your requirements correctly...
function MyView(props) {
// if you only need to set the value once on load just use useState.
// the const total will be the value you pass in to useState.
const [total, setTotal] = useState(props.data.loading ? 0 : props.data.total)
// if its possible that the value is not available on initial load and
// you want to set it only once, when it becomes available, you can use
// useEffect with useState
useEffect(() => {
// some condition to know if data is ready to set
if (!props.data.loading) {
setTotal(props.data.total)
}
}, [props.data.total, setTotal, props.data.loading]
// this array allows you to limit useEffect to only be called when
// one of these values change. ( when total changes in this case,
// as the const function setTotal will not change
// ( but react will fuss if its used and not in the list ).
return (
<p> {total} </p>
);
}
I have the same need. With a functional component, I need to store the inital snapshot of states, let user play with different state values and see their results immediately, eventually, they can just cancel and go back to the initial states. Apply the same structure to your problem, this is how it looks:
import React from 'react';
import { useEffect, useState } from "react";
const TestView = (props: { data: any }) => {
// setting default will help type issues if TS is used
const [initialTotal, setInitialTotal] = useState(props.data.total)
useEffect(() => {
// some condition to know if data is ready to set
setInitialTotal(props.data.total);
// Critical: use empty array to ensure this useEffect is called only once.
}, [])
return (
<div>
<p> { initialTotal } </p>
<p> { props.data.total } </p>
</div>
);
}
export default TestView
You can use getDerivedStateFromProps life cycle method.
static getDerivedStateFromProps(props, state){
if(props.data.total && (props.data.total==10)){
return {
total : props.total // show total only when its 10
}
}else{
return null; // does not update state
}
}