react: Convert from Class to functional component with state - reactjs

How would one convert the class component to a functional one?
import React, { Component } from 'react'
class Test extends Component {
state = {
value: "test text",
isInEditMode: false
}
changeEditMode = () => {
this.setState({
isInEditMode: this.state.isInEditMode
});
};
updateComponentValue = () => {
this.setState({
isInEditMode: false,
value: this.refs.theThexInput.value
})
}
renderEditView = () => {
return (
<div>
<input type="text" defaultValue={this.state.value} ref="theThexInput" />
<button onClick={this.changeEditMode}>X</button>
<button onClick={this.updateComponentValue}>OK</button>
</div>
);
};
renderDefaultView = () => {
return (
<div onDoubleClick={this.changeEditMode}
{this.state.value}>
</div>
};
render() {
return this.state.isInEditMode ?
this.renderEditView() :
this.renderDefaultView()
}}
export default Test;
I assume one needs to use hooks and destructioning, but not sure how to implement it.
Is there a good guidline or best practice to follow?

I gave a brief explanation of what is going on:
const Test = () => {
// Use state to store value of text input.
const [value, setValue] = React.useState("test text" /* initial value */);
// Use state to store whether component is in edit mode or not.
const [editMode, setEditMode] = React.useState(false /* initial value */);
// Create function to handle toggling edit mode.
// useCallback will only generate a new function when setEditMode changes
const toggleEditMode = React.useCallback(() => {
// toggle value using setEditMode (provided by useState)
setEditMode(currentValue => !currentValue);
}, [
setEditMode
] /* <- dependency array - determines when function recreated */);
// Create function to handle change of textbox value.
// useCallback will only generate a new function when setValue changes
const updateValue = React.useCallback(
e => {
// set new value using setValue (provided by useState)
setValue(e.target.value);
},
[setValue] /* <- dependency array - determines when function recreated */
);
// NOTE: All hooks must run all the time a hook cannot come after an early return condition.
// i.e. In this component all hooks must be before the editMode if condition.
// This is because hooks rely on the order of execution to work and if you are removing
// and adding hooks in subsequent renders (which react can't track fully) then you will
// get warnings / errors.
// Do edit mode render
if (editMode) {
return (
// I changed the component to controlled can be left as uncontrolled if prefered.
<input
type="text"
autoFocus
value={value}
onChange={updateValue}
onBlur={toggleEditMode}
/>
);
}
// Do non-edit mode render.
return <div onDoubleClick={toggleEditMode}>{value}</div>;
};
and here is a runnable example

I have released this npm package command line to convert class components to functional components.
It's open source. Enjoy.
https://www.npmjs.com/package/class-to-function

Related

useEffect cleanup runs on every render

I am trying to build a functionality where when a user navigates away from the form i.e when component unmounts it should trigger a save i.e post form data to server. This should happen only if there is any change in form data. Can anyone guide me as to why this is happening. I have tried class based approach which works but I do not want to refactor my production code.
import { useCallback, useEffect, useState } from "react";
import React from "react";
import * as _ from "lodash";
import { useFormik } from "formik";
// for now this is hardcoded here..but let's assume
// this server data will be loaded when component mounts
const serverData = {
choice: "yes",
comment: "some existing comment"
};
const availableChoices = ["yes", "no"];
const Form = () => {
const formik = useFormik({ initialValues: { ...serverData } });
const [isFormChanged, setIsFormChanged] = useState(false);
const valuesHaveChanged = React.memo(() => {
console.log("INIT VALUES= ", formik.initialValues);
console.log("FINAL VALUES = ", formik.values);
return !_.isEqual(formik.initialValues, formik.values);
}, [formik.initialValues, formik.values]);
const triggerSave = () => console.log("Save");
useEffect(() => {
// setForm({ ...serverData });
if (valuesHaveChanged) {
setIsFormChanged(true);
}
return () => {
// when this cleanup function runs
// i.e when this component unmounts,
// i need to check if there
// was any change in the form state
// if there was a change i need to trigger a save
// i.e post form data to server.
if (setIsFormChanged) {
triggerSave();
}
};
});
return (
<form>
<div className="form-group">
{availableChoices.map((choice) => (
<label key={choice}>
{choice}
<input
id="choice"
value={choice}
className="form-control"
type="radio"
name="choice"
checked={choice === formik.values.choice}
onChange={formik.handleChange}
/>
</label>
))}
</div>
<div className="form-group">
<textarea
rows="5"
cols="30"
id="comment"
name="comment"
value={formik.values.comment}
onChange={formik.handleChange}
className="form-control"
placeholder="some text..."
></textarea>
</div>
</form>
);
};
export default Form;
The first problem i spotted is the dependency array.
useEffect(() => {
// the flag can be set anytime upon a field has changed
// maybe formik has a value like that, read doc
if (valuesHaveChanged) {
setIsFormChanged(true);
}
return () => {
if (setIsFormChanged) {
triggerSave();
}
}
// the dependency array is [], can't be missed
}, [])
Currently you are calling this effect and cleanup this effect in every update, ex. if any value changes in this component. But normally you only want to do it once upon dismount.
Even you do the above right, you still need to make sure your code contains no memory leak, because you are trying to do something upon the dismount. So it's better to pass the values:
triggerSave([...formik.values])
And make sure inside triggerSave, you don't accidently call anything about formik or setState.
Try to use useEffect with dependencies
useEffect(() => {
return () => {
// when this cleanup function runs
// i.e when this component unmounts,
// i need to check if there
// was any change in the form state
// if there was a change i need to trigger a save
// i.e post form data to server.
if (!_.isEqual(formik.initialValues, formik.values)) {
triggerSave();
}
};
}, [formik.values]); // won't run on every render but just on formik.values update
Explanation:
useEffect has dependencies as a second argument, if [] is passed - effect is triggered only on mount, if [...] passed, will trigger on the first mount and on any of ... update.
If you don't pass the second agrument, useEffect works as a on-every-render effect.

React - UseEffect not re-rendering with new data?

This is my React Hook:
function Student(props){
const [open, setOpen] = useState(false);
const [tags, setTags] = useState([]);
useEffect(()=>{
let input = document.getElementById(tagBar);
input.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
event.preventDefault();
document.getElementById(tagButton).click();
}
});
},[tags])
const handleClick = () => {
setOpen(!open);
};
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
tagList.push(input.value);
console.log("tag");
console.log(tags);
console.log("taglist");
console.log(tagList);
setTags(tagList);
}
const tagDisplay = tags.map(t => {
return <p>{t}</p>;
})
return(
<div className="tags">
<div>
{tagDisplay}
</div>
<input type='text' id={tagBar} className="tagBar" placeholder="Add a Tag"/>
<button type="submit" id={tagButton} className="hiddenButton" onClick={addTag}></button>
<div>
);
What I am looking to do is be able to add a tag to these student elements (i have multiple but each are independent of each other) and for the added tag to show up in the tag section of my display. I also need this action to be triggerable by hitting enter on the input field.
For reasons I am not sure of, I have to put the enter binding inside useEffect (probably because the input element has not yet been rendered).
Right now when I hit enter with text in the input field, it properly updates the tags/tagList variable, seen through the console.logs however, even though I set tags to be the re-rendering condition in useEffect (and the fact that it is also 1 of my states), my page is not updating with the added tags
You are correct, the element doesn't exist on first render, which is why useEffect can be handy. As to why its not re-rendering, you are passing in tags as a dependency to check for re-render. The problem is, tags is an array, which means it compares the memory reference not the contents.
var myRay = [];
var anotherRay = myRay;
var isSame = myRay === anotherRay; // TRUE
myRay.push('new value');
var isStillSame = myRay === anotherRay; // TRUE
// setTags(sameTagListWithNewElementPushed)
// React says, no change detected, same memory reference, skip
Since your add tag method is pushing new elements into the same array reference, useEffect thinks its the same array and is not re-triggers. On top of that, React will only re-render when its props change, state changes, or a forced re-render is requested. In your case, you aren't changing state. Try this:
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
// Create a new array reference with the same contents
// plus the new input value added at the end
setTags([...tagList, input.value]);
}
If you don't want to use useEffect I believe you can also use useRef to get access to a node when its created. Or you can put the callback directly on the node itself with onKeyDown or onKeyPress
I can find few mistake in your code. First, you attaching event listeners by yourself which is not preferred in react. From the other side if you really need to add listener to DOM inside useEffect you should also clean after you, without that, another's listeners will be added when component re-rendered.
useEffect( () => {
const handleOnKeyDown = ( e ) => { /* code */ }
const element = document.getElementById("example")
element.addEventListener( "keydown", handleOnKeyDown )
return () => element.removeEventListener( "keydown", handleOnKeyDown ) // cleaning after effect
}, [tags])
Better way of handling events with React is by use Synthetic events and components props.
const handleOnKeyDown = event => {
/* code */
}
return (
<input onKeyDown={ handleOnKeyDown } />
)
Second thing is that each React component should have unique key. Without it, React may have trouble rendering the child list correctly and rendering all of them, which can have a bad performance impact with large lists or list items with many children. Be default this key isn't set when you use map so you should take care about this by yourself.
tags.map( (tag, index) => {
return <p key={index}>{tag}</p>;
})
Third, when you trying to add tag you again querying DOM without using react syntax. Also you updating your current state basing on previous version which can causing problems because setState is asynchronous function and sometimes can not update state immediately.
const addTag = newTag => {
setState( prevState => [ ...prevState, ...newTage ] ) // when you want to update state with previous version you should pass callback which always get correct version of state as parameter
}
I hope this review can help you with understanding React.
function Student(props) {
const [tags, setTags] = useState([]);
const [inputValue, setInputValue] = useState("");
const handleOnKeyDown = (e) => {
if (e.keyCode === 13) {
e.preventDefault();
addTag();
}
};
function addTag() {
setTags((prev) => [...prev, inputValue]);
setInputValue("");
}
return (
<div className="tags">
<div>
{tags.map((tag, index) => (
<p key={index}>{tag}</p>
))}
</div>
<input
type="text"
onKeyDown={handleOnKeyDown}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add a Tag"
/>
<button type="submit" onClick={addTag}>
ADD
</button>
</div>
);
}

