UseEffect and useCallback still causes infinite loop in react project - reactjs

I can't seem to resolve an infinite loop issue in my react project.
I'm working on a daily-log react app. Let me explain the project briefly. Here is the picture of the code for quick view:
The same code is available at the bottom.
The structure (from top to bottom)
The DailyLog component has a form that uses Question components into which props are passed.
The Question component uses the props to display a question and description. It also contains an Input component into which props are further passed down.
The Input component takes the props and renders the appropriate form input field.
The logic (from bottom to top)
The Input component handles it's own inputState. The state is changed when the user inputs something and the onChangeHandler is triggered.
The Input component also has a useEffect() hook that calls an onInput() function that was passed down as props from DailyLog.
The onInputHandler() in the DailyLog component updates the formState which is the form-wide state containing all input field values. The formState is amended depending on which input field is filled at the time.
The onInputHandler() uses the useCallback() hook which is supposed to stop an infinite loop caused by any parent/child re-renders. But it doesn't work :frowning:
What's wrong in the code? What am I missing here? Code provided below:
//DailyLog.js
import React, { useState, useCallback } from 'react';
import Question from '../components/FormElements/Question';
import questionData from '../components/DailyLog/questionData';
import './DailyLog.css';
const DailyLog = () => {
const [formState, setFormState] = useState();
const onInputHandler = useCallback(
(inputId, inputValue) => {
setFormState({
...formState,
[inputId]: inputValue,
});
},
[formState]
);
return (
<main className="container">
<form action="" className="form">
<Question
id="title"
element="input"
type="text"
placeholder="Day, date, calendar scheme"
onInput={onInputHandler}
/>
<Question
id="focus"
question={questionData.focusQuestion}
description={questionData.focusDescription}
element="textarea"
placeholder="This month's focus is... This week's focus is..."
onInput={onInputHandler}
/>
</form>
</main>
);
};
export default DailyLog;
//Question.js
import React from 'react';
import Input from './Input';
import './Question.css';
const Question = props => {
return (
<div className="form__group">
{props.question && (
<label className="form__label">
<h2>{props.question}</h2>
</label>
)}
<small className="form__description">{props.description}</small>
<Input
id={props.id}
element={props.element}
type={props.type}
placeholder={props.placeholder}
onInput={props.onInput}
/>
</div>
);
};
export default Question;
//Input.js
import React, { useState, useEffect } from 'react';
import './Input.css';
const Input = props => {
const [inputState, setInputState] = useState();
const { id, onInput } = props;
useEffect(() => {
onInput(id, inputState);
}, [id, onInput, inputState]);
const onChangeHandler = event => {
setInputState(event.target.value);
};
// check if question element type is for input or textarea
const element =
props.element === 'input' ? (
<input
id={props.id}
className="form__field"
type={props.type}
value={inputState}
placeholder={props.placeholder}
onChange={onChangeHandler}
/>
) : (
<textarea
id={props.id}
className="form__field"
rows="1"
value={inputState}
placeholder={props.placeholder}
onChange={onChangeHandler}
/>
);
return <>{element}</>;
};
export default Input;

Remove id and onInput from useEffect sensivity list
useEffect(() => {
onInput(id, inputState);
}, [inputState]);
And set default value of inputState to '' as follow:
const [inputState, setInputState] = useState('');
To prevent 'A component is changing an uncontrolled input of type text to be controlled error in ReactJS'. Also you can init formState:
const [formState, setFormState] = useState({title:'', focus:''});

Related

Why storing form data in useState() hook in React?

