Using refs with conditional rendering - javascript

I have a problem with ref and conditional rendering.
I would like to focus an input tag when I click on a button tag.
Basically, I have this simplified code.
class App extends React.Component {
textInput
constructor(props) {
super(props)
this.state = {isEditing: false}
this.textInput = React.createRef()
}
onClick = () => {
this.setState({isEditing: !this.state.isEditing})
this.textInput.current.focus();
}
render () {
let edit = this.state.isEditing ?
(<input type="text" ref={this.textInput} />)
: ""
return (
<div>
<button onClick={this.onClick}>lorem </button>
{edit}
</div>
);
}
}
When I click on the button, the input tag is displayed but the ref textInput is still set to null. Thus I can't focus the input.
I found some workaround like:
set autoFocus property in the input tag
hide the input tag with css when isEditing == false
But actually it is a very basic pattern and I would like to know if there is a clean solution.
Thank you

TL;DR:
Change this:
this.setState({isEditing: !this.state.isEditing})
this.textInput.current.focus();
to this:
this.setState(previousState => ({isEditing: !previousState.isEditing}), () => {
this.textInput.current.focus();
});
Update: Functional Components / Hooks
It's been asked in the comments how to do this with useState and functional components. Rafał Guźniczak's answer explains it, but I wanted to provide a bit more explanation and a runnable example.
You still don't want to read state immediately after setting it, but instead of using a second argument callback to setState, you need to run some code after the state is updated and the component has re-rendered. How do we do that?
The answer is useEffect. The purpose of effects are to synchronize external "things" (for example: imperative DOM things like focus) with React state:
const { useEffect, useRef, useState } = React;
const { render } = ReactDOM;
function App(props) {
const [isEditing, setIsEditing] = useState(false);
const textInputRef = useRef(null);
const toggleEditing = () => setIsEditing(val => !val);
// whenever isEditing gets set to true, focus the textbox
useEffect(() => {
if (isEditing && textInputRef.current) {
textInputRef.current.focus();
}
}, [isEditing, textInputRef]);
return (
<div>
<button onClick={toggleEditing}>lorem </button>
{isEditing && <input type="text" ref={textInputRef} />}
</div>
);
}
render(
<App />,
document.getElementById('root')
);
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
Details:
You're running into a common problem many people run into with React, which is the assumption that setting state is synchronous. It's not. When you call setState, you're requesting that React update the state. The actual state update happens later. This means that immediately after the setState call, the edit element hasn't been created or rendered yet, so the ref points to null.
From the docs:
setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state. This is the primary method you use to update the user interface in response to event handlers and server responses.
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied.

Thank a lot for your answer #rossipedia. I was wondering if I can do it with hooks.
And apparently you can't pass second parameter to useState setter as in setState. But you can use useEffect like this (note second parameter in useEffect):
const [isEditing, setIsEditing] = React.useState(false);
React.useEffect(() => {
if (isEditing) {
textInput.current.focus();
}
}, [isEditing]);
const handleClick = () => setIsEditing(isEditing);
And it worked! ;)
Source: https://www.robinwieruch.de/react-usestate-callback/

Related

How to avoid unnecessary re-render in other child when I only make change in specific child

