I have two component
Read Mode and Pagination
Read Mode Component
state = {
currentPdf:[],
currentPage: null,
totalPages: null,
intialState:1,
};
constructor(props) {
super(props);
this.onChangePage = this.onChangePage.bind(this);
this.onCurrentPageNo = this.onCurrentPageNo.bind(this);
this.onTotalPage = this.onTotalPage.bind(this);
}
componentDidMount() {
this.props.fetchPdfContent();
}
onCurrentPageNo(currentPageNo){
this.setState({ currentPage: currentPageNo });
}
onChangePage(currentPdf) {
this.setState({ currentPdf: currentPdf });
}
onTotalPage(totalpages){
this.setState({ totalPages: totalpages });
}
gotoPrevPage = (currentTarget) => {
if (this.state.currentPage > 1) {
let prevPage = this.state.currentPage - 1;
this.setState({ intialState: prevPage });
}
}
render() {
return (
<button className="btn btn-primary prev-btn pageBtn" onClick={this.gotoPrevPage.bind(this)}>
<span aria-hidden="true" className="icon-ico_arrow-right icomoon"> Left </span>
</button>
<Pagination initialPage={intialState} items={pdfcontnet} onTotalPage={this.onTotalPage} onChangePage={this.onChangePage} onChangePageNo={this.onCurrentPageNo} />
)
}
Pagination Component
constructor(props) {
super(props);
this.state = { pager: {} };
}
componentDidMount() {
// set page if items array isn't empty
if (this.props.items && this.props.items.length) {
this.setPage(this.props.initialPage);
}
}
componentDidUpdate(prevProps, prevState) {
// reset page if items array has changed
this.setPage(this.props.initialPage);
}
setPage(page) {
console.log(page + 'pages');
var items = this.props.items;
var pager = this.state.pager;
if (page < 1 || page > pager.totalPages) {
return;
}
// get new pager object for specified page
pager = this.getPager(items.length, page);
// get new page of items from items array
var pageOfItems = items.slice(pager.startIndex, pager.endIndex + 1);
// update state
this.setState({ pager: pager });
// call change page function in parent component
this.props.onChangePage(pageOfItems);
}
when i click the gotoPrevPage () initalState value need to pass
this.setPage(this.props.initialPage);
if i assign componentDidUpdate() state I got
You need to change initialPage={intialState} to initialPage={this.state.intialState} in your Read Mode Component render method.
PS: You should actually spell it initial, not intial.
try this:
onClick={()=>this.gotoPrevPage.bind(this)}
Your state object should be inside your constructor function in ReadMode. You should also be using this.setState({}) to update state. You also shouldn't be trying to reload the entire DOM when a new item is added the way you are in the Pagination component via commponentDidUpdate. React uses a virtual DOM and doesn't need to reload every element on the page every time one element is updated. It looks like part of your problem is your App continuously updates and you're getting stuck in a never ending loop updating the DOM.
Also, you might not need Pagination to have local state. You could store the state/data in the parent container and just pass it to Pagination via props. React uses one-way data flow and you should familiarize yourself with passing props from a parent component to a child component.
Brush up on state and lifecycle functions by reading the documentation, get an understanding of how React uses the virutal DOM to update elements, and rewrite your App so that state is stored mainly in the parent component and is passed via props to the child component.
Overlooking basic React concepts and structuring your project in a less than ideal way is the source of your problems.
Related
How can I convert this hook-based code to class-based code? Does the code still work?
I'm using a react/ASP.net Core template. My project consists of several components that are siblings.
I'm trying to send a state from a component to another one.
import { useState } from "react";
//the change is reflected in the ImageEditor component
const ImageEditor = ({ yourState }) => (
<p>State in the ImageEditor = {yourState}</p>
);
//ImageTile changes the state through the setYourState method
const ImageTile = ({ yourState, setYourState }) => (
<button onClick={() => setYourState("World!")}>
Change State from the ImageTile
</button>
);
//App is the parent component and contains both image editor and tile
const App = () => {
//the state which holds the image ID is declared in the parent
const [imageId, setImageId] = useState("Hello");
return (
<div>
<ImageTile yourState={imageId} setYourState={setImageId} />
<ImageEditor yourState={imageId} />
</div>
);
};
export default App;
You can see the complete code on:
https://codesandbox.io/s/billowing-brook-9y9y5?file=/src/App.js:0-775
A parent passes it’s state to a child via props. The child is not allowed to change its parents state, if a child wants to change a parents state then the parent passes a callback to the child that the child can call to change the state. This is fundamental to reacts state management. A child does not need to know how a parent stores it’s state (class instance, hook instance or state library).
if your application uses a global state manager like redux, then global state is mapped to props and a store action can be called to update global state. In this case the child does not need to know who else is using the state because it’s global.
class Foo extends Component {
constructor (props) {
super(props);
this.state = { myState: 0 };
this.setMyState = this.setMyState.bind(this);
}
setMyState (value) {
this.setState({
myState: value
});
}
render () {
return (
<MyChildCompoent myStat={this.state.myState} setMyState={this.setMyState} />
);
}
}
you'll need to declare the state in the parent:
state = {
someKey: '',
};
And then in the parent, define some function to update it:
updateSomeKey = (newValue) => {
this.setState({ someKey: newValue });
}
And then pass both of these values as props to your sibling components:
render() {
return (
<div>
<Sib1 someKey={this.state.someKey} updateSomeKey={this.updateSomeKey} />
<Sib2 someKey={this.state.someKey} updateSomeKey={this.updateSomeKey} />
</div>
)
}
You shouldn't need to in order to update the 'shared' state. In the code above, the someKey state can be updated in either component by calling the updateSomeKey function that is also available as a prop.
If either component calls that function (this.props.updateSomeKey('hello!')) the updated state will propagate to both components.
I need to hook-up to the component update event, to fetch fresh data. Hence, I wrote something like:
...
constructor(props) {
super(props);
const values = queryString.parse(this.props.location.search),
type = values['type'];
this.props.loadChallengeDetails(type);
}
shouldComponentUpdate() {
console.log('shouldComponentUpdate');
/* if (this.props.location.search !== nextProps.location.search) {
console.log('should reload report');
const values = queryString.parse(nextProps.location.search),
type = values['type'];
this.props.loadChallengeDetails(type);
} */
return true;
}
componentDidUpdate(prevProps) {
console.log('componentDidUpdate');
if (this.props.location.search !== prevProps.location.search) {
console.log('should reload report');
const values = queryString.parse(this.props.location.search),
type = values['type'];
this.props.loadChallengeDetails(type);
}
}
render() {
console.log('details:', this.props.details);
...
This page/view is a "detail" for a "master" page, if you will. Depending on the value of type passed in the query, it fetches some data.
On first render it happens fine. Both shouldComponentUpdate or componentDidUpdate lifecycle methods are invoked. When I go back then view details for another record, however, it shows stale data. Console log prints details inside render, but does not call either shouldComponentUpdate or componentDidUpdate.
What am I doing wrong? Please advise.
I've been trying to optimise an app by preventing the container component in the example below from re-rendering every time the child component updates. The app example only has one counter which is updated via a key. I've stripped the example down to just one counter although I'm using multiple counters in the actual app.
It works by dispatching an increment action every second, which updates the redux state. The container maps through each counter (in this case just one) held in state and renders a child component which just displays the count.
I've looked at using PureComponent and shouldComponentUpdate but couldn't get it to stop re-rendering and I'm also not sure if the latter is the best approach. From similiar questions I've gleaned that the problem might be that both the container and child component are referring to the same object in state and therefore are both updated. Sandbox example here: https://codesandbox.io/s/nostalgic-rubin-k1gxy
Container
class CounterContainer extends React.PureComponent {
renderWatches = () => {
//was some map logic here originally hence separate method
const { counters } = this.props;
const counterKeys = Object.keys(counters);
return counterKeys.map(key => {
return <SingleCounter key={key} mapKey={key} />;
});
};
render() {
console.log("CounterContainer rendered");
return <div>{this.renderWatches()}</div>;
}
}
const mapStateToProps = state => {
return {
counters: state.counters
};
};
export default connect(mapStateToProps)(CounterContainer);
Child
export const SingleCounter = props => {
useEffect(() => {
const interval = setInterval(() => {
props.dispatch(actionCreators.increment(props.counter.key));
}, 1000);
return () => clearInterval(interval);
});
console.log("Child rendered");
return <div>{props.counter.counter}</div>;
};
export const mapStateToProps = (state, ownProps) => {
return {
counter: state.counters[ownProps.mapKey]
};
};
export default connect(mapStateToProps)(SingleCounter);
Looking at the console logs, I'd ideally like to see the child component re-render every time the counter is incremented however the container should render just once (or in the actual app, whenever another counter is added)
I'm having a heck of time with this. I do NOT want my child to re-render when the parents state changes. I've tried to use shouldComponentUpdate in the child component, but for some reason that's not even being invoked.
My issue is this. I have several charts on a grid, I want to update one of the charts configuration settings, which I pass as a prop to the component. The parent, which they all share, updates that childs config, but in the process, the config changes and thus they all re-render.
Why isn't shouldComponentUpdate being invoked? It gets invoked on the parent, so I'm assuming it is invoked where the state changes???
My code looks something like:
Parent - has selectDataType with setState
Child1 - calls selectDataType which was passed down as a prop, which re-renders
Child2 - no changes to it's props, but re-renders, which I need to stop
Parent:
selectDataType(e) {
e.stopPropagation();
var cellId = e.currentTarget.dataset.id;
var cellValue = e.currentTarget.childNodes[0].value;
var newCells = [];
newCells = this.state.cells.map(function (cell, i) {
var newObj = Object.assign({}, cell);
if (cellId == cell.id) {
newObj['dataType'] = cellValue;
}
return newObj;
});
this.setState({
cells: newCells
});
return;
}
Child1:
export default class Pie extends React.Component {
constructor(props) {
super(props);
this.create = this.create.bind(this);
}
shouldComponentUpdate() {
return false;
}
create() {
return {
//some data
}
}
render() {
return (
<div>
<ReactECharts
option={ this.create() }
style={{ position: "absolute", top: 0, bottom: 0, left: 0, right: 0, height: "100%" }}
theme="chalk"
notMerge={ true }
/>
</div>
)
}
}
Child2: exactly like Child1
Make sure your children are given a static key prop (as for all arrays of components). If they're given a random key, then when the parent re-renders it'll give all the children a new random key (different from their old one). This will make React think it's a completely different component, so the existing one will completely unmount and be remounted (with the new properties). So the child component isn't updating, it's remounting. Hence shouldComponentUpdate won't be called.
You might find answers here, for both classes and hooks:
ReactJS: setState on parent inside child component
Short answer for hooks would be:
Send parent state and setState in child prop. Lets call those parentState and setParentState
In child component:
have a useEffect which only does something when parentState is updated:
useEffect(() => {
props.parentStateSetter(childStateValueYouWantToTrack);
}, [props.parentState, childStateValueYouWantToTrack]);
You should check out the link for more information
I have the following scenario:
1) There is a parent component "ModuleListContainer".
2) A module (in the module list, also a child component, but not interesting in this context) gets selected when hovering over it a module item in the list.
3) When hovering over a module, a menu should be shown in the corner of the module.
4) The whole parent component should NOT be updated when a module is selected, since it can be quite a long list of modules, that is why I set shouldComponentUpdate = false when updating which module should be selected.
5) The menu is loaded when the parent component loads, and only its position is updated when hovering over a module.
This is the parent component (simplified)...
class ModuleListContainer extends Component {
constructor(props) {
super(props);
this.state = {
selectingModule: false,
currentlySelectedModule: nextProps.currentModule
}
}
shouldComponentUpdate(nextProps, nextState) {
if (nextState.selectingModule === true) {
this.setState({
selectingModule: false,
currentlySelectedModule: null
})
return false;
}
return true;
}
mouseEnterModule = (e, moduleItem) => {
const menu = document.getElementById('StickyMenu');
const menuPosition = calculateModuleMenuPosition(e.currentTarget);
if (moduleItem.ModuleId !== this.props.currentModuleId) {
this.props.actions.selectModule(moduleItem);
this.setState({
selectingModule: true
});
}
menu.style.top = menuPosition.topPos + 'px';
menu.style.left = menuPosition.leftPos + 'px';
}
render() {
return (
<div>
<section id="module-listing">
{/* ... list of mapped modules with mouseEnterModule event */}
</section>
<ModuleMenu {... this.props} currentlySelectedModule={this.state.currentlySelectedModule} />
</div>
);
}
}
This is the menu component (simplified)...
class ModuleMenu extends Component {
constructor(props) {
super(props);
this.state = {
currentModule: this.props.currentlySelectedModule
};
}
clickMenuButton = () => {
console.log('CURRENT MODULE', this.state.currentModule);
}
render() {
return (
<div id="StickyMenu">
<button type="button" onClick={this.clickMenuButton}>
<span className="fa fa-pencil"></span>
</button>
</div>
);
}
}
When, in my menu component, I try to console.log the current module from the state, I keep getting null.
My question is if this is because...
I have set the shouldComponentUpdate to false and the menu's state does not get updated?
Or could it be because I do not re-render the whole component?
Or is it because I load the menu together with the parent component
and it does not get re-rendered when a module is selected?
Or is it possibly a combination of some of the above?
The react docs (https://reactjs.org/docs/react-component.html) says:
Returning false does not prevent child components from re-rendering
when their state changes.
Therefore, I am hoping that it is none of the above since I really don't want to have to re-render the entire component when selecting a module.
Your children state doesn't change in this case, you're only changing the state of the parent. What you should probably do is split the render method of your component into two components:
render() {
return (
<div>
<NoUpdateComponent someProps={this.props.someProps}/>
<ModuleMenu {... this.props} currentlySelectedModule={this.state.currentlySelectedModule} />
</div>
);
}
And then in your first costly component, use the shouldComponentUpdate method to prevent it from re rendering
I think that in your code there are other problems you need to solve before looking for a practical solution, starting from the use you make of shouldComponentUpdate().
Official doc says that:
Use shouldComponentUpdate() to let React know if a component’s output is not affected by the current change in state or props. The default behavior is to re-render on every state change, and in the vast majority of cases you should rely on the default behavior.
shouldComponentUpdate() is invoked before rendering when new props or state are being received. Defaults to true. This method is not called for the initial render or when forceUpdate() is used.
If you perform a setState() call inside the shouldComponentUpdate() function it might work but essentially you are telling React to start a new render cycle before knowing if in this execution it should render or not.
Also keep in mind that setState() is not guaranteed to be executed immediately:
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
Moreover (not very clear from the code, so I guess) you are separating Component and DOM object for ModuleMenu: its representation must be guided by state or props, here instead you are using HTMLElement.style.x syntax to set its representation properties.
I'd restructure ModuleListContainer to store in its state an object that represents ModuleMenu properties, that will be passed to ModuleMenu component as props, something like this:
moduleMenu {
currentModuleId: ... ,
top: ... ,
left: ...
}
And set the state in mouseEnterModule handler:
mouseEnterModule = (e, moduleItem) => {
const menuPosition = calculateModuleMenuPosition(e.currentTarget);
if (moduleItem.ModuleId !== this.props.currentModuleId) {
this.props.actions.selectModule(moduleItem);
this.setState({
moduleMenu: {
currentModuleId: moduleItem.ModuleId,
left: menuPosition.leftPos + 'px',
top: menuPosition.topPos + 'px'
}
});
}
}
Then ModuleMenu can get the new position like this:
<div id="StickyMenu">
style={{
left: this.props.left,
top: this.props.top
}}
>
...
</div>
Of course you can still use shouldComponentUpdate() to determine which modules in the list should be updated but this time returning just a boolean after a comparison of (once again, I guess) ids of items; avoiding too many calls to setState() method.
Hope this may help you!