I am somewhat familiar with React.
In react tutorials, I have seen people creating useState hooks to store form data and also making them controlled and started doing the same but recently I saw a video of coder creating a form without using any useState for storing the data?
This has left me stumped because I am not understanding why I need to store form data in useState hook now. For what type of situations, is it useful to use state or just simply take the form value?
Please help.
I have tried searching google, reading some blogs on useState hooks, how it is used where we want to re-render a component.
Like, If I am displaying some data, and want to change it on some event, then I will use useState hook so that it will re-render the display component, with the updated state.
But I am still not understanding its use case with forms.
In a React form, you may want to do something on the screen as a user is typing using an onChange callback. You would need to store the user's input in state in order to properly deal with it in a useEffect. Just storing naked variables in components in React is terrible practice.
You generally only use destructured constants or derived conditional variables after hooks are called in a component in order to affect the display or to pass hook results as input to other hooks.
I would be interested to see the video or tutorial that showed you how to do a form "without state" in React because I'm not sure how that's possible - unless they were using a third-party library like formik which abstracts the useState away from you through it's own methods.
*ADDED EXAMPLE
Consider this simple comparison of two forms, one with state and one without:
https://codesandbox.io/p/sandbox/zen-sammet-imfzl5
app.tsx
import { useState } from "react";
import "./App.css";
import FormWithState from "./FormWithState";
import FormWithoutState from "./FormWithoutState";
function App() {
return (
<>
<div
style={{
border: "1px solid green",
marginBottom: "1rem",
padding: "1rem",
}}
>
<div>
This form with state notifies after 3 characters, and on submit.
</div>
<FormWithState />
</div>
<div style={{ border: "1px solid red", padding: "1rem" }}>
<div>This form without state can't alert or useEffect</div>
<FormWithoutState />
</div>
</>
);
}
export default App;
FormWithState.tsx
import { useEffect, useState } from "react";
import "./App.css";
export default function FormWithState() {
const [message, setMessage] = useState("");
const onChange = (e: any) => {
setMessage(e.target.value);
};
const onSubmit = (e: any) => {
e.preventDefault();
alert(`You sent the message: ${message}`);
};
useEffect(() => {
if (message.length >= 3) {
alert("This message is longer than 3 characters. Wow!");
}
}, [message]);
return (
<div className="App">
<form onSubmit={onSubmit}>
<input type="text" name="name" onChange={onChange} />
<button type="submit">Submit</button>
</form>
</div>
);
}
FormWithoutState.tsx
import { useEffect } from "react";
import "./App.css";
export default function FormWithState() {
let message = "";
const onChange = (e: any) => {
message = e.target.value;
};
const onSubmit = (e: any) => {
e.preventDefault();
alert(`You sent the message: ${message}`);
};
useEffect(() => {
if (message.length > 3) {
alert("This message is longer than 3 characters. Wow!");
}
}, [message]);
return (
<div className="App">
<form onSubmit={onSubmit}>
<input type="text" name="name" onChange={onChange} />
<button type="submit">Submit</button>
</form>
</div>
);
}
Notice how the green form can alert and useEffect as the user types / submits - while the red form cannot.
Both Forms:
This works (with state):
This does not work (without state):
I hope that helps a bit!

React input onChange not rerendering state when useState has default value

So this is working, input changes when I type.
const [editfield, setEdit_field] = React.useState();
function updateField(event) {
setEdit_field(event.target.value);
}
function editPost() {
setPostbody(<div><input onChange={updateField} value={editfield}></input></div>)
}
But when a put a default value in the useState it doesnt work anymore
const [editfield, setEdit_field] = React.useState(post.body);
function updateField(event) {
setEdit_field(event.target.value);
}
function editPost() {
setPostbody(<div><input onChange={updateField} value={editfield}></input></div>)
}
Access code here: https://codesandbox.io/s/brt7ok
You were setting JSX inside the state, that might be the issue, I have created a codesandbox demo which will help you in conditional Rendering
DEMO
You are rendering that function so when state will updated it re-run that function and resetting the value.
You can use below approach.
Take one state which represents the field is editable or not. And add condition with input component that it should only render when field is editable.
For example,
const [isEditable, setIsEditable] = useState(false);
<button onClick={() => setIsEditable(!isEditable)}>edit</button>
isEditable && (
<div>
<input onChange={updateField} value={editfield} />
</div>
)
For more idea, just put console before return. You will get idea.
Hope this helps :)
import React, { useState } from "react";
export default function App() {
let body = "hello";
const [editfield, setEditfield] = useState(body);
const [visible, setVisible] = useState(false);
function updateField(event) {
setEditfield(event.target.value);
}
function editPost() {
setVisible(true);
}
return (
<div>
<div>{visible?<div>
<input onChange={updateField} value={editfield}/>
</div>:body}</div>
<div>
<button onClick={editPost}>edit</button>
</div>
</div>
);
}

How to focus and select a checkbox using React ref?

I have been looking around a method to correctly focus and select a checkbox in React code.
The methods focus() and select() that I'm using in the example below are not working :
import React, { useRef } from "react";
export const HelloWorld = () => {
const checkboxref = useRef(null);
const handleOnClick = () => {
checkboxref.current.focus();
checkboxref.current.select();
};
return (
<div>
<button onClick={handleOnClick}>Focus</button>
<input type="checkbox" ref={checkboxref} />
</div>
);
};
When I click on the button, my checkbox is not focused and not selected...
Any solution please ?
Thank you so much.
You don't need to create a separate function to handle onChange event
const checkboxref = useRef(null);
You can simply get the current value of the checkbox with:
checkboxref.current.checked
// which returns a boolean
use this one it might help you. here I am using createRef instead of useRef and also uses the callback hook which ensures the availability of ref when you click the button.
import React,{createRef, useCallback} from 'react';
export const HelloWorld = () => {
const checkboxref = createRef();
const handleOnClick = useCallback(() => {
const node = checkboxref.current;
if(node){
node.focus();
node.select();
}
}, [checkboxref]);
return (
<div>
<button onClick={handleOnClick}>Focus</button>
<input type="checkbox" ref={checkboxref} />
</div>
);
};
The select methods selects text in elements such as text inputs and text areas, so I'm not sure what effect you expect it to have on the checkbox. As for focus, it can focus, but again, there is not much you can do with a focused checkbox. I can only think of styling it https://jsfiddle.net/4howanL2/1/

