React Intercept component unmounting (Functional and Class Components) - javascript

I need to always intercept when React unmounts a Component, no matter if that is a Functional or Class based component.
Here is my case:
function observe(component) {
const p = component.type.prototype;
const delegate = p.componentWillUnmount || function noop() {};
if(!delegate.__decorated) {
p.componentWillUnmount = function() {
console.log('I am going to be unmounted');
return delegate.apply(this, arguments);
}
p.componentWillUnmount.__decorated = true;
}
return component;
}
class Comp extends React.Component {
render() {
return (<h1>Hello World</h1>);
}
}
class App extends React.Component {
render() {
const active = this.state && this.state.active;
const toggle = () => this.setState({
active: !active,
});
return (
<div>
<button onClick={toggle}>Toggle</button>
<hr />
{active && observe(<Comp />)}
</div>
);
}
}
Now, as you can easily see, I am able to hook on every time <Comp /> gets unmounted. That is just what I need.
Things will dramatically change when that <Comp /> is a functional component:
function observe(component) {
const p = component.type.prototype;
const delegate = p.componentWillUnmount || function noop() {};
if(!delegate.__decorated) {
p.componentWillUnmount = function() {
console.log('I am going to be unmounted');
return delegate.apply(this, arguments);
}
p.componentWillUnmount.__decorated = true;
}
return component;
}
function Comp() {
return (<h1>Hello World</h1>);
}
class App extends React.Component {
render() {
const active = this.state && this.state.active;
const toggle = () => this.setState({
active: !active,
});
return (
<div>
<button onClick={toggle}>Toggle</button>
<hr />
{active && observe(<Comp />)}
</div>
);
}
}
So, my question is:
How can I hook on functional components?
I can change approach (or use React internal Apis), I just need to always intercept changes on a component passed as arguments for observe.

You can't. Functional components don't have lifecycles (yet).
Instead of messing with the functional component directly, why don't you just wrap the functional component in a class with a HOC. You could use recompose toClass for this.
function observe(component) => {
const classComponent = toClass(component):
const p = classComponent.type.prototype;
const delegate = p.componentWillUnmount || function noop() {};
if(!delegate.__decorated) {
p.componentWillUnmount = function() {
console.log('I am going to be unmounted');
return delegate.apply(this, arguments);
}
p.componentWillUnmount.__decorated = true;
}
return classComponent;
}
Or just copy the code from here.

Related

React context reset consumer state

