Extending component event handler props in React - reactjs

I'm attempting to extend the default event handler functionality of a component that I'm "subclassing" via composition.
If I have a simple component like this:
export class Foo extends React.Component {
doSomething() {
// I want to do this first...
}
render() {
return <Bar onClick="() => this.doSomething.bind(this)" />
}
}
...and I'm attempting to extend that via composition:
export class FancyFoo extends React.Component {
doSomethingFancy() {
// ...and do this second
}
render() {
return <Foo onClick="() => this.doSomethingFancy.bind(this)" />
}
}
How can I ensure in Foo that Foo.doSomething is executed immediately before SuperFoo.doSomethingFancy is? I tried an approach like this:
export class Foo extends React.Component {
constructor(props) {
super(props);
this.doSomething = this.doSomething.bind(this);
}
doSomething() {
// do the default thing
console.log('here!'); // <-- never happens
// if "subclass" provided a callback, call it next
'function' === typeof this.props.onClick && this.props.onChange.apply(this, arguments);
}
render() {
return (
<Bar
onClick={this.doSomething}
{...this.props} />
);
}
}
...but Foo.doSomething is never called while SuperFoo.doSomethingFancy is. I'm new to React, and assuming I'm overlooking something obvious. Thanks

I resolved this by leveraging spread object destructuring in Foo.render method:
render() {
// extract FancyFoo's onClick handler
const {onClick, ...props} = this.props;
// pass only remaining props to Bar constructor, override onClick
return (
<Bar
onClick={this.doSomething}
{...props} />
);
}
...and then Foo's doSomething works as expected:
doSomething() {
// do the default thing
// ...
// this.props. onClick references FancyFoo's implementation
'function' === typeof this.props.onClick && this.props.onChange.apply(this, arguments);
}
Now Foo.doSomething is executed followed immediately by FancyFoo.doSomething.

Related

Pass 'this' object to component - Typescript / React

I'd like to call a public method from the FirstComponent in my OtherComponent. However, I'm seeing this error:
"TypeError: firstCompObj.foo() is not a function" when I click the "Submit" button. After doing some debugging, the firstCompObj isn't showing up as type FirstComponent, but as an empty object.
I know that technically "foo" is a method, not a function, but what is the difference, and is it possible to call this method from the OtherComponent? Can I pass an object like this using the "this" keyword?
"FirstComponent.tsx":
export class FirstComponent: extends React.Component<{}, {}> {
..
public foo() {
// do something
}
private _otherComonent = (): JSX.Element => {
return (
<Other firstComponentObj={this} />
);
};
}
"otherfile.tsx":
export const Other: Component = (firstCompObj: FirstComponent) => {
...
<Button text='Submit' onClick={() => {firstCompObj.foo();}} />
...
}
You want to pass a function that calls foo() rather than passing the whole FirstComponent.
Other isn't actually a component right now because it doesn't take a props object. Lets make it accept the prop doFoo which is a function that requires no arguments and returns nothing.
interface OtherProps {
doFoo: () => void;
}
export const Other: React.FC<OtherProps> = ({ doFoo }) => {
return <button onClick={doFoo}>Submit</button>;
};
We we call Other in the render method of FirstComponent, we provide that doFoo prop. It will be an anonymous function that calls this.foo on the FirstComponent instance. The arrow => binds the this context.
export class FirstComponent extends React.Component<{}, {}> {
foo() {
// do something
console.log("called foo from FirstComponent");
}
render() {
return (
<div>
<p>There might be some content here</p>
<Other doFoo={() => this.foo()} />
</div>
);
}
}
FirstComponent could easily be a function component instead of a class component, but I'm keeping it the way that you had it.

Call child method from parent class - React typescript