useCallback with dependency vs using a ref to call the last version of the function

While doing a code review, I came across this custom hook:
import { useRef, useEffect, useCallback } from 'react'
export default function useLastVersion (func) {
const ref = useRef()
useEffect(() => {
ref.current = func
}, [func])
return useCallback((...args) => {
return ref.current(...args)
}, [])
}
This hook is used like this:
const f = useLastVersion(() => { // do stuff and depends on props })
Basically, compared to const f = useCallBack(() => { // do stuff }, [dep1, dep2]) this avoids to declare the list of dependencies and f never changes, even if one of the dependency changes.
I don't know what to think about this code. I don't understand what are the disadvantages of using useLastVersion compared to useCallback.
That question is actually already more or less answered in the documentation: https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback
The interesting part is:
Also note that this pattern might cause problems in the concurrent mode. We plan to provide more ergonomic alternatives in the future, but the safest solution right now is to always invalidate the callback if some value it depends on changes.
Also interesting read: https://github.com/facebook/react/issues/14099 and https://github.com/reactjs/rfcs/issues/83
The current recommendation is to use a provider to avoid to pass callbacks in props if we're worried that could engender too many rerenders.
My point of view as stated in the comments, that this hook is redundant in terms of "how many renders you get", when there are too frequent dependencies changes (in useEffect/useCallback dep arrays), using a normal function is the best option (no overhead).
This hook hiding the render of the component using it, but the render comes from the useEffect in its parent.
If we summarize the render count we get:
Ref + useCallback (the hook): Render in Component (due to value) + Render in hook (useEffect), total of 2.
useCallback only: Render in Component (due to value) + render in Counter (change in function reference duo to value change), total of 2.
normal function: Render in Component + render in Counter : new function every render, total of 2.
But you get additional overhead for shallow comparison in useEffect or useCallback.
Practical example:
function App() {
const [value, setValue] = useState("");
return (
<div>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
type="text"
/>
<Component value={value} />
</div>
);
}
function useLastVersion(func) {
const ref = useRef();
useEffect(() => {
ref.current = func;
console.log("useEffect called in ref+callback");
}, [func]);
return useCallback((...args) => {
return ref.current(...args);
}, []);
}
function Component({ value }) {
const f1 = useLastVersion(() => {
alert(value.length);
});
const f2 = useCallback(() => {
alert(value.length);
}, [value]);
const f3 = () => {
alert(value.length);
};
return (
<div>
Ref and useCallback:{" "}
<MemoCounter callBack={f1} msg="ref and useCallback" />
Callback only: <MemoCounter callBack={f2} msg="callback only" />
Normal: <MemoCounter callBack={f3} msg="normal" />
</div>
);
}
function Counter({ callBack, msg }) {
console.log(msg);
return <button onClick={callBack}>Click Me</button>;
}
const MemoCounter = React.memo(Counter);
As a side note, if the purpose is only finding the length of input with minimum renders, reading inputRef.current.value would be the solution.

How can I change the state of one React component based on the value of its sibling?

I'm attempting to build a form in React that has some auto-filling features. I'm trying to create the ability to 'lock' the value of LossOver50k to either 'Yes' or 'No' based on the ReplacementCost value to prevent input errors. The code I have right now is
import React, { useState } from "react";
import ReplacementCost from "./ReplacementCost";
import LossOver50K from "./LossOver50K";
const LossAmount = () => {
const [replacementCost, setReplacementCost] = useState("");
const [highValue, setHighValue] = useState("No");
const handleChange = (newReplacementCost) => {
setReplacementCost(newReplacementCost);
if (replacementCost >= 50000) {
setHighValue("Yes");
}
};
// const changeHighValue = (newHighValue) => {
// setHighValue(newHighValue);
// };
return (
<div>
<ReplacementCost value={replacementCost} onChange={handleChange} />
<LossOver50K value={highValue} />
</div>
);
};
export default LossAmount;
My commented piece was working as an onChange handler on the LossOver50k component but I'm trying to manipulate the value of LossOver50k if the value of Replacement Cost is equal to or greater than 50,000. I have each of these components successfully updating their state when I input the values directly by passing the props down to the child components. Any ideas?
TIA!
Your condition should be with the most updated value and not with a staled one (as setState is async):
const handleChange = (newReplacementCost) => {
setReplacementCost(newReplacementCost);
if (newReplacementCost >= 50000) {
setHighValue("Yes");
}
};

Using React hooks, how can I update an object that is being passed to a child via props?

The parent component contains an array of objects.
It maps over the array and returns a child component for every object, populating it with the info of that object.
Inside each child component there is an input field that I'm hoping will allow the user to update the object, but I can't figure out how to go about doing that.
Between the hooks, props, and object immutability, I'm lost conceptually.
Here's a simplified version of the parent component:
const Parent = () => {
const [categories, setCategories] = useState([]);
useEffect(()=>{
// makes an axios call and triggers setCategories() with the response
}
return(
categories.map((element, index) => {
return(
<Child
key = {index}
id = {element.id}
firstName = {element.firstName}
lastName = {element.lastName}
setCategories = {setCategories}
})
)
}
And here's a simplified version of the child component:
const Child = (props) => {
return(
<h1>{props.firstName}</h1>
<input
defaultValue = {props.lastName}
onChange={()=>{
// This is what I need help with.
// I'm a new developer and I don't even know where to start.
// I need this to update the object's lastName property in the parent's array.
}}
)
}
Maybe without knowing it, you have lifted the state: basically, instead of having the state in the Child component, you keep it in the Parent.
This is an used pattern, and there's nothing wrong: you just miss a handle function that allows the children to update the state of the Parent: in order to do that, you need to implement a handleChange on Parent component, and then pass it as props to every Child.
Take a look at this code example:
const Parent = () => {
const [categories, setCategories] = useState([]);
useEffect(() => {
// Making your AXIOS request.
}, []);
const handleChange = (index, property, value) => {
const newCategories = [...categories];
newCategories[index][property] = value;
setCategories(newCategories);
}
return categories.map((c, i) => {
return (
<Child
key={i}
categoryIndex={i}
firstName={c.firstName}
lastName={c.lastName}
handleChange={handleChange} />
);
});
}
const Child = (props) => {
...
const onInputChange = (e) => {
props.handleChange(props.categoryIndex, e.target.name, e.target.value);
}
return (
...
<input name={'firstName'} value={props.firstName} onChange={onInputChange} />
<input name={'lastName'} value={props.lastName} onChange={onInputChange} />
);
}
Few things you may not know:
By using the attribute name for the input, you can use just one handler function for all the input elements. Inside the function, in this case onInputChange, you can retrieve that information using e.target.name;
Notice that I've added an empty array dependecies in your useEffect: without it, the useEffect would have run at EVERY render. I don't think that is what you would like to have.
Instead, I guest you wanted to perform the request only when the component was mount, and that is achievable with n empty array dependecies;

Resources