How do I make a form submission save data in a useState hook variable?

I'm new to React and JavaScript; currently looking at hooks. I have these two React functional components in an application I generated using Create React App:
App.js
import React, { useState} from 'react';
import NameTaker from './components/NameTaker'
export default function App() {
const [name, setName] = useState(null);
if (name == null) {
return <NameTaker onSubmit={submitHandler} />
}
return <p>Your name is: {name}</p>
function submitHandler(e) {
setName(e.value)
}
}
and NameTaker.js
import React from 'react';
export default function NameTaker(props) {
return(
<div>
<p>Please enter your name.</p>
<form onSubmit={props.onSubmit}>
<label>
Name:
<input type='text' name='name' />
</label>
<button type='submit'>Submit</button>
</form>
</div>
)
}
This is the render statement in index.js:
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
The application runs ok locally and the all the elements in NameTaker.js display correctly on the webpage.
When I fill out a name and click the button, I expect it to execute the callback function, store the name string inside the name variable, then when the App function component re-renders, the new name should be stored and <p>Your name is: {name}</p> should get returned instead of the form when the conditional statement is hit. But instead, when I click the button, the URL just changes to add /?name=whatever on the end, and the form remains on the page.
I tried modifying the program so that it gives the callback to the button instead of the form like this:
<button onClick={props.onSubmit}>Submit</button>
but there is not change in behaviour.
I tried adding e.preventDefault(); before and then after the setName call in my callback function but that didn't work either.
The exact behaviour I'm looking for is: when name is null, return component displaying a form/input box, use callback to save the submitted name in state of the parent component, re-render parent component, but because state is no longer null return the paragraph displaying name instead of the form component.
Can anyone show me how to do that please?
You just need to modify your submitHandler function as follows:
function submitHandler(e) {
e.preventDefault();
setName(e.currentTarget.name.value)
}
You need e.preventDefault() to stop the page from re-rendering, though often it isn't as needed as it used to be, I'm not entirely sure why.
In order to get values from the form, you have to specify which values you want. e.currentTarget.name.value gives you the current target (i.e. the form) then the .name part gives you the input of the form with the name or id of name, and then you can get the value of that input.
const {useState} = React;
function NameTaker(props) {
return(
<div>
<p>Please enter your name.</p>
<form onSubmit={props.onSubmit}>
<label>
Name:
<input type='text' name='name' />
</label>
<button type='submit'>Submit</button>
</form>
</div>
)
}
function App() {
const [name, setName] = useState(null);
function submitHandler(e) {
e.preventDefault();
setName(e.currentTarget.name.value)
}
if (name == null) {
return <NameTaker onSubmit={submitHandler} />
}
return <p>Your name is: {name}</p>
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
<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>
<div id="root"/>
import React, { useState} from 'react';
import NameTaker from './components/NameTaker'
export default function App() {
const [name, setName] = useState(null);
function submitHandler(e) {
setName(e.target.value)
}
return(
{!!name ? <p>Your name is: {name}</p> : <NameTaker onSubmit={submitHandler} />
})
}

How to create a component in a custom hook that handles state within the hook?

How can I create a component in a custom hook where the hook holds the state for the component?
My attempt basically does the right thing, but drag-and-drop is not working as expected. Instead the slider will only change the value on click. I think the problem is, that the useState hook gets called outside of the X definition. But how can we create a component in a hook then where I need to work with the state of that internal component within the rest of the hook?
https://codesandbox.io/s/material-demo-milu3?file=/demo.js:0-391
import React from "react";
import Slider from "#material-ui/core/Slider";
function useComp() {
const [value, setValue] = React.useState(30);
const X = () => <Slider value={value} onChange={(_, v) => setValue(v)} />;
return { X, value };
}
export default function ContinuousSlider() {
const { X, value } = useComp();
return (
<div>
{value}
<X />
</div>
);
}
Whenever the custom hook is called (on each render), a new Slider (Broken) component is created. Since a new component is created, the event handlers are recreated as well, and the drag is cancelled. You can solve this problem in two ways:
Wrap the component in useCallback(), and pass the value when rendering the component (sandbox):
const Broken = useCallback(({ value }) => (
<Slider value={value} onChange={changeHandler} />
), [changeHandler]);
// usage
<Broken value={broken} />
Render the component in the hook, and use the include it in the component (sandbox):
function useComp() {
const [broken, setBroken] = React.useState(30);
const changeHandler = useCallback((_, v) => setBroken(v), []);
const slider = <Slider value={broken} onChange={changeHandler} />;
return { slider, broken };
}
// usage
<div>
Broken: {broken}
{slider}
OK: {ok}
<Slider value={ok} onChange={(_, v) => setOk(v)} />
</div>

Resources