How do I call a child method from parent class ? or to put it simply how to assign a ref to ReactElement?
I've seen examples where ref is assigned to HTMLDivElement or HTMLInputElement, but not a ReactElement.
class Example extends React.Component<Props, State> {
...
childRef = React.createRef<React.ReactElement>();
...
next = () => {
this.childRef.someFunction();
}
render() {
<Child ref={this.childRef}/>
}
}
The above code gives me two errors:
Generic type 'ReactElement<P>' requires 1 type argument(s).
Property 'someFunction' does not exist on type 'RefObject<any>'.
The main issue is React.createRef<React.ReactElement>(). You need to change ReactElement to the type that you want, in this case Child.
One more issue in this.childRef.someFunction();. It's missing .current. then it'll be this.childRef.current.someFunction();.
Here's a full example:
Or try live demo on CodeSandbox
import * as React from "react";
import { render } from "react-dom";
interface ChildState {
lastCalled?: Date
}
class Child extends React.Component<{}, ChildState> {
state: ChildState = {};
render() {
if (!this.state.lastCalled) {
return "Not called yet";
}
return `Last called at ${this.state.lastCalled.toLocaleTimeString()}`;
}
someFunction = () => {
this.setState({
lastCalled: new Date()
});
};
}
class App extends React.Component {
childRef = React.createRef<Child>();
next = () => {
if (!this.childRef.current) {
return;
}
this.childRef.current.someFunction();
};
render() {
return (
<React.Fragment>
<Child ref={this.childRef} />
<div>
<button type="button" onClick={this.next}>
Next Call
</button>
</div>
</React.Fragment>
);
}
}
render(<App />, document.getElementById("root"));
Update -- 16 May 2019:
When I opened the CodeSandBox sample above and updated to latest dependencies, it didn't seem to like:
childRef = React.createRef<Child>();
It throws an error on the close bracket ).
To make it work, I changed it to:
childRef:React.RefObject<Child> = React.createRef();
When you use React.createRef(), the resulting object looks like {current : null}. React then assigns whatever the actual reference is to refObject.current.
So, in your example, you need this.childRef.current.someFunction().
You may also have to do some TypeScript declarations to let it know the object stored inside the ref has that function available.
I think you need to pass in a function to assign the reference variable.
private childRef: any;
private assignRef = (ref) => this.childRef = ref;
next = () => {
this.childRef.someFunction();
}
render() {
<Child ref={this.assignRef}/>
}

Sending onMouseDown from parent to child

Is there a way to send the clickevent from the parent to the child?
This is my parent component:
<Component {...props}>
<Child />
{props.children}
</Component>
This is the child component:
<Component onMouseDown={e => this.handleClick(e, props)}></Component>
Whenever the parent component is clicked I want to trigger the handleclick component of my child.
Thanks in advance!
You can use a reference to your child component:
// parent.js
constructor(props) {
super(props);
this.child = React.createRef();
}
handleMouseDown = e => {
this.child.current.handleClick(e, this.props);
}
render() {
return (
<Component onMouseDown={this.handleMouseDown} {...props}>
<Child ref={this.child}/>
{props.children}
</Component>
)
}
You can do this using rxjs with Observable and Subscriptions. Here is a working example and I'll explain what's going on https://codesandbox.io/s/7wjwnznk3j
Relevant reading:
fromEvent: https://rxjs-dev.firebaseapp.com/api/index/function/fromEvent
subscription: https://rxjs-dev.firebaseapp.com/api/index/class/Subscription
I used Typescript since I prefer it, but is absolutely not a requirement. You parent class will look like this:
interface State {
obs$?: Observable;
}
class App extends React.Component<null, State> {
public readonly state: State = {};
public ref: React.Ref<React.ReactHTMLElement>;
componentDidMount() {
this.setState({
obs$: fromEvent(this.ref, 'click')
});
}
#Bind()
setParentRef(el: HTMLElement) {
this.ref = el;
}
render() {
return (
<div style={parentStyles} ref={this.setParentRef}>
<Child parentClick={this.state.obs$} />
</div>
);
}
}
We have our ref this.ref and set it through the function, we need this since it is the target of a fromEvent and click is the event. This automatically creates an observable that will emit to any subscribers when it is clicked. You will want to pass this as a prop to your child component. Then in that component you can subscribe to it and do whatever you want when there is a click in the parent.
interface Props {
parentClick?: Observable;
}
interface State {
onClick$?: Subscription;
numClicks: number;
}
class Child extends React.Component<Props, State> {
public readonly state: State = { numClicks: 0 };
componentDidMount() {
if (this.props.parentclick) {
this.handle();
}
}
componentDidUpdate(prevProps: Props) {
if (
this.props.parentClick !== undefined &&
this.state.onClick$ === undefined
) {
this.handleSubscribe();
}
}
componentWillUnmount() {
if (this.state.onClick$) {
this.state.onClick$.unsubscribe();
}
}
handleSubscribe() {
this.setState({
onClick$: this.props.parentClick.subscribe(this.onParentClick)
});
}
#Bind()
onParentClick() {
this.setState((prevState: State) => ({
numClicks: prevState.numClicks + 1
}));
}
render() {
return (
<div style={childStyles}>
Parent clicked {this.state.numClicks} time(s)
</div>
);
}
}
So in this instance, when the parent is clicked the subscription invokes the onParentClick method. Then in that method we implement a simple counter and display it in the HTML.
One thing important thing is to ALWAYS make sure you unsubscribe from subscriptions. If you don't this will create a memory leak and will be really tricky to track down, since it is easy to overlook.

