Gathering a collection of elements for a basic tab switcher - reactjs

Objective
I want to make a basic tab switcher with React, starting with gathering a collection of tab elements.
Error
The array of HTML elements I'm able to assemble after my code has run contains a number of null entries equal to that of the actual elements.
Attempt
I have a wrapper for the entire structure, and here is its render method:
render(){
return (
<section className={"TR-Resume " + this.state.closedClass} ref="resume">
<div className={"TR-ResumeInner " + this.state.collapsedClass} ref="resumeInner">
<header className="TR-ResumeHeader">
<h2>Resumé</h2>
<Link to="/" className="TR-ResumeClose" onClick={this.close.bind(this)}>×</Link>
</header>
<section className="TR-ResumeMain js-ResumeTabs" ref="resumeTabs">
<header className="TR-ResumeTabs">
<Tabs className="js-Tabs" resume={this.props.resume.resume} tabRef={el => this.tabs.push(el)} />
</header>
<section className="TR-ResumeContent">
<Sections tabs={this.props.resume.resume} sectionRef={el => this.sections.push(el)} />
</section>
</section>
</div>
</section>
)
}
You'll notice I am using the React recommended method of exposing refs of children components to parents. I am appending each to a property of the parent component: tabRef={el => this.tabs.push(el)}
I understand I am re-declaring that collection every time the block renders, so to prevent appending the same elements to the collection, I'm clearing it in componentWillUpdate() lifecycle event:
componentWillUpdate() {
this.tabs.length = 0;
}
Result
My plan is to attach click events to the tabs to switch between panels, but I can't get that far, because by the time the componentDidUpdate() event fires, this is was is output in the array when I log it to the console:
[ null, null, null, null, a.TR-ResumeTab.js-Tab.TR-ResumeTab_active, a.TR-ResumeTab.js-Tab, a.TR-ResumeTab.js-Tab, a.TR-ResumeTab.js-Tab ]
Is there a better way to do this? Am I doing something wrong?
I plan to take these to animate tab panel with GSAP and want to avoid using jQuery.
Any help would be appreciated.

This is an issue with how you are using refs. When you do an inline ref declaration ( ref={ref => this.input = ref} ), the first time the component renders, it will receive a null value. Then on the second render, the refs will be correctly assigned. So that is why you first see the 4 null values, and then the 4 correct values.
To get around this, you should supply a class method to the ref prop instead of an inline function.
Example:
This:
render() {
return (
...
<Tabs className="js-Tabs" resume={this.props.resume.resume} tabRef={el => this.tabs.push(el)} />
...
)
}
Should be:
applyRef = ref => {
this.tabs.push(ref);
}
render() {
return (
...
<Tabs className="js-Tabs" resume={this.props.resume.resume} tabRef={this.applyRef} />
...
)
}
When you use a class method to apply a ref, it only gets called once when the actual element has been added to the dom, so you shouldn't get the initial null values anymore.

Related

How to get DOM elements within (any/all) nested components in React?

I have a wrapper component in my App as follows. I have logic in the wrapper component that gets the DOM nodes with props.children that have a specific data attribute.
When the DOM elements are direct children, this works fine. When they are children of any nested component they're not found. How can I iterate through the entire structure and get all instances of the DOM nodes by attr?
I'm new to react and sure this should be straightforward, however I've not been able to implement from a number of examples / SO answers. I've tried to implement useRef and useContext hooks but can't get this?
// App.js
<Wrapper>
<div data-elem></div> // Get's found
<Component {...pageProps} />
</Wrapper>
// index.js
const Page = () => {
return (
<>
<div data-elem></div> // Not found
</>
);
}
// Wrapper.js ( simplified version )
export default function Wrapper(props) {
const detectedElements = props.children.filter((item) => item.props['data-elem'] === true);
console.log(detectedElements.length)
return (
<div className="wrapper">
{children}
</div>
)
}
Ultimately I managed to achieve this simply by using querySelectorAll within the useEffect hook. I'm not sure if this is the 'correct' way to do it, but is definitely the most straightforward of the the variations I tried, and seems to do the job, regardless of where the corresponding elements are.
useEffect(() => {
const detectedElements = document.querySelectorAll('[data-elem]')
}, []);