In my react project
I have three components
One is called BigForm
The other two are called Form A and Form B.
In BigForm, there are two state, DataA and DataB, which would be passed into FormA and FormB.
Question:
Whenever I add input value into Form A or Form B, it will also trigger unnecessary render on the other components.
How can I avoid it? Example code would be appreciated. (please also see my edit before trying) thanks ; I heard redux may help, but I am not sure how to put this in work in this example
import React, {useState, useEffect} from "react";
const FormA = (props) => {
useEffect(()=>{ console.log('Form A was just rendered')})
const { dataA, setDataA } = props;
return (
<div>
<input onChange={(e) => setDataA(e.target.value)}></input>
<p>Input Form A{dataA}</p>
</div>
);
};
const FormB = (props) => {
const { dataB, setDataB } = props;
useEffect(()=>{ console.log('Form B was just rendered')})
return (
<div>
<input onChange={(e) => setDataB(e.target.value)}></input>
<p>Input Form B{dataB}</p>
</div>
);
};
export function BigForm (props) {
const [dataA,setDataA] = useState()
const [dataB,setDataB] = useState()
return (
<div className="App">
<FormA dataA={dataA} setDataA={setDataA}></FormA>
<FormB dataB={dataB} setDataB={setDataB}></FormB>
</div>
);
}
Edit:
For some reason, it is my intention to set the state in parent, instead of having the child to held its own state, because at the end, I need to aggregate the data from all other forms for other purpose.
.Memo is not what I want too coz in my real examples, it is not working due to there is other complexity preventing it to work.
There are a few ways to optimize re-rendering in React. The first way is shouldComponentUpdate(). This is a function that is called by React before a component is re-rendered. If shouldComponentUpdate() returns false, then the component will not be re-rendered.
The second way to optimize React components is to use PureComponent. PureComponent is a React component that is similar to Component, but it implements shouldComponentUpdate() with a shallow comparison of props and state. This means that if two values are ===, then PureComponent will not re-render the component.
The third way to optimize React components is to use React. memo(). React.memo() is a higher order component that allows you to memoize the result of a component so that it doesn't have to be re-rendered unless the props or state change.

Calling useEffect in a functional component within a functional component causes this message: Rendered more hooks than during the previous render