How to share a property with React components?

I'm new to React and I have a question about sharing properties from one component to another. For example, I want a parent component that has a "visible" function that I can pass to other child components.
Example:
CustomInput visible="true";
CustomDropDown visible="false"
I'd like to know the best way to do this, respecting good practices. Thank you for your help!
Real simple. You can pass methods as props. Suppose you have a parent, or Higher Order Component (HOC), you could do something like this:
class Parent extends React.Component {
logWord = (word) => {
console.log(word);
}
render () {
return <ChildComponent handleLogging={ this.logWord } />
}
}
Then, in the ChildComponent, you simply access the method from props. For instance:
class ChildComponent extends React.Component {
render () {
return (
<div onClick={ this.props.handleLog.bind(null, 'Logged!') }>Click me to log a word!</div>
}
}
}
So, in your example, if you wanted a method that existed on the parent that updated a visibility attribute on your state, you could write:
class Parent extends React.Component {
constructor () {
this.state = {
visible: false
}
}
setVisible = (bool) => {
this.setState({ visible: bool });
}
render () {
return <ChildComponent updateVisible={ this.setVisible } visible={ this.state.visible } />;
}
}
ChildComponent:
class ChildComponent extends React.Component {
render () {
return (
<div>
<div onClick={ this.props.updateVisible.bind(null, true) }>Set me to visible!</div>
<div onClick={ this.props.updateVisible.bind(null, false) }>Set me to invisible!</div>
{ this.props.visible && <div>I'm visible right now!</div> }
</div>
}
}
}

Context lost in parent component when called from child

I've got a parent component which feeds a onSomeAction prop to a child component:
export default class myComponent extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="mycomponent">
<ChildComponent onSomeAction={this.doSomething} />
</div>
)
}
doSomething() {
console.log(this);
}
}
In the child component when something is clicked on I'm calling a method whiih in turns calls the onSomeAction prop:
export default class ChildComponent extends Component {
render() {
return (
<div className="">
<a onClick={() => this.doIt()}>doIt</a>
</div>
)
}
doIt() {
const { onSomeAction } = this.props;
onSomeAction();
}
}
The problem I'm seeing is back in the parent component the this context seems to have been lost - the console.log in the doSomething method returns undefined. Why is this? I need to be able to access
the parent component context.
You need set context in parent component, you can do it with .bind
<ChildComponent onSomeAction={ this.doSomething.bind(this) } />
^^^^^^^^^^^^
Example
There are two options for you on how you can get the element that has been clicked or the whole component scope this.
option one:
instead of logging this you should logg the event target like so:
doSomething(e) {
console.log(e.target);
}
option two:
you have to attach the this keyword to the doSomething method like so:
constructor(props) {
super(props);
this.doSomething = this.doSomething.bind(this);
}
That was you'll be able to access the keyword this in your do something method.
Option3:
If you want to refer with this to the child component then you have to bind(this) on the function call int he child component
Actually you can fix it 3 ways:
<ChildComponent onSomeAction={ this.doSomething.bind(this) } />
<ChildComponent onSomeAction={ () => this.doSomething() } />
<ChildComponent onSomeAction={this.doSomething} />
and add this to constructor: this.doSomething = this.doSomething.bind(this)

Resources