CSSTransition nodeRef for component with direct children - reactjs

I want to get rid of the warning on StrictMode for findDOMNode when using react-transition-group but I stumbled upon an issue.
My <Slide> component looks like this:
class Slide extends React.Component {
nodeRef = React.createRef();
render() {
return (
<CSSTransition
in={this.props.in}
timeout={ANIMATION_DURATION}
mountOnEnter={true}
unmountOnExit={true}
classNames={{
enter: "slideEnter",
enterActive: "slideEnterActive",
exit: "slideExit",
exitActive: "slideExitActive"
}}
nodeRef={this.nodeRef}
>
{this.props.children}
</CSSTransition>
);
}
}
It receives a Drawer element as children, the Drawer component looks like this:
class Drawer extends React.Component {
render() {
return (
<div className="drawer">
<button onClick={this.props.onClose}>close me</button>{" "}
<div>This is my drawer</div>
</div>
);
}
}
I cannot wrap the children element with a HTML tag (to attach a ref <div ref={this.nodeRef}>{this.props.children}</div> because it breaks the animation of the content. (I'm using this for children that are different drawers with position absolute)
I've also tried with cloneElement but it still doesn't work (with the code from below it behaves like this: 1. in no animation, 2. out no animation, 3. in animation works but I get the warning findDOMNode so it seems that nodeRef is sent as null, 4. out animation does not work.
const onlyChild = React.Children.only(this.props.children);
const childWithRef = React.cloneElement(onlyChild, {
ref: this.nodeRef;
});
Is there any solution for this situation? Thanks!

The problem is that nodeRef needs to point to a DOM Node, as the name suggests, in your case it points to an instance of the Drawer class. You have two options:
Pass the ref through another prop, e.g. forwardedRef, and in the Drawer class pass that prop to the root element:
React.cloneElement(onlyChild, {
forwardedRef: this.nodeRef,
})
<div ref={this.props.forwardedRef} className="drawer">
Convert Drawer to a function component and use React.forwardRef:
const Drawer = React.forwardRef((props, ref) => {
return (
<div ref={ref} className="drawer">
<button onClick={props.onClose}>close me</button>{" "}
<div>This is my drawer</div>
</div>
);
});

Related

Creating a parent 'workspace' component in ReactJS

Using ReactJS, I am trying to create a common workspace component that will have toolbar buttons and a navigation menu. The idea I have is to re-use this component to wrap all other dynamic components that I render in the app.
Currently, I've created a Toolbar and MenuBar components that I then add to each component in the app as such:
<Toolbar/>
<MenuBar/>
<Vendors/>
This does not feel right, since my aim is to have just one component which would be something like:
<Workspace>
<Vendor/>
</Workspace>
However, I am not sure of how to achieve this and whether this is the right approach.
As to whether or not it is the right approach is subjective, but I can provide insight into one way to make a "wrapper" type component:
// Your workspace wrapper component
class Workspace {
render() {
return (
<div className="workspace">
<div className="workspace__toolbar">
Toolbar goes here
</div>
<div className="workspace__nav">
Navgoes here
</div>
<div className="workspace__content">
{this.props.children}
</div>
</div>
)
}
}
// Using the component to define another one
class MyComponent {
render() {
return (
<Workspace>
This is my workspace content.
</Workspace>
)
}
}
You can also look at HOC's or Higher Order Components to wrap things.
React offer two traditional ways to make your component re useable
1- High-order Components
you can separate the logic in withWorkspace and then give it a component to apply that logic into it.
function withWorkSpace(WrappedComponent, selectData) {
// ...and returns another component...
return class extends React.Component {
render() {
// ... and renders the wrapped component with the fresh data!
// Notice that we pass through any additional props
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
const Component = () => {
const Content = withWorkSpace(<SomeOtherComponent />)
return <Content />
}
2- Render Props
or you can use function props then give the parent state as arguments, just in case you need the parent state in child component.
const Workspace = () => {
state = {}
render() {
return (
<div className="workspace">
<div className="workspace__toolbar">
{this.props.renderTollbar(this.state)}
</div>
<div className="workspace__nav">
{this.props.renderNavigation(this.state)}
</div>
<div className="workspace__content">
{this.props.children(this.state)}
</div>
</div>
)
}
}
const Toolbar = (props) => {
return <div>Toolbar</div>
}
const Navigation = (props) => {
return <div>Toolbar</div>
}
class Component = () => {
return (
<Workspace
renderNavigation={(WorkspaceState) => <Navigation WorkspaceState={WorkspaceState} />}
renderTollbar={(WorkspaceState) => <Toolbar {...WorkspaceState} />}
>
{(WorkspaceState) => <SomeComponentForContent />}
</Workspace>
)
}

Passing ref to child component to animate with GSAP

I'm new to React and i am trying to integrate GSAP to animate a child component using refs. The process to try and animate worked fine before I seperated out the elements into their different components!
There are no error codes from React, but I do get the following console error:
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
I've looked into the forwardRef mentioned but not sure if it's right for what i'm trying to achieve.
/**
* Parent Component
*/
import React, { Component } from 'react';
import Spirit from './Spirit';
import { TweenMax, Power1 } from 'gsap';
class SpiritResults extends Component {
constructor(props) {
super(props);
this.multiElements = [];
}
componentDidMount() {
TweenMax.staggerFromTo(
this.multiElements,
0.5,
{ autoAlpha: 0 },
{ autoAlpha: 1, ease: Power1.easeInOut },
0.1
);
}
render() {
return (
<ul className="mm-results">
{this.props.spirits.map(({ id, title, featImg, link, slug, index }) => (
<Spirit
key={id}
slug={slug}
title={title}
featImg={featImg}
link={link}
ref={li => (this.multiElements[index] = li)}
/>
))}
</ul>
);
}
}
/**
* Child Component
*/
import React from 'react';
const Spirit = ({ slug, link, featImg, title, index }) => (
<li id={slug} className="mm-item" ref={index}>
<a href={link}>
<div className="inner">
<img src={featImg} alt={title} />
<h3>{title}</h3>
</div>
</a>
</li>
);
export default Spirit;
Any tips that can be given to get the animation to work would be appreciated. If there are any better ways of animating react with GSAP please let me know your thoughts.
Thanks
There are a few mistakes here.
ref is not a prop. You can't just pass it to a custom component and access it like props.ref
You're appending the wrong ref inside <li>, index is not a valid ref object
To pass down a ref to a custom component you need to use React.forwardRef and append the ref to your Child's <li>. Something like this
const Parent = () =>{
const ref = useRef(null)
return <Child ref={ref} title='hey i\'m a prop'/>
}
const Child = React.forwardRef((props, ref) =>(
<li ref={ref}>
{props.title}
</li>
))

Re-render React descendant components without re-rendering ancestors

I'm trying to build a Navbar using React 16.8.3. I would like to use composition to pass the Navbar content instead of passing a config object via props, in order to have more flexibility. Something like this:
<Navbar>
<NavItem>Some label</NavItem>
<NavItem>
<span>Some arbitrary content</span>
<NavItem>
</Navbar>
instead of:
const navItems = [
{
label: 'Some label'
},
{
label: 'Some other label'
}
]
<Navbar items={navItems} />
So far the Navbar is working fine. I've added some logic in the shouldComponentUpdate method to prevent multiple re-renders:
shouldComponentUpdate(nextProps) {
return nextProps.selectedItem !== this.props.selectedItem;
}
so the Navbar only re-renders when its selected item changes, and not, for instance, when the Navbar parent re-renders.
Problem is that one NavItem contains a badge with a task count that must be updated whenever the user does some tasks:
Todos screenshot
and the item markup is:
<Navbar>
<NavItem>
<div className="has-badge">
<span>Label</span>
<span className="badge">{this.props.toDoCount}</span>
</div>
</NavItem>
</Navbar>
this.props.toDoCount is a prop of the Navbar parent, and not of the Navbar itself.
How can I update the badge number without re-rendering the whole Navbar?. So far I've tried creating a Badge component, adding some state, and a method to update the badge number using a ref in the Navbar parent:
import React, { PureComponent } from 'react';
interface BadgeProps {
number: number;
}
class Badge extends PureComponent<BadgeProps> {
state = {
number: 0
};
setCount(number) {
this.setState({
number
});
}
render() {
return <span className="badge">{this.state.number}</span>;
}
}
In the Navbar parent:
private todos = createRef<Badge>();
...
componentDidUpdate(prevProps: EhrProps) {
this.todos.current.setCount(toDosCount);
}
and it's working, but... is there an easier or cleaner way of doing this in React??
Thanks!
PS: We are using Redux in the project, but I would like to avoid using the store in the Navbar or its items.
EDIT:
I'm using React.children and React.cloneElement in the Navbar's render method:
render() {
const { className, children, selectedItem, ...rest } = this.props;
const classes = classNames(
{
navbar: true
},
className
);
return (
<nav className={classes} {...rest}>
{React.Children.map(children, child => {
if (child.type === NavItem) {
return React.cloneElement(child, {
onClick: this.handleItemClick,
selected: child.props.name === selectedItem
});
}
return child;
})}
</nav>
);
}
And each NavItem handles its own render:
return (
<div className={classes} onClick={handleClick} onKeyPress={handleKeyPress} role="menuitem" tabIndex={0}>
{children}
</div>
);
Presumably, you have some code for the the Navbar component that looks a bit like this.
class Navbar extends React.Component<Props> {
render() {
return (
<div>
{this.props.navItem.map(item => <NavItem key={item.label}>{item.label}</NavItem>)};
</div>
);
}
}
and then some code to render each child NavItem.
To make the component fairly efficient, it's sufficient to have the whole of Navbar rerender, but only not re-render each child.
What I would recommend is:
Make each child of Navbar be rendered in its own component; in the component above it's called NavItem
Use either componentShouldUpdate or React.PureComponent (look into this! Once you understand it, it's a great general solution to use by default instead of React.Component for every component) to make sure that each child only re-renders when its value changes
What will happen when you update the badge for the single NavItem is that Navbar will re-render. Most of the NavItems will see their Props haven't changed, and not re-render. The single child of Navbar that has the badge will have changed, and will re-render. With this, the real overhead is actually quite low.
If your Navbar has a ton of children or your badge for that single child changes a lot, you can probably optimize it more by using React.Context or Redux to pass in the value for that single child, but that feels messy and seems like premature optimization.
Good luck!

React constructor called only once for same component rendered twice

I expected this toggle to work but somehow the constructor of component <A/> is called only once. https://codesandbox.io/s/jvr720mz75
import React, { Component } from "react";
import ReactDOM from "react-dom";
class App extends Component {
state = { toggle: false };
render() {
const { toggle } = this.state;
return (
<div>
{toggle ? <A prop={"A"} /> : <A prop={"B"} />}
<button onClick={() => this.setState({ toggle: !toggle })}>
toggle
</button>
</div>
);
}
}
class A extends Component {
constructor(props) {
super(props);
console.log("INIT");
this.state = { content: props.prop };
}
render() {
const { content } = this.state;
return <div>{content}</div>;
}
}
ReactDOM.render(<App />, document.getElementById("root"));
I already found a workaround https://codesandbox.io/s/0qmnjow1jw.
<div style={{ display: toggle ? "none" : "block" }}>
<A prop={"A"} />
</div>
<div style={{ display: toggle ? "block" : "none" }}>
<A prop={"B"} />
</div>
I want to understand why the above code is not working
In react if you want to render same component multiple times and treat them as different then you need to provide them a unique key. Try the below code.
{toggle ? <A key="A" prop={"A"} /> : <A key="B" prop={"B"} />}
Since that ternary statement renders results in an <A> component in either case, when the <App>'s state updates and changes toggle, React sees that there is still an <A> in the same place as before, but with a different prop prop. When React re-renders it does so by making as few changes as possible. So since this is the same class of element in the same place, React doesn't need to create a new element when toggle changes, only update the props of that <A> element.
Essentially, the line
{toggle ? <A prop="A"/> : <A prop="B"/> }
is equivalent to
<A prop={ toggle ? "A" : "B" }/>
which perhaps more clearly does not need to create a new <A> component, only update the existing one.
The problem then becomes that you set the state.content of the <A> using props.prop in the constructor, so the state.content is never updated. The cleanest way to fix this would be to use props.prop in the render method of the <A> component instead of state.content. So your A class would look like this:
class A extends Component {
render() {
const { prop } = this.props;
return <div>{ prop }</div>;
}
}
If you must take the prop prop and use it in the <A> component's state, you can use componentDidUpdate. Here's an example:
class A extends Component {
constructor(props) {
super(props);
this.state = {content: props.prop};
}
componentDidUpdate(prevProps) {
if (prevProps.prop !== this.props.prop) {
this.setState({content: this.props.prop});
}
}
render() {
const { content } = this.state;
return <div>{ content }</div>
}
}
React will only call the constructor once. That's the expected outcome.
Looks like you're trying to update the state of the component A based on the props.
You could either use the prop directly or use the componentDidUpdate lifecycle method, as Henry suggested. Another way is using the static method getDerivedStateFromProps to update the state based on the prop passed.
static getDerivedStateFromProps(props, state) {
return ({
content: props.prop
});
}

react scroll to component

I have 3 components which is my site. Each component js-file is loaded and all 3 shows on one page like this:
Topmenu
SectionOne
SectionTwo
In the Topmenu component I have a menu only. I’ve tried to setup the scrollToComponent onClick at a menu field (SectionOne). But I cannot figure out how to get it to scroll to SectionOne when clicked?
I know this.sectionOne is a ref to an internal ref, right? But how to direct it to a ref inside the “sectionOne.js” file?
I have the following inside my TopMenu.js file
onClick={() => scrollToComponent(this.sectionOne , { offset: 0, align: 'top', duration: 1500})}
To forward the ref to somewhere inside the component, you can use the logic of forwardRef.
This means that we create the ref in the parent component and pass the same to the component which passes it down to DOM element inside it.
class App extends React.Component {
constructor(props) {
super(props);
this.section1 = React.createRef();
this.section2 = React.createRef();
this.scrollToContent = this.scrollToContent.bind(this);
}
scrollToContent(content) {
switch(content) {
case 1:
this.section1.current.scrollIntoView({behavior: 'smooth'});
break;
case 2:
this.section2.current.scrollIntoView({behavior: 'smooth'});
}
}
render() {
return (
<main>
<Menu goTo={this.scrollToContent} />
<div className='main'>
<Section1 ref={this.section1} />
<Section2 ref={this.section2} />
</div>
</main>
);
}
}
and then, inside Section1,
const Section1 = React.forwardRef((props, ref)=>{
return (
<section ref={ref} className='section'>
<p>Section 1</p>
</section>
);
});
You can see a working sample here
I'm not using scroll-to-component package, but the same thing applies.
Forward Refs are supported only in React 16.3 now, you can use this alternative approach if you need the feature in lower versions.

Resources