first off - Happy Friday!
I just came on here to see if anyone had any input to an issue that I am seeing in my ReactJs application. So I have a functional component renderViews and in that functional component, there are multiple views to render. Then within the renderViews I have another functional component carDetailsView and I try to make a call to an api when that particular component appears(as a modal). requestCarsDetails() should only be called when that component appears so thats why I nested a useEffect hook in the carDetailsView. But that causes an issue:
Rendered more hooks than during the previous render
.Please see code below:
const renderViews = () = > {
useEffect(()=> {
requestCarInfos()
.then((res) => {
setCars(cars);
});
}, []);
const carDetailsView = () => {
useEffect(() => {
requestCarDetails()
.then((res) => {
setDetails(res.details);
});
}, []);
return (<div>carDetailsView</div>)
}
return (<div>{determineView()}</div>)
}
The useEffect that is being used at the top level works fine. The issue only appeared after I added the second useEffect which is in the carDetailsView. Any help or advice is appreciated. Thanks!
Its a rule of hooks.
https://reactjs.org/docs/hooks-rules.html
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.
React relies on the order in which Hooks are called.
As long as the order of the Hook calls is the same between renders, React can associate some local state with each of them.
if we put a Hook call inside a condition we can skip the Hook during rendering, the order of the Hook calls becomes different:
React wouldn’t know what to return for the second useState Hook call. React expected that the second Hook call in this component corresponds to the persistForm effect, just like during the previous render, but it doesn’t anymore. From that point, every next Hook call after the one we skipped would also shift by one, leading to bugs.
This is why Hooks must be called on the top level of our components. If we want to run an effect conditionally, we can put that condition inside our Hook:
use the lint https://www.npmjs.com/package/eslint-plugin-react-hooks
this is a caveat of using functional components, on each render everything inside the functional component gets kind of executed. so react needs to maintain the list of all hooks which have been defined when the component was created. think of it as an array.
on each render, useState will return the value for you. if you understand this, you will understand what stale state also means. ( stale state can happen, when closures occur within these components )
Something like that?
const CarDetailsView = () => {
React.useEffect(() => {
console.log("Running CarDetailsView useEffect...") ;
},[]);
return(
<div>I amCarDetailsView</div>
);
};
const Views = () => {
const [showCarDetails,setShowCarDetails] = React.useState(false);
const toggleCarDetails = () => setShowCarDetails(!showCarDetails);
React.useEffect(() => {
console.log("Running Views useEffect...") ;
},[]);
return(
<div>
<div>I am Views</div>
<button onClick={toggleCarDetails}>Toggle car details</button>
{showCarDetails && <CarDetailsView/>}
</div>
);
};
const App = () => {
return(<Views/>);
};
ReactDOM.render(<App/>,document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.0/umd/react-dom.production.min.js"></script>
<div id="root"/>

State not reset when component toggles

I've been experimenting with conditionally displaying components.
Externally handled:
{show && <MyComponent />}
Internally handled:
const MyComponent = () => {
const [externalState] = useContext();
const [state, setState] = useState("")
// Don't render base on some state value
if(externalState === false) return null;
return <input value={state} onChange={e=>setState(e.currentTarget.value)} type="text"/>
}
I've noticed that in the second method, when I toggle the Component on and off, the state does not reset. Is there a way to fix this or is the second approach not recommended.
The reason you'll see different behaviors with state retention is because the first method will mount or unmount MyComponent whenever the condition changes.
If show is false, MyComponent is not just hidden, it is actually removed from the DOM.
In the second method, only the JSX returned from MyComponent is removed from the DOM, the rest of the component remains mounted, meaning state is maintained.
As far as "fixing" the problem (most will actually see the loss of state a bug, so this is a different perspective), the simplest method is to continue using option 1. You can certainly use option 2, but you will have to add some extra logic/function calls to reset state each time the internally managed show is updated.

How to update component state on redux state change?

Since I have a component with forms, I need the forms to be connected to the component state. The initial data comes from Redux so try to initialize and update the component by setting the state with the props:
componentWillMount = () => {
this.setState({
language: this.props.language || 'en'
})
}
language is a connected prop and I checked that it is updated in the store.
const mapStateToProps = state => ({
language: state.language
})
I also tried to use componentWillReceiveProps and componentWillUpdate but it doesn't work. I get the initial state, and even though the store and the connected props change, the component's state doesn't update.
{this.props.language} // updates
{this.state.language} // doesn't change
What is the correct way to manage forms from Redux data?
The render part:
render () {
const {classes, theme, web} = this.props
const language = (
<CardContent>
<Typography type="headline">
Language
</Typography>
<Divider/>
<form className={classes.container} autoComplete="off">
<FormControl fullWidth margin="normal">
<InputLabel htmlFor="language">Select Block</InputLabel>
<Select
value={this.state.language} // <==================== language
onChange={this.handleLanguaheChange}
input={<Input id="language"/>}
>
<MenuItem value={'en'}>English</MenuItem>
<MenuItem value={'he'}>עברית</MenuItem>
</Select>
</FormControl>
</form>
</CardContent>
)
...
return (
<Grid
container
spacing={theme.spacing.unit * 3}
justify={'space-between'}
className={classes.gridWrap}
>
<Grid item xs={6}>
<Card className={classes.card}>
{language}
</Card>
...
First, you are using an arrow function for componentWillMount. Rule of thumb is, do not use arrow functions for life-cycle hooks(componentWillMount, shouldComponentUpdate, ...etc). It's usual to setState in componentWillMount hook. But never set state in componentDidMount.
please try to re-write it as,
constructor(props) {
super(props)
this.state = {
language: 'en',
}
}
componentWillMount() {
const { language } = this.props
if (language) this.setState(prevState => ({ language: prevState.language = language }))
}
in some exceptional cases, such as i wrote two classes in a single .js file(like i said, some exceptions) and i couldn't be able to modify it from componentWillMount as expected(later noted, the props are modified by the child class).
in such cases, you can override it in render
render() {
const { language } = this.props
if (language) this.setState(prevState => ({ language: prevState.language = language }))
To accomplish this with React Hooks:
Track previous value with useRef()
Compare with previous value and conditionally update the local component state
The posted example didn't really make sense to me so here is the problem I faced:
I have a form with component state that I needed to clear out when the redux state changed.
To accomplish this my component looks like this:
import { useSelector } from 'react-redux';
import React, { useState, useEffect, useRef } from 'react';
const CreateCase = () => {
//redux state
const accounts = useSelector(state => state.accounts);
//component state
const [productId, setProductId] = useState(undefined);
const prevAccountRef = useRef<string>();
useEffect(() => {
//compare current with previous account and clear productId if changed
if (account.id != prevAccountRef.current) {
setProductId(undefined);
}
//set previous account for next render
prevAccountRef.current = account.id;
});
//... render
}
It's very important that you only run setState inside of useEffect conditionally.
even though the store and the connected props change, the component's state doesn't update
The way you have it written, the state won't update unless you explicitly update it using setState() (most likely in the componentWillReceiveProps() method).
When you use mapStateToProps() with the Redux connect() HOC, you are mapping your Redux state to your component through its props, so in your case this.props.language will update when the Redux stored updates.
componentWillReceiveProps will be called only when your component re-rendered.
Initially when component first time mounting, it not going to triggered.
You can not call setState inside componentwillupdate.
In order to initialise the initial state of component from redux store,you should use constructor.
constructor(props) {
super(props);
this.state = {
language: this.props.language || 'en'
}
}
Not sure if this applies, but I ran into a similar problem recently and my case was due to the fact that calling this.setState does not guarantee an instant update to state; it only says that the state change will be effected eventually.
From the react-component documentation:
Think of setState() as a request rather than an immediate command to
update the component. For better perceived performance, React may
delay it, and then update several components in a single pass. React
does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may
batch or defer the update until later. This makes reading this.state
right after calling setState() a potential pitfall. Instead, use
componentDidUpdate or a setState callback (setState(updater,
callback)), either of which are guaranteed to fire after the update
has been applied. If you need to set the state based on the previous
state, read about the updater argument below.
If you need to make some "instant" changes to state or things that depend on state, then there's that callback on setState() that you can use to lock things in place. The callback is what worked for me.

setState doesn't update the state immediately [duplicate]

This question already has answers here:
Why does calling react setState method not mutate the state immediately?
(9 answers)
The useState set method is not reflecting a change immediately
(15 answers)
Closed 8 months ago.
I would like to ask why my state is not changing when I do an onClick event. I've search a while ago that I need to bind the onClick function in constructor but still the state is not updating.
Here's my code:
import React from 'react';
import Grid from 'react-bootstrap/lib/Grid';
import Row from 'react-bootstrap/lib/Row';
import Col from 'react-bootstrap/lib/Col';
import BoardAddModal from 'components/board/BoardAddModal.jsx';
import style from 'styles/boarditem.css';
class BoardAdd extends React.Component {
constructor(props) {
super(props);
this.state = {
boardAddModalShow: false
};
this.openAddBoardModal = this.openAddBoardModal.bind(this);
}
openAddBoardModal() {
this.setState({ boardAddModalShow: true }); // set boardAddModalShow to true
/* After setting a new state it still returns a false value */
console.log(this.state.boardAddModalShow);
}
render() {
return (
<Col lg={3}>
<a href="javascript:;"
className={style.boardItemAdd}
onClick={this.openAddBoardModal}>
<div className={[style.boardItemContainer,
style.boardItemGray].join(' ')}>
Create New Board
</div>
</a>
</Col>
);
}
}
export default BoardAdd
Your state needs some time to mutate, and since console.log(this.state.boardAddModalShow) executes before the state mutates, you get the previous value as output. So you need to write the console in the callback to the setState function
openAddBoardModal() {
this.setState({ boardAddModalShow: true }, function () {
console.log(this.state.boardAddModalShow);
});
}
setState is asynchronous. It means you can’t call it on one line and assume the state has changed on the next.
According to React docs
setState() does not immediately mutate this.state but creates a
pending state transition. Accessing this.state after calling this
method can potentially return the existing value. There is no
guarantee of synchronous operation of calls to setState and calls may
be batched for performance gains.
Why would they make setState async
This is because setState alters the state and causes rerendering. This
can be an expensive operation and making it synchronous might leave
the browser unresponsive.
Thus the setState calls are asynchronous as well as batched for better
UI experience and performance.
Fortunately setState() takes a callback. And this is where we get updated state.
Consider this example.
this.setState({ name: "myname" }, () => {
//callback
console.log(this.state.name) // myname
});
So When callback fires, this.state is the updated state.
You can get mutated/updated data in callback.
For anyone trying to do this with hooks, you need useEffect.
function App() {
const [x, setX] = useState(5)
const [y, setY] = useState(15)
console.log("Element is rendered:", x, y)
// setting y does not trigger the effect
// the second argument is an array of dependencies
useEffect(() => console.log("re-render because x changed:", x), [x])
function handleXClick() {
console.log("x before setting:", x)
setX(10)
console.log("x in *line* after setting:", x)
}
return <>
<div> x is {x}. </div>
<button onClick={handleXClick}> set x to 10</button>
<div> y is {y}. </div>
<button onClick={() => setY(20)}> set y to 20</button>
</>
}
Output:
Element is rendered: 5 15
re-render because x changed: 5
(press x button)
x before setting: 5
x in *line* after setting: 5
Element is rendered: 10 15
re-render because x changed: 10
(press y button)
Element is rendered: 10 20
Live version
Since setSatate is a asynchronous function so you need to console the state as a callback like this.
openAddBoardModal(){
this.setState({ boardAddModalShow: true }, () => {
console.log(this.state.boardAddModalShow)
});
}
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
setState() will always lead to a re-render unless shouldComponentUpdate() returns false. If mutable objects are being used and conditional rendering logic cannot be implemented in shouldComponentUpdate(), calling setState() only when the new state differs from the previous state will avoid unnecessary re-renders.
The first argument is an updater function with the signature:
(state, props) => stateChange
state is a reference to the component state at the time the change is being applied. It should not be directly mutated. Instead, changes should be represented by building a new object based on the input from state and props. For instance, suppose we wanted to increment a value in state by props.step:
this.setState((state, props) => {
return {counter: state.counter + props.step};
});
Think of setState() as a request rather than an immediate command to
update the component. For better perceived performance, React may
delay it, and then update several components in a single pass. React
does not guarantee that the state changes are applied immediately.
Check this for more information.
In your case you have sent a request to update the state. It takes time for React to respond. If you try to immediately console.log the state, you will get the old value.
The above solutions don't work for useState hooks.
One can use the below code
setState((prevState) => {
console.log(boardAddModalShow)
// call functions
// fetch state using prevState and update
return { ...prevState, boardAddModalShow: true }
});
This callback is really messy. Just use async await instead:
async openAddBoardModal(){
await this.setState({ boardAddModalShow: true });
console.log(this.state.boardAddModalShow);
}
If you want to track the state is updating or not then the another way of doing the same thing is
_stateUpdated(){
console.log(this.state. boardAddModalShow);
}
openAddBoardModal(){
this.setState(
{boardAddModalShow: true},
this._stateUpdated.bind(this)
);
}
This way you can call the method "_stateUpdated" every time you try to update the state for debugging.
Although there are many good answers, if someone lands on this page searching for alternative to useState for implementing UI components like Navigation drawers which should be opened or closed based on user input, this answer would be helpful.
Though useState seems handy approach, the state is not set immediately and thus, your website or app looks laggy... And if your page is large enough, react is going to take long time to compute what all should be updated upon state change...
My suggestion is to use refs and directly manipulate the DOM when you want UI to change immediately in response to user action.
Using state for this purspose is really a bad idea in case of react.
setState() is asynchronous. The best way to verify if the state is updating would be in the componentDidUpdate() and not to put a console.log(this.state.boardAddModalShow) after this.setState({ boardAddModalShow: true }) .
according to React Docs
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately
According to React Docs
React does not guarantee that the state changes are applied immediately.
This makes reading this.state right after calling setState() a potential pitfall and can potentially return the existing value due to async nature .
Instead, use componentDidUpdate or a setState callback that is executed right after setState operation is successful.Generally we recommend using componentDidUpdate() for such logic instead.
Example:
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
class App extends React.Component {
constructor() {
super();
this.state = {
counter: 1
};
}
componentDidUpdate() {
console.log("componentDidUpdate fired");
console.log("STATE", this.state);
}
updateState = () => {
this.setState(
(state, props) => {
return { counter: state.counter + 1 };
});
};
render() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<button onClick={this.updateState}>Update State</button>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
this.setState({
isMonthFee: !this.state.isMonthFee,
}, () => {
console.log(this.state.isMonthFee);
})
when i was running the code and checking my output at console it showing the that it is undefined.
After i search around and find something that worked for me.
componentDidUpdate(){}
I added this method in my code after constructor().
check out the life cycle of react native workflow.
https://images.app.goo.gl/BVRAi4ea2P4LchqJ8
Yes because setState is an asynchronous function. The best way to set state right after you write set state is by using Object.assign like this:
For eg you want to set a property isValid to true, do it like this
Object.assign(this.state, { isValid: true })
You can access updated state just after writing this line.

Categories

Resources