I m trying to use React Context API to pass state data from a Parent component to a deep nested Child.
The parent is a class component and the child is a function.
When from the child i update the parent state with a function passed through the Context the parent state is updated successfully but then the local state of the child is reset to the initial value.
Parent:
export class ObjectLinking extends Component {
constructor(props) {
super(props);
this.state = {
setCurrentlyDisplayed: this.setCurrentlyDisplayed,
currentlyDisplayed: []
};
}
[...]
render() {
return (
<ObjectLinkingContext.Provider value={this.state}>
{//Panel body will contain deep nested child that is connected to context}
<PanelBody />
</ObjectLinkingContext.Provider>
);
}
}
Child:
import ObjectLinkingContext from '../../context/ObjectLinkingContext';
const AssetListFilters = ({ assets, filtersModel }) => {
const [searchByNameVal, setSearchByNameVal] = useState([]);
const panelContext = useContext(ObjectLinkingContext);
useEffect(() => {
filterBySearchNameVal();
}, [searchByNameVal]);
const filterBySearchNameVal = () => {
if (searchByNameVal.length) {
const { setCurrentlyDisplayed } = panelContext;
const { value: searchedId } = searchByNameVal[0];
const searchedAsset = assets.filter(asset => asset.assetId === searchedId) || [];
setCurrentlyDisplayed(searchedAsset);
}
};
return (
<Autocomplete
onChange={val => setSearchByNameVal(val)}
/>
);
};
Find here the full Parent.js and VeryDeepChild.js component

React append component programmatically

I want to create a react component instance and render it in a static place programmatically.
My use-case is that I open a sequence of dialogs in an unknown length and when I get a response from a dialog I open the next.
I want to do something like:
const DialogExample = () => ({ question, onAnswer }) =>
(<div>
{question}
<button onClick={onAnswer}>answer</button>
</div>);
class SomeComponent extends React.Component {
async start() {
const questions = await getSomeDynamicQuestions();
this.ask(questions);
}
ask(questions) {
if (questions.length === 0) {
// DONE.. (do something here)
return;
}
const current = questions.pop();
React.magicMethod(
// The component I want to append:
<DialogExample
question={current}
onAnswer={() => this.ask(questions)}
/>,
// Where I want to append it:
document.getElementsByTagName('body')[0]);
}
render() {
return (
<div>
<button onClick={this.start}>start</button>
</div>);
}
}
I know that's not very "react-like", and I guess the "right" way of doing it will be storing those questions in state and iterate over them in "someComponent" (or other) render function, but still, I think that this pattern can make sense in my specific need.
Sounds like a case for Portals. I'd recommend doing something like this:
class SomeComponent extends React.Component {
constructor(props) {
super(props);
this.body = document.getElementsByTagName('body')[0];
this.state = {
questions: [],
}
}
async start() {
const questions = await getSomeDynamicQuestions();
this.setState({ questions });
}
nextQuestion() {
this.setState(oldState => {
const [first, ...rest] = oldState.questions;
return { questions: rest };
})
}
render() {
const { questions } = this.state;
return (
<div>
<button onClick={this.start}>start</button>
{questions.length > 0 && ReactDOM.createPortal(
<DialogExample
question={questions[0]}
onAnswer={() => this.nextQuestion()}
/>,
this.body,
)}
</div>
);
}
}

Handling event on array item won't work - ReactJS

I try to map an array and put click event on the array items. I know it's a bit different because of how JavaScript handles functions but I can't make it work. I get the error: Cannot read property 'saveInStorage' of undefined. Can anyone tell me what I'm doing wrong? Thanks in advance! Here is my code:
import React from "react";
const data = require('../data.json');
export default class Gebruikers extends React.Component {
constructor() {
super();
this.state = {
users: data.users
};
this.saveInStorage = this.saveInStorage.bind(this)
}
saveInStorage(e){
console.log("test");
}
renderUser(user, i) {
return(
<p key={i} onClick={this.saveInStorage(user)}>f</p>
);
}
render() {
return (
<div>
{
this.state.users.map(this.renderUser)
}
</div>
);
}
}
this is undefined in renderUser()
You need to bind this for renderUser() in your constructor.
Also, you are calling saveInStorage() every time the component is rendered, not just onClick, so you'll need to use an arrow function in renderUser
import React from "react";
const data = require('../data.json');
export default class Gebruikers extends React.Component {
constructor() {
super();
this.state = {
users: data.users
};
this.saveInStorage = this.saveInStorage.bind(this);
this.renderUser = this.renderUser.bind(this);
}
saveInStorage(e){
console.log("test");
}
renderUser(user, i) {
return(
<p key={i} onClick={() => this.saveInStorage(user)}>
);
}
render() {
return (
<div>
{
this.state.users.map(this.renderUser)
}
</div>
);
}
}
Instead of binding you can also use an arrow function (per mersocarlin's answer). The only reason an arrow function will also work is because "An arrow function does not have its own this; the this value of the enclosing execution context is used" (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions). The enclosing execution in your case is your render, where this is defined.
You need to make two changes to your code which are outlined below.
You are invoking the function when the component is rendered. To fix this update this line to the following
<p key={i} onClick={() => this.saveInStorage(user)}>
This means that the function will only be invoked when you click on the item.
You also need to bind the renderUser in your constructor or else use an arrow function.
this.renderUser = this.renderUser.bind(this);
See working example here.
Your onClick event handler is wrong.
Simply change it to:
onClick={() => this.saveInStorage(user)}
Don't forget to also bind renderUser in your constructor.
Alternatively, you can choose arrow function approach as they work the same as with bind:
class Gebruikers extends React.Component {
constructor(props) {
super(props)
this.state = {
users: [{ id: 1, name: 'user1' }, { id: 2, name: 'user2' }],
}
}
saveInStorage = (e) => {
alert("test")
}
renderUser = (user, i) => {
return(
<p key={i} onClick={() => this.saveInStorage(user)}>
{user.name}
</p>
)
}
render() {
return (
<div>{this.state.users.map(this.renderUser)}</div>
);
}
}
ReactDOM.render(
<Gebruikers />,
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>
Paul Fitzgeralds answer is the correct one, although I'd like to propose a different way of handling this, without all the binding issues.
import React from "react";
const data = require('../data.json');
export default class Gebruikers extends React.Component {
constructor() {
super();
this.state = {
users: data.users
};
}
saveInStorage = (e) => {
console.log("test");
};
render() {
return (
<div>
{this.state.users.map((user, i) => {
return (<p key={i} onClick={() => this.saveInStorage(user)}>f</p>);
})}
</div>
);
}
}
With saveInStorage = (e) => {}; you are binding the saveInStorage function to the this context of your class. When invoking saveInStorage you'll always have the (at least I guess so in this case) desired this context.
The renderUser function is basically redundant. If you return one line of JSX, you can easily do this inside your render function. I think it improves readability, since all your JSX is in one function.
You are not sending the parameters to this.renderUser
this.state.users.map((user, i) => this.renderUser(user, i))
Also your onClick function should be slightly changed. Here's the full code changed:
import React from "react";
const data = require('../data.json');
export default class Gebruikers extends React.Component {
constructor() {
super();
this.state = {
users: data.users
};
this.saveInStorage = this.saveInStorage.bind(this)
}
saveInStorage(e){
console.log("test");
}
renderUser(user, i) {
return(
<p key={i} onClick={() => this.saveInStorage(user)}>f</p>
);
}
render() {
return (
<div>
{
this.state.users.map((user, i) => this.renderUser(user, i))
}
</div>
);
}
}

Pass state updater clickHandler in React

I have a React app like:
Main.js-
import React, { Component } from 'react';
import _ from 'underscore';
import ApplicationsButtons from '../components/ApplicationsButtons';
let applications_url = 'http://127.0.0.1:8889/api/applications'
export default class Main extends Component {
constructor(props) {
super(props);
this.state = {applications: [], selected_app: 1};
this.updateSelectedApp = this.updateSelectedApp.bind(this);
}
componentDidMount() {
let self = this;
$.ajax({
url: applications_url,
method: 'GET',
success: function(data) {
console.log(data);
let objects = data.objects;
let apps = objects.map(function(object) {
return {name: object.name, id: object.id};
});
console.log(apps);
self.setState({applications: apps});
}
});
}
updateSelectedApp(id) {
this.setState({selected_app: id});
}
render() {
return (
<div>
{this.state.selected_app}
<ApplicationsButtons apps={this.state.applications} />
</div>
);
}
}
ApplicationsButtons.js-
import React, { Component } from 'react';
export default class ApplicationsButtons extends Component {
render() {
var buttons = null;
let apps = this.props.apps;
let clickHandler = this.props.clickHandler;
if (apps.length > 0) {
buttons = apps.map(function(app) {
return (<button key={app.id}>{app.name} - {app.id}</button>);
// return (<button onClick={clickHandler.apply(null, app.id)} key={app.id}>{app.name} - {app.id}</button>);
});
}
return (
<div>
{buttons}
</div>
);
}
}
I want to pass an onClick to the buttons that will change the currently selected app. Somehow, I just got my first infinite loop in React ("setState has just ran 20000 times"). Apparently, when I tried to pass the event handler to be called on click, I told it to keep calling it.
The onClick function should change state.selected_app for the Main component, based on the id for the button that was clicked.
You are not passing the handler as prop.
Here's what you should do:
render() {
return (
<div>
{this.state.selected_app}
<ApplicationsButtons
apps={this.state.applications}
handleClick={this.updateSelectedApp}
/>
</div>
);
}
And in ApplicationButtons:
render() {
var buttons = null;
let apps = this.props.apps;
let clickHandler = this.props.handleClick;
if (apps.length > 0) {
buttons = apps.map(app =>
<button key={app.id} onClick={() => clickHandler(app.id)}>{app.name} - {app.id}</button>);
);
}
return (
<div>
{buttons}
</div>
);
}

Indeterminate checkbox in React JSX

How do I render an indeterminate checkbox via JSX?
Here's what I've tried:
function ICB({what}) {
return <input type="checkbox"
checked={what === "checked"}
indeterminate={what === "indeterminate"} />;
}
However, indeterminate is not an attribute on the HTMLElement, but a property. How do I set properties from React / JSX?
Solution:
As most of the answers below use findDOMNode or string refs, both of which are no longer considered good practice in React, I've written a more modern implementation:
function ICB() {
const [state, setState] = React.useState(0);
const indetSetter = React.useCallback(el => {
if (el && state === 2) {
el.indeterminate = true;
}
}, [state]);
const advance = () => setState(prev => (prev + 1) % 3);
return <input type="checkbox"
checked={state === 1}
ref={indetSetter}
onClick={advance} />;
}
ReactDOM.render(<ICB />, document.getElementById("out"));
<div id="out"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
You can also use the ref function directly:
ReactDOM.render(
<label>
<input
type="checkbox"
ref={input => {
if (input) {
input.indeterminate = true;
}
}}
/>
{' '}
Un test
</label>,
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>
I would probably create a composite component that encapsulates the necessary hooks to set or unset the checkbox's indeterminate property. It looks like you're using ES2015 syntax, so I'll use some of those features here.
class IndeterminateCheckbox extends React.Component {
componentDidMount() {
if (this.props.indeterminate === true) {
this._setIndeterminate(true);
}
}
componentDidUpdate(previousProps) {
if (previousProps.indeterminate !== this.props.indeterminate) {
this._setIndeterminate(this.props.indeterminate);
}
}
_setIndeterminate(indeterminate) {
const node = React.findDOMNode(this);
node.indeterminate = indeterminate;
}
render() {
const { indeterminate, type, ...props } = this.props;
return <input type="checkbox" {...props} />;
}
}
// elsewhere
render() {
return <IndeterminateCheckbox
checked={this.props.state === "checked"}
indeterminate={this.props.state === "indeterminate"} />
}
Working example: https://jsbin.com/hudemu/edit?js,output
You can use the componentDidMount step (which is invoked after the initial rendering) to set that property:
componentDidMount() {
React.findDOMNode(this).indeterminate = this.props.state === "indeterminate";
}
If you want that property to be updated with subsequent renders, do the same thing in componentDidUpdate also.
I'd suggest creating a simple component (code ported from coffeescript so mind you, might have some simple typos):
const React = require('react');
module.exports = class IndeterminateCheckbox extends React.Component {
componentDidMount() {
this.refs.box.indeterminate = this.props.indeterminate;
}
componentDidUpdate(prevProps, prevState) {
if(prevProps.indeterminate !== this.props.indeterminate) {
this.refs.box.indeterminate = this.props.indeterminate;
}
}
render() {
return <input {...this.props} ref="box" type="checkbox"/>;
}
}
Now you have a simple component that behaves exactly like a checkbox, that supports the indeterminate prop. Note there's plenty of room for improvements here, namely setting propTypes and proper defaults for some props, and of course implementing componentShouldUpdate to only do something when needed.
An alternative would be to use a ref attribute with a callback to set the property on the DOM node. For example:
render: function() {
return (
<input
type="checkbox"
ref={function(input) {
if (input != null) {
React.findDOMNode(input).indeterminate = this.props.indeterminate;
}}
{...this.props} />
)
}
Dont use React.findDOMNode(this).
It is risky.
export class SelectAll extends Component {
constructor(props) {
super(props);
this.state = {
checked: false
};
this.myRef = React.createRef();
this.onChange = this.onChange.bind(this);
this.update = this.update.bind(this);
}
onChange(e) {
const checked = e.target.checked;
this.setState({
checked: checked
});
this.selectAllNode.indeterminate = false;
}
update(state: {
checked: Boolean,
indeterminate: Boolean
}) {
this.setState({
checked: state.checked
});
this.myRef.current.indeterminate = state.indeterminate;
}
render() {
return ( <
input type = "checkbox"
name = "selectAll"
checked = {
this.state.checked
}
onChange = {
this.onChange
}
ref = {
this.myRef
}
/>
);
}
}
React v15 implementation:
import React from 'react';
export default class Checkbox extends React.Component {
componentDidMount() {
this.el.indeterminate = this.props.indeterminate;
}
componentDidUpdate(prevProps, prevState) {
if(prevProps.indeterminate !== this.props.indeterminate) {
this.el.indeterminate = this.props.indeterminate;
}
}
render() {
const {indeterminate, ...attrs} = this.props;
return <input ref={el => {this.el = el}} type="checkbox" {...attrs}/>;
}
}
Taken from my tutorial which shows how this works with the recent React features. I hope this helps someone who stumbles upon this older question:
const App = () => {
const [checked, setChecked] = React.useState(CHECKBOX_STATES.Empty);
const handleChange = () => {
let updatedChecked;
if (checked === CHECKBOX_STATES.Checked) {
updatedChecked = CHECKBOX_STATES.Empty;
} else if (checked === CHECKBOX_STATES.Empty) {
updatedChecked = CHECKBOX_STATES.Indeterminate;
} else if (checked === CHECKBOX_STATES.Indeterminate) {
updatedChecked = CHECKBOX_STATES.Checked;
}
setChecked(updatedChecked);
};
return (
<div>
<Checkbox
label="Value"
value={checked}
onChange={handleChange}
/>
<p>Is checked? {checked}</p>
</div>
);
};
const Checkbox = ({ label, value, onChange }) => {
const checkboxRef = React.useRef();
React.useEffect(() => {
if (value === CHECKBOX_STATES.Checked) {
checkboxRef.current.checked = true;
checkboxRef.current.indeterminate = false;
} else if (value === CHECKBOX_STATES.Empty) {
checkboxRef.current.checked = false;
checkboxRef.current.indeterminate = false;
} else if (value === CHECKBOX_STATES.Indeterminate) {
checkboxRef.current.checked = false;
checkboxRef.current.indeterminate = true;
}
}, [value]);
return (
<label>
<input ref={checkboxRef} type="checkbox" onChange={onChange} />
{label}
</label>
);
};

Categories