How to use React.useRef() in class component? - reactjs

I need this code to be implemented in class component.
It is in order to use a upload progress in my class component with react-toastify
function Example(){
const toastId = React.useRef(null);
function handleUpload(){
axios.request({
method: "post",
url: "/foobar",
data: myData,
onUploadProgress: p => {
const progress = p.loaded / p.total;
if(toastId.current === null){
toastId = toast('Upload in Progress', {
progress: progress
});
} else {
toast.update(toastId.current, {
progress: progress
})
}
}
}).then(data => {
toast.done(toastId.current);
})
}
return (
<div>
<button onClick={handleUpload}>Upload something</button>
</div>
)
}
How can i do that?

useRef() is among react hooks which are meant to be used in Functional components. But if you want to create a reference in a class-based component, you can do it from the class constructor like the code below:
constructor(props) {
super(props);
this.myRef = React.createRef();
}
Check React.createRef().

Assign your value in constructor i.e bind with this.
createRef !== useRef, useRef is used to preserve the value across re-renders and for that in class component you need to bind it with this not createRef

Related

ReadOnly state when I need to change it

I have the following code and I really need to be able to change the state however I am having issues when I try and do the following.
export default class Mediaplayer extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: [],
station: null,
playButton: false,
muteButton: false,
};
}
render() {
const { station, playButton, muteButton } = this.state;
const handleMClick = (e) => {
// Event("Play Button", "Listner Hit Play", "PLAY_BUTTON");
console.log("clicking the play and pause button");
this.setState({ playButton: !playButton });
playButton
? document.getElementById("player").play()
: document.getElementById("player").pause();
};
return (
<i onClick={handleMClick}>
{playButton ? <PlayCircle size={60} /> : <PauseCircle size={60} />}
</i>
);
}
}
I am getting this state is ReadOnly.
setState() only takes effect after the whole eventHandler is
finished, this is called state batching.
Your this.setState({playButton:!playButton}) only run after handleMClick() is finished.
In other words, playButton === true will not available within your handleMClick() function.
On solution could be to put this:
playButton ? document.getElementById("player").play() : document.getElementById("player").pause()
Inside a componentDidUpdate() so it will take effect in the next render after your state is updated.
Direct dom manipulation is not a recommended way of doing things in react because you can always change dom element state according to your react component state or props.
I see your component is called media player but it doesn't have the #player inside it? Perhaps you could reconsider how you arranging the dom element.
Also try to use a functional component instead of class component. I will give an answer with a functional component.
MediaPlayer Component
import { useState } from 'react';
const MediaPlayer = props => {
const [play, setPlay] = useState(false);
const togglePlay = () => {
setPlay( !play );
}
return (
<i onClick={togglePlay}>
{!play ?
<PlayCircle size={60}/>
:
<PauseCircle size={60}/>}</i>
}
);
}

How to update state just after rendering

I have the following component:
import React from 'react';
import './styles.css';
import ToolTip from '../../Common/components/ToolTip/ToolTip';
export default class RouteTitleTooltipComponent extends React.Component {
constructor(props) {
super(props);
this.titleParagraphRef = React.createRef();
this._tooltipTimer = null;
this.state = { shouldPopupBeEnabled: false, isTooltipShown: false };
this._showTooltip = this._showTooltip.bind(this);
this._hideTooltip = this._hideTooltip.bind(this);
}
componentDidMount() {
const { scrollWidth, clientWidth } = this.titleParagraphRef.current;
const shouldPopupBeEnabled = scrollWidth > clientWidth;
this.setState({ shouldPopupBeEnabled });
}
_showTooltip() {
this._tooltipTimer = setTimeout(
() => {
this.setState({ isTooltipShown: true });
}, 1000,
);
}
_hideTooltip() {
clearTimeout(this._tooltipTimer);
this.setState({ isTooltipShown: false });
}
render() {
const { shouldPopupBeEnabled, isTooltipShown } = this.state;
const { message } = this.props;
return (
<ToolTip
message="Tooltip!!"
popoverOpen={shouldPopupBeEnabled && isTooltipShown}
>
<div
ref={this.titleParagraphRef}
onMouseOver={this._showTooltip}
>
{message}
</div>
</ToolTip>
);
}
}
This basically renders a floating tooltip over a div element if the message inside of it is bigger than the container. To do that, I use scrollWidth and clientWidth of the div element using a React reference. To detect those values I use componentDidMount, but this only works in full renders of the component. That is, if I have the component visible and reload the page, both values are equal to 0 and it does not work.
In addition, if I change the message, it does not work either because the component is already mounted.
So what I want is to change the state right after the component is mounted or updated so that the react reference is rendered and clientWidth and scrollWidth are not 0.
I have tried replace componentDidUpdate instead of componentDidMount but it's not a good practica to use setState inside componentDidUpdate.
Any solution?
First you should know that componentDidMount will execute only once. Therefor you can go for componentDidUpdate but don't forget to put a condition as it will render in a loop.
componentDidUpdate(prevProps,prevState) {
const shouldPopupBeEnabled = scrollWidth > clientWidth;
if (shouldPopupBeEnabled !== this.state.shouldPopupBeEnabled ) {
this.setState({shouldPopupBeEnabled });
}
}
Or you can go for functional components and use useEffect which will only render again if state changes.
useEffect(() => {
console.log('mounted');
}, [shouldPopupBeEnabled]) // It will re render id `shouldPopupBeEnabled` changes