React: Force complete update of component

I have a component that I have written (it is a list). When the component is updated (this list is changed), the onclick event does not update and passes the same values as before the component was rendered. Is there a way to force React to update the entire div from scratch? The variable "value.edit_title_normal" does change, just not the onclick event.
*I have removed some excess code for brevity *
class SearchResults extends React.PureComponent {
//Code to show the detailed information
render() {
//Individual record
const startItem = (this.props.itemsPerPage * this.props.page) - this.props.itemsPerPage;
const endItem = startItem + this.props.itemsPerPage;
//Slice is from/to
let all = searchableDatabase.slice(startItem, endItem).map((value, index) => {
return (
<div onClick={(e) => this.showDetailedInfo(index, e)} > // <== This bit here is not updating when re-rendered
{value.edit_title_normal}
</div>
)
});
//Main container
return (
<div>
<div>
<Pagination allLength={searchableDatabase.length} page={this.props.page} itemsPerPage={this.props.itemsPerPage} />
</div>
<div>
{all}
</div>
</div>
);
}
}
You've misdiagnosed the problem. The index in the map callback always starts from zero, so it's impossible for it to ever change to something else. You're operating on a new array sliced from the original and the map method doesn't know or care about the original array's indexes.
I assume that you expect the index to start from startItem instead of 0. Then you'll have to add it manually to the index:
<div onClick={(e) => this.showDetailedInfo(index + startItem, e)} >

Cannot find ref on initial render

