I built a simplistic tab container in ReactJS using the idea that the container component keeps in its state an integer index denoting the tab pane to display and then renders only the child (from the this.props.children array) that is found at that index position.
The gist of this approach was:
const TabContainer = React.createClass({
props: {
tabNames: React.PropTypes.arrayOf(React.PropTypes.string).isRequired
},
getInitialState: function() {
return {
activeIndex: 0
};
},
setTab: function(n) {
this.setState({activeIndex: n});
},
render: function render() {
const childToRender = this.props.children[this.state.activeIndex];
return (
<div className='tab-container'>
<Tabs
tabNames= {this.props.tabNames}
active = {this.state.active}
setTab = {this.setTab}
/>
<div className='tab-pane'>
{childToRender}
</div>
</div>
);
}
});
I.e. only the indexed child is selected and rendered (I've omitted for the sake of simplicity the code handling the edge case where this.props.children is not an Array).
I found out that this approach was unsatisfactory as when the user selected different tabs, the component corresponding to the pane to render was mounted and unmounted repeatedly and any state that the panes had could not be preserved.
Ultimately, I used an approach in which all children are rendered and all panes, except the selected one, are assigned a class hidden that is then used to style those panes as: display:none. When this later solution was used my panes remained mounted even after the user clicked through the various tabs and any state they had wasn't lost as the user cycled through the tabs.
My questions are:
was the initial approach (where only a specific child is rendered) an anti-pattern or was there some other mechanism I could have used to preserve the state of the individual panes or prevent them from being unmounted?
if the initial approach was indeed an anti-pattern how can that anti-pattern be expressed more generally?
I don't think the initial approach was an antipattern at all. Choosing whether or not to mount/unmount in your logic is just dependent on your circumstances. If you want state preserved, then don't unmount. If you want a fresh element, complete with a call to getInitialState, then unmounting is the right way to go.
As an easy counterexample, consider React-Router. The Router completely unmounts/remounts components on route change. And route changing is effectively just a higher order of tabbing.
But given the situation where you want state preserved, I think your solution is the proper one. You might want to take a look at material-ui, which does something very similar in their tabbing: https://github.com/callemall/material-ui/blob/master/src/Tabs/TabTemplate.js
Related
I have a parent and child components, In my example, when increase button was clicked, child is gonna be rerendered.
import React, {useEffect,useState} from 'react';
export const App = (props) => {
const [count, setCount] = useState(0);
const handleCount = ()=>{
setCount(count+1);
}
useEffect(()=>{console.log("Parent was rerendered")})
return (
<div className='App'>
<h1>This is parent</h1>
<button onClick={handleCount}>increase</button>
<Child/>
</div>
);
}
const Child = props => {
useEffect(()=>{console.log('Child was rerendered')})
return (<>
<h1>This is child</h1>
</>)
}
I think it is pretty simply. And I also understand that, to prevent child rendering, I could us memo.
However, at some point, I studied the the concept of virtual dom. In it, it explained that the greatest of React is that it can prevent unchanged components to get rerendered, in order to prevent unnecessary computation.
If virual dom exist, isnt it that my child didnt change at all? Why is that my child get rerender eventually?
Edit:
I have to emphasize that my question is not about how to prevent it from re rendering. I knew the existence of memo. My question is virtual dom. React says virtual dom can compare the previous and current UI, then it will automatically prevent unchanged component from re rendering. If that's the case, why my example still got rendered? When virtual dom comes it, it doesnt make sense at all. Please dont neglect my concerns on virtual dom when providing answer
Changing parent state triggers re-rendering the component along with its children. Your child component will be re-rendered when you press 'increase' button without regard to its internal changes in view of the code you shared.
To prevent children from re-rendering, you may use React.memo like below
const Button = React.memo((props: PropsWithChildren<any>) => {
return (
<Button type="button" color={props.color} size={props.size} onClick={props.onClick}>
{props.label}
</Button>
);
});
If you do not use that kind of approach, children will be re-rendered by default.
If you wonder opposite; if state changes in child component, that will not cause parent component re-rendering. But there are ways to make that happen.
NOTE: DOM will not update unless something changed
Further reading
React Memo
UPDATE
I guess there's a misconception. When you press the 'increase` button, there will be re-rendering
The scenario:
When the state of a component changes, React updates the virtual DOM. React then compares the current version of the virtual DOM with the previous virtual DOM.
After comparing both and finding out which objects have changed, React updates them.
React updates DOM only if the render process produces changes. I hope it is now more clear to you.
To better understand, here's an article
React Virtual Dom
EXTRA UPDATE
Let's assume that two red nodes shown in the picture below are your components in the DOM (parent and child respectively)
When you press the 'increase' button in the parent component, it changes the inner state which leads to re-rendering of parent component along with its child component. So two red circles are re-rendered.
React, compares the old Virtual DOM and brand new Virtual DOM. Even if your parent and child component re-rendered, that will not lead to an update on the Real DOM because there's neither a visible change nor a structural change. You can think of it such as showing the current number on your parent component as a text. To visualize it, please take a look at the code below:
export const App = (props) => {
const [count, setCount] = useState(0);
const handleCount = () => setCount(count+1);
return (
<div className='App'>
<h3>Current count: {count}</h3>
<button onClick={handleCount}>increase</button>
<Child/>
</div>
);
}
Now, we have changed your parent component with an additional h3 tag in which we show the current count. In this case, if we press the 'increase' button that will lead to re-render again and this time the Real DOM will be updated because React will find out that there's a difference between old and new versions of the Virtual DOM.
So rendering and updating the real DOM are different processes.
Back to React.memo
If you use React.memo in your child component, React will render it, memoize the result instead of re-rendering and React will not perform a Virtual DOM difference check.
I'm writing a React application with a lot of modals (but only one ever active at a time with no nested modals), and I'm unsure of which of these two solutions is preferable when it comes to handling showing and hiding these modals:
Have a state boolean variable in the parent component that determines whether or not the modal should be shown. If this boolean variable is false, don't render the component. If it's true, render it. Allow the modal to influence this event via a passed in props callback it can leverage. Example snippets:
{ this.state.prompt === "makePW" ?
<MakePassword closeModal={this.closePWModal} /> :
null
}
Now, within the component, it is always visible and does not control its lifecycle. If it is being rendered then it is visible.
Just always show the component in the parent component, like so:
<MakePassword />
Then within the component itself handle its entire lifecycle. That is, the component will have a boolean state variable for its visibility. Some snippets for this approach:
<Modal open={this.state.open} onClose={this.closeModal}>
<Modal.Header>Header</Modal.Header>
<Modal.Content>Body</Modal.Content>
</Modal>
There are also hybrid approaches, and other solutions as well I'm sure.
Regardless, I suppose the essence of my question is wondering about the preferable solution for showing and hiding modals: is it to always render them and just toggle their visibility, or to actually toggle between adding and removing them from the DOM? And, should the modal itself generally control its lifecycle or should the parent?
i prefer 1st one but if you want to use the second , i would extend the makepassword component from PureComponent to optimize your component.
Instead of writing shouldComponentUpdate() by hand, you can inherit from React.PureComponent. It is equivalent to implementing shouldComponentUpdate() with a shallow comparison of current and previous props and state. - source react docs
class MakePassword extends React.PureComponent{
...
}
I think it depends on your application.
For example, React-Bootstrap modals stay in the DOM and that allows for nice in/out animations.
I'm trying to understand how I can do the following flow in React
page load
ajaxA()
-return dataA
-render dropdown HTML
-ajaxB(dataA)
-return dataB
-render chart HTML
handleDropdownChange()
-ajaxB()
-return dataB
-render chart HTML
I have it split out into two components
a top section with controls (ajaxA returns data to build some dropdowns.
a couple of chart below. When one of the dropdowns in the top part changes, then the chart should update.
Should ParentApp contain all methods for updating the charts and data related to child components, passing both in as props, eg:
handleControlChange (event) {
this.setState(selectedOption: event.target.value)
},
render () {
return (
<ParentApp>
<Controls
handleControlChange={this.handleControlChange} />
<Chart />
</ParentApp>
)
}
or should that functionality live within the Controls component? The second way seems more reasonable to me but since Controls and Chart are siblings I'm not sure how I can pass data to Chart when there is a change within`Controls. As I understand it data only flows from parents to children in React. It seems like ParentApp would soon contain basically all the methods needed by its children components which kind of seems it goes against keeping components modular.
Where should I store my data and my methods to interact with it?
In principle React only lift the state up when this state must be shared between siblings and/or parent, when a child component doesn't need to send its state to parent or siblings it can have its own state and no lifting up is needed.
But practically speaking most apps will have state in central place at parent component, there are even libraries for central states like Redux , that being said we didn't lose modularity in any way because state is only part of the story, for example spliting the app into components will give you modularity in rendering, also because child components takes props you can reuse the same component in multiple places with different props to control its appearance almost the same way you change the appearance of an <a> link by just applying a class.
I have issue keeping ref of nested original element while cloned parent gets unmounted. Not sure what I am doing wrong.
Have page with structure like this
<Sortable>
<Collapsible ref='Collapsible-1'>...</Collapsible>
<Collapsible ref='Collapsible-2'>...</Collapsible>
</Sortable>
Sortable component wraps each children to Sortable__item component
When user starts sorting(dragging) one of Sortable__item components I React.cloneElement() original Sortable__item to display it as dragable shadow
It gets cloned with all children, in this scenario with Collapsible component on which Page has ref['Collapsible-1'] saved. That ref on page gets changed to this shadows ref.
Once touchEnd kicks in I update state of Sortable to not show shadow(it gets unmounted).
When it gets unmounted it also removes ref inside Page(changes to null)
ISSUE: page doesn't have ref to original Collapsible as first its got changed to shadow, and then shadow got unmounted so now its null
Quick hack/fix to solve this annoying issue. This way refs never get updated if they already exist. This is pretty bad, but I don't know how else prevent this. Anyone can point me to right direction?
let registerRef = function(name, item){
if(this.items[name]) return;
this.items[name] = item;
}
<Sortable>
<Collapsible ref={registerRef.bind(this,'Collapsible-1')}>...</Collapsible>
<Collapsible ref={registerRef.bind(this,'Collapsible-2')}>...</Collapsible>
</Sortable>
I have re-thought my solution, and changed how it works.
Idea to duplicate entire child with all components inside, only to show it as draggable shadow, is bad idea! It is performance heavy task to do and also it causes unexpected results for reactjs refs.
I have chosen other solution. When I start sorting(dragging) child I allow user to take it out from list and move it(I mean I apply display: absolute; and some css transforms to follow finger). Then I create div inside list like a dropbox to indicate where draggable item will be droped on onTouchEnd. This way is much better for performance because I am not duplicating entire item DOM tree and it doesn't cause issue with duplication of children refs.
Solved.
My parent component is a list of tabs. Clicking on tab leads me to Child #1 or Child #2. I can also route to Child #1 or Child #2 by URL. In this case, I check the path to know which tab to select programatically.
My problem comes when I redirect from one child to another. My parent is oblivious to the fact that a change has occurred - none of the initializing functions in that component get called, since it is already rendered. Therefore the tab that was originally selected (Child #1) - remains selected even though I am now viewing Child #2.
The tab that is selected is being stored in the parent's state - is there any way for me to update the parent's state from the child component?
Pass a function down to the child(ren) from the parent that modifies the state accordingly. A very simple version could be something like:
class Parent {
setActive(activeId) {
this.setState({active: activeId});
}
render() {
return (
<Child setActive={this.setActive}/>
);
}
}