React hooks component did update with prevProps

I was converting my react code from class to hooks
I previously had something like this
export default class Editor extends React.Component {
constructor(props) {
super(props)
this.state = this.getStateObject()
}
getStateObject() {
const { page } = this.props
return {
label: page.label || '',
session: page.session || false
}
}
componentDidUpdate(prevProps) {
if (!_.isEqual(this.props.projects, prevProps.projects)) {
this.setState(this.getStateObject())
}
}
When Trying to move to functional, I did something like this
export default function tabEditor ({page}:Page) {
const [appPageInfo, setAppPageInfo] = useState({
label: page.label || '',
session: page.session || false
})
/* Equivalence in hooks
componentDidUpdate(prevProps) {
if (!_.isEqual(this.props.projects, prevProps.projects)) {
this.setState(this.getStateObject())
}
}
*/
const handleOnClickUpdate = () => {
updateMobileAppPage(Object.assign({}, page, appPageInfo))
close()
}
but I am unable to determine the equivalence of componentDidUpdate in React hooks.
You could use the useEffect hook add pass it the projects props as dependency like this :
useEffect(() => {
// Whatever code you want to run if props.projects change
}, [props.projects]);
To mimic componentDidUpdate in Hooks, you make use of useEffect and then pass in the conditional parameter with which useEffect will run whenever the parameter changes. Example below
useEffect(() => {
.......
//This useEffect will re-run whenever the parameter in the square bracket updates/changes.
}, [parameter])

How to get the DOM node from a Class Component ref with the React.createRef() API

I have these two components:
import { findDOMNode } from 'react-dom';
class Items extends Component {
constructor(props) {
super(props);
this.ref = React.createRef();
this.selectedItemRef = React.createRef();
}
componentDidMount() {
if (this.props.selectedItem) {
this.scrollToItem();
}
}
componentWillReceiveProps(nextProps) {
if (this.props.selectedItem !== nextProps.selectedItem) {
this.scrollToItem();
}
}
scrollToItem() {
const itemsRef = this.ref.current;
const itemRef = findDOMNode(this.selectedItemRef.current);
// Do scroll stuff here
}
render() {
return (
<div ref={this.ref}>
{this.props.items.map((item, index) => {
const itemProps = {
onClick: () => this.props.setSelectedItem(item.id)
};
if (item.id === this.props.selectedItem) {
itemProps.ref = this.selectedItemRef;
}
return <Item {...itemProps} />;
})}
</div>
);
}
}
Items.propTypes = {
items: PropTypes.array,
selectedItem: PropTypes.number,
setSelectedItem: PropTypes.func
};
and
class Item extends Component {
render() {
return (
<div onClick={() => this.props.onClick()}>item</div>
);
}
}
Item.propTypes = {
onClick: PropTypes.func
};
What is the proper way to get the DOM node of this.selectedItemRef in Items::scrollToItem()?
The React docs discourage the use of findDOMNode(), but is there any other way? Should I create the ref in Item instead? If so, how do I access the ref in Items::componentDidMount()?
Thanks
I think what you want is current e.g. this.selectedItemRef.current
It's documented on an example on this page:
https://reactjs.org/docs/refs-and-the-dom.html
And just to be safe I also tried it out on a js fiddle and it works as expected! https://jsfiddle.net/n5u2wwjg/195724/
If you want to get the DOM node for a React Component I think the preferred way of dealing with this is to get the child component to do the heavy lifting. So if you want to call focus on an input inside a component, for example, you’d get the component to set up the ref and call the method on the component, eg
this.myComponentRef.focusInput()
and then the componentRef would have a method called focusInput that then calls focus on the input.
If you don't want to do this then you can hack around using findDOMNode and I suppose that's why it's discouraged!
(Edited because I realized after answering you already knew about current and wanted to know about react components. Super sorry about that!)

getting error: Cannot read property state of undefined

import React, { Component } from "react";
import FormUpdate from "../components/formUpdate";
import { fetchClothingItem, updateClothingItem } from "../actions/crud";
export default class Update extends Component {
constructor(props) {
super(props);
this.state = {
updateClothingItem: {}
};
}
componentWillMount() {
fetchClothingItem(this.props.match.params.postId)
.then(data => {
this.setState(state => {
state.updateClothingItem = data;
return state;
});
console.log("data", data);
//HERE IT IS RETURNING EXPECTED DATA
console.log("this.state.updateClothingItem",this.state.updateClothingItem)
})
.catch(err => {
console.error("err", err);
});
}
handleSubmit(data) {
//HERE IT IS THROWING:
> "TypeError: Cannot read property 'state' of undefined"
console.log("this.state.updateClothingItem", this.state.updateClothingItem);
updateClothingItem(this.state.updateClothingItem.id, data); this.props.router.push("/update");
}
render() {
return (
<div>
<FormUpdate
//onSubmit={this.handleSubmit.bind(this)}
id={this.state.updateClothingItem.id}
name={this.state.updateClothingItem.name}
sleeveLength={this.state.updateClothingItem.sleeveLength}
fabricWeight={this.state.updateClothingItem.fabricWeight}
mood={this.state.updateClothingItem.body}
color={this.state.updateClothingItem.color}
/>
<button
type="submit"
onClick={this.handleSubmit}
className="addItemButton"
>
Button
</button>
</div>
);
}
}
There are a few things that are technically wrong in terms of React code implementation.
Firstly, With ES6 style of writing a class, any function that needs to access the Class properties need to be explicitly binded. In your case you need to bind the handleSubmit function using arrow function of or binding in constructor.
See this answer for more details: Why and when do we need to bind functions and eventHandlers in React?
Secondly: You have your async request set up in the componentWillMount function and in the success response of it, you are setting state. However using setState in componentWillMount is triggered after the component is rendered so you still need to have an undefined check. You should instead make use of componentDidMount lifecycle function for async requests.
Check this answer on whether to have AJAX request in componentDidMount or componentWillMount
Third: setState is asynchronous and hence logging the state values after the setState function won't result in the correct output being displayed. Use the setState callback instead.
See these answers for more details:
calling setState doesn't mutate state immediately
When to use React setState callback
Code:
export default class Update extends Component {
constructor(props) {
super(props);
this.state = {
updateClothingItem: {}
};
}
componentDidMount() {
fetchClothingItem(this.props.match.params.postId)
.then(data => {
this.setState(state => {
state.updateClothingItem = data;
return state;
});
console.log("data", data);
//HERE IT IS RETURNING EXPECTED DATA
console.log("this.state.updateClothingItem",this.state.updateClothingItem)
}) // this statement will not show you correct result since setState is async
.catch(err => {
console.error("err", err);
});
}
handleSubmit = (data) => { . // binding using arrow function here
console.log("this.state.updateClothingItem", this.state.updateClothingItem);
updateClothingItem(this.state.updateClothingItem.id, data); this.props.router.push("/update");
}
render() {
return (
<div>
<FormUpdate
//onSubmit={this.handleSubmit.bind(this)}
id={this.state.updateClothingItem.id}
name={this.state.updateClothingItem.name}
sleeveLength={this.state.updateClothingItem.sleeveLength}
fabricWeight={this.state.updateClothingItem.fabricWeight}
mood={this.state.updateClothingItem.body}
color={this.state.updateClothingItem.color}
/>
<button
type="submit"
onClick={this.handleSubmit}
className="addItemButton"
>
Button
</button>
</div>
);
}
}
You forgot to bind your handleSubmit function to the class. You can either use arrow function to define the function.
handleSubmit=(data) =>{
...
}
Or you can bind the function in your constructor.
constructor(props) {
super(props);
this.state = {
updateClothingItem: {}
};
this.handleSubmit= this.handleSubmit.bind(this,data);
}
there is no state in constructor yet
if you want to set state in constructor you can do it like this
class SomeComponent extends Component {
constructor(props){
super(props)
this.state = { someKey: someValue }
}
}
or even like this
class SomeComponent extends Component {
state = { someKey: someValue }
}
but in this case babel should be properly configured

Resources