I just read in the official documents that componentDidUpdate isn't called on first render and I think maybe that's why dom isn't defined the first time this Component of mine is rendered.
This is a pop up modal that pops up when a page needs to be edited.
Is there any other way I can go about this?
componentDidUpdate() {
this.renderSingularForm();
}
renderSingularForm() {
let dom = ReactDOM.findDOMNode( this.refs.singularForm );
if ( this.props.pageObjToEdit && dom ) {
// DOESN'T GO HERE ON FIRST RENDER BECAUSE DOM IS NULL
createForm( window, dom, this.props.pageObjToEdit );
}
}
render() {
if ( this.props.pageObjToEdit ) {
return (
<div>
<div ref="singularForm" />
</div>
);
}
else {
return null;
}
}
This is an issue with how you are using refs. You shouldn't be using string refs anymore since they will be deprecated soon. Most people are now using an inline ref ( ref={ref => this.input = ref} ), but when you do that, the first time the component renders it will receive a null value. Then on the second render, the refs will be correctly assigned with the DOM element.
To get around this, you should supply a class method to the ref prop instead of an inline function.
Example:
This:
render() {
return (
...
<div ref="singularForm" />
...
)
}
Should be:
applyRef = ref => {
this.singularForm = ref;
}
render() {
return (
...
<div ref={this.applyRef} />
...
)
}
When you use a class method to apply a ref, it only gets called once when the actual element has been added to the dom, so you shouldn't get the initial null values anymore.
UPDATE 10/18:
You can now avoid this problem altogether using the new React.createRef to create your refs.
You can and should use the componentDidMount in order to safely get DOM elements or refs
From the DOCS:
componentDidMount() is invoked immediately after a component is
mounted. Initialization that requires DOM nodes should go here.
...
It can, however, be necessary for cases like modals and tooltips when
you need to measure a DOM node before rendering something that depends
on its size or position.
Also Note that you are using the old ref API.
You should use the new ref API
Running Example:
class App extends React.Component {
componentDidMount() {
console.log(this.myDiv.id);
}
render() {
return (
<div>
<div id="some-id" ref={ref => (this.myDiv = ref)}>some div</div>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<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="root"></div>

React - component will not render in foreach loop?

I have a small application when you open a tab the React Route redirects to the given component (container). And from the container i want to render the child components. I use the foreach loop to go through a list that has the props that need to be given to the child. However, the child doesn't get rendered.
I don't want to use map because i use libraries that depend on the list class.
render() {
return (
<div>
{this.state.list.ForEach((element) =>
<ChildComponent childName={element.name}
}
</div>
);
}
}
You are confusing Array.forEach with Array.map. forEach does not return anything. The correct way is:
<div>
{this.state.list.map((element, index) =>
<ChildComponent key={index} childName={element.name} />
)}
</div>
map converts the given element into another element, a component in this case. The result of calling map is an array of components that is then rendered. forEach always returns undefined therefore the result of your code is the same as writing:
<div>
{undefined}
</div>
Note that key is also necessary when rendering lists of components.

When should I be using React.cloneElement vs this.props.children?

I am still a noob at React and in many examples on the internet, I see this variation in rendering child elements which I find confusing. Normally I see this:
class Users extends React.Component {
render() {
return (
<div>
<h2>Users</h2>
{this.props.children}
</div>
)
}
}
But then I see an example like this:
<ReactCSSTransitionGroup
component="div"
transitionName="example"
transitionEnterTimeout={500}
transitionLeaveTimeout={500}
>
{React.cloneElement(this.props.children, {
key: this.props.location.pathname
})}
</ReactCSSTransitionGroup>
Now I understand the api but the docs don't exactly make clear when I should be using it.
So what does one do which the other can't? Could someone explain this to me with better examples?
props.children isn't the actual children; It is the descriptor of the children. So you don't have actually anything to change; you can't change any props, or edit any functionality; you can only read from it. If you need to make any modifications you have to create new elements using React.CloneElement.
https://egghead.io/lessons/react-use-react-cloneelement-to-extend-functionality-of-children-components
An example:
main render function of a component such as App.js:
render() {
return(
<Paragraph>
<Sentence>First</Sentence>
<Sentence>Second</Sentence>
<Sentence>Third</Sentence>
</Paragraph>
)
}
now let's say you need to add an onClick to each child of Paragraph; so in your Paragraph.js you can do:
render() {
return (
<div>
{React.Children.map(this.props.children, child => {
return React.cloneElement(child, {
onClick: this.props.onClick })
})}
</div>
)
}
then simply you can do this:
render() {
return(
<Paragraph onClick={this.onClick}>
<Sentence>First</Sentence>
<Sentence>Second</Sentence>
<Sentence>Third</Sentence>
</Paragraph>
)
}
Note: the React.Children.map function will only see the top level elements, it does not see any of the things that those elements render; meaning that you are providing the direct props to children (here the <Sentence /> elements). If you need the props to be passed down further, let's say you will have a <div></div> inside one of the <Sentence /> elements that wants to use the onClick prop then in that case you can use the Context API to do it. Make the Paragraph the provider and the Sentence elements as consumer.
Edit:
Look at Vennesa's answer instead, which is a better explanation.
Original:
First of all, the React.cloneElement example only works if your child is a single React element.
For almost everything {this.props.children} is the one you want.
Cloning is useful in some more advanced scenarios, where a parent sends in an element and the child component needs to change some props on that element or add things like ref for accessing the actual DOM element.
In the example above, the parent which gives the child does not know about the key requirement for the component, therefore it creates a copy of the element it is given and adds a key based on some unique identifier in the object. For more info on what key does: https://facebook.github.io/react/docs/multiple-components.html
In fact, React.cloneElement is not strictly associated with this.props.children.
It's useful whenever you need to clone react elements(PropTypes.element) to add/override props, without wanting the parent to have knowledge about those component internals(e.g, attaching event handlers or assigning key/ref attributes).
Also react elements are immutable.
React.cloneElement( element, [props], [...children] ) is almost equivalent to:
<element.type {...element.props} {...props}>{children}</element.type>
However, the children prop in React is especially used for containment (aka composition), pairing with React.Children API and React.cloneElement, component that uses props.children can handle more logic(e.g., state transitions, events, DOM measurements etc) internally while yielding the rendering part to wherever it's used, React Router <switch/> or compound component <select/> are some great examples.
One last thing that worth mentioning is that react elements are not restricted to props.children.
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">
{props.left}
</div>
<div className="SplitPane-right">
{props.right}
</div>
</div>
);
}
function App() {
return (
<SplitPane
left={
<Contacts />
}
right={
<Chat />
} />
);
}
They can be whatever props that makes sense, the key was to define a good contract for the component, so that the consumers of it can be decoupled from the underlying implementation details, regardless whether it's using React.Children, React.cloneElement, or even React.createContext.

Resources