I've hot React component which returns input tag. Can you please explain what is going on at the eighth line ref={element => element && (element.onChange = onChange)}? I
import React from 'react';
export default function MyInput({
onChange,
...rest
}) {
return (
<input
{...rest}
ref={element => element && (element.onChange = onChange)}
/>
);
}
React's ref is used to access the DOM directly, and in general is recommended to use as less as possible. The point of functional refs, and keep in mind that they're deprecated, is to assign the element into a class component's variable. e.g.:
Class MyComponent extends Component {
constructor(props) {
super(props);
this.inputRef = null;
}
...stuff
render() {
...stuff
<input ref={element => this.inputRef = element} />
}
}
Then, you could do something like:
this.inputRef.current.style.color = 'blue';
In your case, there is no need for this. If you want to assign the onChange you get from props, just do this:
<input {...stuff} onChange={onChange} />
Read more about React refs here.
As for element && element.onChange, it's designed to make sure that element exists before accessing it's onChange property. Another way to do it, using optional chaining (only avaliable in react-scripts v3.3 and above), is this:
element?.onChange
Refs are used to access DOM elements
The value of ref differs depending on the type of node:
When the ref attribute is used on an HTML element, the ref created
in the constructor with React.createRef() receives the underlying
DOM element as its current property.
When the ref attribute is used on a custom class component, the ref
object receives the mounted instance of the component as its
current.
They are used in cases where we want to change the value of a child component, without making use of props and all. But in your case, i think you have no need to use ref because you simply wants to assign onChange that you received from props.
Related
I used to have refs in my component when rendering, and it worked:
// props.children is ReactElement<HTMLDivElement>[]
const [childRefs] = useState<RefObject<any>[]>(props.children.map(() => createRef()));
// working code, all the variables (props, childRefs etc) are defined earlier in scope
return <div {...props}>
{
props.children.map((c, i) => <div key={i} ref={childRefs[i]}>{c}</div >)
}
</div>
Basically I'm using the refs to imperatively directly set some transforms to style as mouse moves on JS mousemove event.
However, I now need to inject some CSS class into the passed component automatically. I've created a component (named Layer) that takes the child element, clones it, sets the CSS class, and returns it:
function Layer(props:LayerProps){
const elem = cloneElement(props.children, {...props.children.props,
className: styles['layer']
});
return elem;
}
I've updated the main component like this too:
return <div {...props}>
{
props.children.map((c, i) => <Layer key={i} ref={childRefs[i]}>{c}</Layer>)
}
</div>
However now my refs aren't passed now, understandibly, as the Layer functional component can't have a ref (as it's a function). When I try to set the ref to Layer it can't, and have this error (understandably):
(property) ref: React.RefObject<any>
Type '{ children: ReactElement<HTMLDivElement, string | JSXElementConstructor<any>>; key: number; ref: RefObject<any>; }' is not assignable to type 'IntrinsicAttributes & LayerProps'.
Property 'ref' does not exist on type 'IntrinsicAttributes & LayerProps'.ts(2322)
If I try to forward the ref using forwardRef it doesn't have anything to set that ref to as I'm just modifying the passed child element and returning it, not returning a new element like <div>...</div> that I could forward ref to like <div ref={forwardedRef}>...</div>.
How can I modify the CSS class and keep a ref to the object? I know how to do each one (if I just need to add class I cloneElement, if I just need to ref it I use forwardRef and pass it to the child component in JSX) yet I couldn't figure out being able to do both at the same time.
How can I do it?
Okay, after a bit digging and experimenting I've realized I can give ref "prop" (which isn't technically a real prop, but anyway) in cloneElement just like any prop.
I've ended up forwarding ref to the functional component, and provided ref as a prop to the newly cloned element, and it worked.
Yet, the TypeScript definitions are incorrectly flagging ref property as non-existent while it works perfectly. I needed it to cast the props to any to silence the linter error though:
const Layer = forwardRef((props:LayerProps, ref:any) => {
const elem = cloneElement(props.children, {...props.children.props,
className: `${props.children.props.className ?? ''} ${styles['layer']}`,
ref: ref
} as any); // as any fixes ref complaining
return elem;
});
And in many component:
return <div {...props}>
{
props.children.map((c, i) => <Layer key={i} ref={childRefs[i]}>{c}</Layer>)
}
</div>
Let's say I have a component with a scrollable subcomponent, and I want to expose the ability to scroll:
const MyComponent = (props) => {
return <ScrollView ... />
}
I want to be able to do
<MyComponent ref={myRef} />
...
myRef.scrollTo({x: 0});
So I need a way to forward the ref to the <ScrollView>. Let's try putting the ref on the props:
const MyComponent = (props) => {
return <ScrollView ref={props.scrollRef} ... />
}
...
<MyComponent scrollRef={myRef} />
...
myRef.scrollTo({x: 0});
I just tried that with React Native on iOS, and it indeed works. I see several advantages over React.forwardRef:
Simpler, because I don't need to use another React API.
Works also if there is more than one child who needs ref forwarding.
Seems to me that this approach is
What's the advantage of React.forwardRef? Why was it added in React 16.3?
Note that there is no difference between using another named prop like innerRef FOR FORWARDING, it works the same.
Refactoring class components
Since React moved toward function components (hooks) you might want to refactor the class component code to a function component without breaking the API.
// Refactor class component API to function component using forwardRef
<Component ref={myRef} />
React.forwardRef will be your only option (further explained in details).
Clean API
As a library author you may want a predictable API for ref forwarding.
For example, if you implemented a Component and someone wants to attach a ref to it, he has two options depending on your API:
<Component innerRef={myRef} />
The developer needs to be aware there is a custom prop for forwarding
To which element the innerRef attached? We can't know, should be mentioned in the API or we console.log(myRef.current)
<Component ref={myRef} />
Default behavior similar to ref prop used on HTML elements, commonly attached to the inner wrapper component.
Notice that React.forwardRef can be used for function component and HOC (for class component see alternative below).
Ref forwarding is not limited to DOM components. You can forward refs to class component instances, too.
For function components, forwardRef sometimes comes with useImperativeHandle combo (in class component you just call the class methods on ref instance: ref.current.myAttr().
// Same usage
<Component ref={myRef} />
const Component = React.forwardRef((props, ref) => {
// you can forward ref <div ref={ref} />
// you can add custom attributes to ref instance with `useImperativeHandle`
// like having ref.myAttribute() in addition to ones attached to other component.
});
Important behavior of ref prop without forwardRef.
For the class component, this code alone will attach the ref to CLASS INSTANCE which is not useful by itself and need another ref for forwarding:
// usage, passing a ref instance myRef to class Component
<Component ref={myRef} />
Full example, check the logs:
// We want to forward ref to inner div
class ClassComponent extends React.Component {
innerRef = React.createRef();
render() {
// Notice that you can't just `this.props.ref.current = node`
// You don't have `ref` prop, it always `undefined`.
return <div ref={this.innerRef}>Hello</div>;
}
}
const Component = () => {
const ref = React.useRef();
useEffect(() => {
// The ref attached to class instance
console.log(ref.current);
// Access inner div through another ref
console.log(ref.current.innerRef);
}, []);
return <ClassComponent ref={ref} />;
};
In function components, it won't even work because functions don't have instances.
By default, you may not use the ref attribute on function components because they don’t have instances. [1]
forwardRef.
Refs and the DOM.
Why we need ref forwarding?
Parent element (Board) creates list of children and passes them method to access this list, like this:
export default class Board extends React.Component {
constructor(props) {
super(props);
this.getList = this.getList.bind(this);
const nodes = this.props.data.map(item => (
<BoardItem key={item.id} next={item.next} accessSiblings={this.getList} />
));
this.state = {data: this.props.data, nodes: nodes}
}
getList() {
return this.state.nodes;
}
render() {
return (
<div>
{this.state.nodes}
</div>
);
} }
Then I call update() method and receive this list, filter it and correctly get the required object:
update() {
console.log(this.props.accessSiblings().find(x => x.key == this.props.next));
}
However it returns Symbol(react.element), and I am trying to get such properties as "offsetTop", "offsetHeight" of already rendered element.
Basically, I want to call an event in one element that gets some DOM properties of sibling element(e.g. offsetTop) and changes the state of this sibling.
Is this the correct approach? It feels very hacky and doesn't work at the moment.
You need to use "refs" (see also: how to access a dom element in react).
However, you should avoid working with DOM objects, if possible.
You do need a ref for accessing offsetTop etc., but apart from that you should not pass DOM or ReactElements, but you should only work with state (like "plain javascript" objects) as far as possible, and then render ReactElements (JSX, like <BoardItem ...) as the last step, and never care about DOM elements (React does it for you).
It is also usually not necessary to store ReactElements in variables or state, I suggest to try if you can focus a little bit more on state, and understand JSX more as a way to view the state.
I need to remove a prop from a child.
I have a container element which uses a property on it's children to perform some enhancements on the children. That property should be removed from the child before rendering.
<AsyncContainer>
<Button onClick={this.asyncStuff} asyncHandler="onClick"/>
</AsyncContainer>
The asyncHandler property should be removed from the button before rendering.
AsyncContainer uses React.cloneElement(child, properties).
I've tried nulling the asyncHandler property, setting it to undefined and deleting the property from the child.props. It seems that it is impossible to get rid of this property again.
I just ran into this issue. You can just create a new element and use the old element's type and props you want to pass through. I'm not sure if this an anti-pattern or not, I just stumbled on it and it seems to be working well so far.
It should look something like this:
function AsyncContainer(props) {
const child = React.Children.only(props.children)
const { asyncHandler, ...childProps } = child.props
// do asyncHandler stuff
return React.createElement(child.type, childProps)
}
function AsyncContainer(props) {
const child = React.Children.only(props.children);
return React.cloneElement(
child,
{ asyncHandler: undefined }
);
}
How it works
You clone element using React.cloneElement because element is immutable and only way to change its props is to create clone.
Use second React.cloneElement argument to add new props and remove old props. Unneeded props should be assigned with undefined. You need to do this because by default cloned element is cloned with all its props.
As per the comments you cannot modify the props directly as they are immutable.
However, I think I have a simple solution to this problem. I have no idea what library that is or how it works, so this may or may not work. However, this is a general answer to how you would remove a prop before a component gets mounted.
That being said, I would try to create my own component which renders a <Button />:
class MyButtonComponent extends React.Component {
...
render() {
return <Button onClick={this.props.onClickHandler} />;
}
}
Then in the component you want to do your enhancements:
render() {
<AsyncContainer>
<MyButtonComponent onClickHandler={this.asyncStuff} asyncHandler="onClick"/>
</AsyncContainer>
}
This way you maintain your onClick eventlistener on the <Button /> component but you don't pass the illegal asyncHandler prop.
Edit:
Alternatively, you could also do:
class MyButtonComponent extends React.Component {
...
componentWillMount() {
let newProps = this.props;
delete newProps.asyncHandler;
this.setState({properties: newProps}):
}
render() {
return <Button {...this.state.properties} />;
}
}
This will apply all the props (with the spread operator) to <Button /> except for asyncHandler which we delete prior to the component being mounted by creating a copy of the props in state but with asyncHandler removed.
Also check this answer I gave to a similar question.
I've a React component. Some elements will be inserted through the children. Some of these elements will have a specific classname.
How can I get a list of these DOM nodes in my outermost Component?
<MyComponent>
<div classname="snap"/>
<p></p>
<div classname="snap"/>
<p></p>
<div classname="snap"/>
</MyComponent>
What I want to know is how many elements with the classname "snap" are inserted in my component.
You can achieve it, via findDOMNode of react-dom, like below:
ReactDOM.findDOMNode(<instance-of-outermost-component>).getElementsByClassName('snap') // Returns the elements
If you need the count,
ReactDOM.findDOMNode(<instance-of-outermost-component>).getElementsByClassName('snap').length
You can use ReactDOM.findDOMNode. Even though the documentation encourage using ref, let's see how it works:
findDOMNode()
ReactDOM.findDOMNode(component)
If this component has been mounted into the DOM, this returns the
corresponding native browser DOM element. This method is useful for
reading values out of the DOM, such as form field values and
performing DOM measurements. In most cases, you can attach a ref to
the DOM node and avoid using findDOMNode at all.
When a component renders to null or false, findDOMNode returns null.
When a component renders to a string, findDOMNode returns a text DOM
node containing that value. As of React 16, a component may return a
fragment with multiple children, in which case findDOMNode will return
the DOM node corresponding to the first non-empty child.
Note: findDOMNode is an escape hatch used to access the underlying DOM
node. In most cases, use of this escape hatch is discouraged because
it pierces the component abstraction. findDOMNode only works on
mounted components (that is, components that have been placed in the
DOM). If you try to call this on a component that has not been mounted
yet (like calling findDOMNode() in render() on a component that has
yet to be created) an exception will be thrown. findDOMNode cannot be
used on functional components.
Also let's look at the ref, which is recommended:
Adding a Ref to a Class Component
When the ref attribute is used on a custom component declared as a
class, the ref callback receives the mounted instance of the component
as its argument. For example, if we wanted to wrap the CustomTextInput
above to simulate it being clicked immediately after mounting:
class AutoFocusTextInput extends React.Component {
componentDidMount() {
this.textInput.focusTextInput();
}
render() {
return (
<CustomTextInput
ref={(input) => { this.textInput = input; }} />
);
}
}
Note that this only works if CustomTextInput is declared as a class:
class CustomTextInput extends React.Component {
// ...
}
Yoy can also use this.props.children to get number of child nodes with given class:
let snapCount = React.Children.toArray(this.props.children).filter((item) => item.props.className === 'snap').length;
I had a similar issue where document.getElementsByClassName was not returning what I needed. I found that using document.querySelectorAll did the trick. In terms of the code in question:
const elements = document.querySelectorAll(["classname=snap"])
const length = elements.length
Why does this work? As per https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector, document.querySelector will return the first element within the document that matches the specified selector. Therefore, document.querySelectorAll will return all elements within the document that matches the specified selector.
I know this post is quite old but hopefully the above can help someone in a similar position to me. I came to the conclusion that document.getElementsByClassName probably doesn't work with React because according to https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByClassName this is applicable to elements that have a class as opposed to a className.