Get previous props value with React Hooks - javascript

I am using usePreviousValue custom hook to get previous props value from my component:
const usePreviousValue = value => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
const MyComponent = ({ count }) => {
const prevCount = usePreviousValue(count)
return (<div> {count} | {prevCount}</div>)
}
But in this case, in prevCount I always have only the first count prop value when a component was rendered, and the next updated prop value is never assigned to it. Are there any ways to properly compare nextProp and prevProp with functional React components?

Your code sample seems to be working just fine. How exactly are you using the component? Try to run the snippet below:
const { useEffect, useRef, useState } = React;
const usePreviousValue = value => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
const MyComponent = ({ count }) => {
const prevCount = usePreviousValue(count);
return (<div> {count} | {prevCount}</div>);
}
function App() {
const [count, setCount] = useState(0);
return (
<div>
<MyComponent count={count} />
<button
onClick={() => setCount((prevCount) => prevCount + 1)}
>
Count++
</button>
</div>
);
}
ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>

As previously answered, the easiest way to do it is using a custom hook:
import isEqual from "lodash/isEqual";
import { useEffect, useRef } from "react";
const useComponentDidUpdate = (callback, data, checkIfIsEqual) => {
const prevData = useRef(data);
useEffect(() => {
const isTheSame = checkIfIsEqual ? isEqual(data, prevData) : undefined;
callback(prevData.current, isTheSame);
prevData.current = data;
}, [data]);
return null;
};
export default useComponentDidUpdate;
Then in your component:
const Component = ({age})=>{
const [state, setState] = useState({name: 'John', age})
useComponentDidUpdate(prevStateAndProps=>{
if(prevStateAndProps.age !== age || prevStateAndProps.state.name !== state.name){
// do something
}
}, {state, age})
...
}

Related

While rendering a component it is showing an error- "Cannot update a component (`App`) while rendering a different component (`EventList`). "

I Can't render my events. Its showing this error -
"Cannot update a component (App) while rendering a different component (EventList). To locate the bad setState() call inside EventList, follow the stack trace as described in https://reactjs.org/link/setstate-in-render"
Here is EventList Component code -
import { useEffect, useState } from "react";
import EventList from "../../event-list";
import EventForm from "../event-form";
const EventAction = ({
getEventsByClockID,
addEvent,
updateEvent,
clockID,
deleteEvent,
deleteEventsByClockID,
}) => {
const [isCreate, setIsCreate] = useState(false);
const [isToggle, setIsToggle] = useState(false);
const [eventState, setEventState] = useState(null)
const handleCreate = () => {
setIsCreate(!isCreate);
}
useEffect(() => {
setEventState(getEventsByClockID(clockID, true));
}, [isToggle])
const handleToggle = () => {
setIsToggle(!isToggle);
}
return (
<div>
<div>
<button onClick={handleCreate}>Create Event</button>
<button onClick={handleToggle}>Toggle Events</button>
</div>
{isCreate && (
<>
<h3>Create Event</h3>
<EventForm
clockID={clockID}
handleEvent={addEvent}
/>
</>
)}
{isToggle && (
<>
<h3>Events of this clock</h3>
<EventList
clockID={clockID}
eventState={eventState}
deleteEvent={deleteEvent}
updateEvent={updateEvent}
deleteEventsByClockID={deleteEventsByClockID}
/>
</>
)}
</div>
)
}
export default EventAction;
Here is my App Component Code -
import ClockList from "./components/clock-list";
import LocalClock from "./components/local-clock";
import useApp from "./hooks/useApp";
import { localClockInitState } from "./initialStates/clockInitState";
const App = () => {
const {
localClock,
clocks,
updateLocalClock,
createClock,
updateClock,
deleteClock,
getEventsByClockID,
addEvent,
deleteEvent,
updateEvent,
deleteEventsByClockID,
} = useApp(localClockInitState);
return (
<div>
<LocalClock
clock={localClock}
updateClock={updateLocalClock}
createClock={createClock}
/>
<ClockList
clocks={clocks}
localClock={localClock.date}
updateClock={updateClock}
deleteClock={deleteClock}
getEventsByClockID={getEventsByClockID}
addEvent={addEvent}
deleteEvent={deleteEvent}
updateEvent={updateEvent}
deleteEventsByClockID={deleteEventsByClockID}
/>
</div>
)
}
export default App;
and Here is my useApp hook -
import { useState } from "react";
import deepClone from "../utils/deepClone";
import generateID from "../utils/generateId";
import useEvents from "./useEvents";
const getID = generateID('clock');
const useApp = (initValue) => {
const [localClock, setLocalClock] = useState(deepClone(initValue));
const [clocks, setClocks] = useState([]);
const {
// events,
// getEvents,
getEventsByClockID,
addEvent,
deleteEvent,
deleteEventsByClockID,
updateEvent,
} = useEvents();
const updateLocalClock = (data) => {
setLocalClock({
...localClock,
...data,
})
}
const createClock = (clock) => {
clock.id = getID.next().value;
setClocks((prev) => ([
...prev, clock
]))
}
const updateClock = (updatedClock) => {
setClocks(clocks.map(clock => {
if(clock.id === updatedClock.id) return updatedClock;
return clock;
}));
}
const deleteClock = (id) => {
setClocks(clocks.filter(clock => clock.id !== id));
}
return {
localClock,
clocks,
updateLocalClock,
createClock,
updateClock,
deleteClock,
getEventsByClockID,
addEvent,
deleteEvent,
updateEvent,
deleteEventsByClockID,
}
}
export default useApp;
I want to show all events incorporated with each individual clock.

React: How to change props values

In my code, I replace these values
const [items, setItem] = useState<string[]>([]);
const [value, setValue] = useState('')
const [error, setValue]= useState('')
to this
type Props = {
items?: string[],
value?: string,
error?: string
}
and then change the following setItem, setValue, setValue which causes the following error
import React, { useRef, useState } from "react";
import ReactDOM from "react-dom";
import Chip from "#material-ui/core/Chip";
import TextField from "#material-ui/core/TextField";
type Props = {
items?: string[],
value?: string,
error?: string
}
export const TagActions = (props:Props) => {
const { items, value, error } = props;
// const [items, setItem] = useState<string[]>([]);
// const [value, setValue] = useState('')
// const [error, setError]= useState('')
const divRef = useRef<HTMLDivElement>(null)
const handleDelete = (item:any) => {
console.log("handleDelete", item)
const result = items?.filter(i => i !== item)
setItem(result)
};
const handleItemEdit = (item:any) =>{
console.log("handleItemEdit", item)
const result = items?.filter(i => i !== item)
items = result // setItem(result)
value = item // setValue(item)
console.log("value", value)
};
const handleKeyDown = (evt:any) => {
if (["Enter", "Tab", ","].includes(evt.key)) {
evt.preventDefault();
var test = value?.trim();
if (test && isValid(test)) {
items?.push(test)
setValue("")
}
}
};
const isValid = (email:any)=> {
let error = null;
if (isInList(email)) {
error = `${email} has already been added.`;
}
if (!isEmail(email)) {
error = `${email} is not a valid email address.`;
}
if (error) {
setError(error);
return false;
}
return true;
}
const isInList = (email:any)=> {
return items?.includes(email);
}
const isEmail = (email:any)=> {
return /[\w\d\.-]+#[\w\d\.-]+\.[\w\d\.-]+/.test(email);
}
const handleChange = (evt:any) => {
setValue(evt.target.value)
// setError("")
};
const handlePaste = (evt:any) => {
evt.preventDefault();
var paste = evt.clipboardData.getData("text");
var emails = paste.match(/[\w\d\.-]+#[\w\d\.-]+\.[\w\d\.-]+/g);
if (emails) {
var toBeAdded = emails.filter((email:any) => !isInList(email));
setItem(toBeAdded)
}
};
return (
<>
<div>
<TextField id="outlined-basic" variant="outlined"
InputProps={{
startAdornment: items?.map(item => (
<Chip
key={item}
tabIndex={-1}
label={item}
onDelete={() => handleDelete(item)}
onClick={() => handleItemEdit(item)}
/>
)),
}}
ref={divRef}
value={value}
placeholder="Type or paste email addresses and press `Enter`..."
onKeyDown={(e) => handleKeyDown(e)}
onChange={(e) => handleChange(e)}
onPaste={(e) => handlePaste(e)}
/>
</div>
{error && <p className="error">{error}</p>}
</>
);
}
I am a beginner in react typescript, so I don't know how to fix this, Please give me a solution to fix this problem
While changing const to let may fix immediate errors in the console, I doubt this will give you the behaviour that you desire.
The main issue here is that you are mutating the value of props, which in general you should never do. Props are used to pass stateful data down from a parent to a child component. If you wish to update the state of this data from the child component, you should pass an update function using props as well. Below gives an example of what I mean by this by implementing the delete item function (no typescript, but hopefully it gets the idea across):
const ParentComponent = () => {
const [items, setItems] = useState(["item1", "item2", "item3"])
const deleteItem = (itemToDelete) => {
//here we use the functional update form of setState which is good practise when the new state depends on the old state
setItems((items) => items.filter((item) => item!==itemToDelete))
}
return <ChildComponent items={items}, onDeleteItem={deleteItem} />
}
const ChildComponent = ({items, onDeleteItem}) => {
//e.g. to delete item2 call
onDeleteItem("item2")
}
This is one of the more confusing patterns in React, but it is very important to get your head around. Only the component where state is declared should actually be updating that state - as it is the only place where you have access to the setState function.

Problem with rendering object prop sent to child component

The problem is: Every time I click the button, I see "Button render" in the console. But I want to see this post only once
The problem is: Every time I click the button, I see "Button render" in the console. But I want to see this post only once
import React, { useState, useCallback } from "react";
import Button from "./Button";
const UseCallback = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount((prevState) => prevState + 1);
}, []);
return (
<div>
<p>{count}</p>
<Button
deneme={{ aaa: "aaa", bbb: "bbb" }}
handleClick={handleClick}
></Button>
</div>
);
};
export default UseCallback;
import React from "react";
const Button = ({ handleClick }) => {
console.log("Button - rerender");
return (
<div>
<button onClick={handleClick}>Sayacı artır</button>
</div>
);
};
export default React.memo(Button);
It is suppose to re-render since the callback is changing on render and you are passing a new object reference on each render to button. You already have React.memo but since you have a new object reference every time it's a re-rendering button.
Try wrapping object in useMemo to keep the same reference or create a variable at top and pass it in deneme prop
const UseCallback = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount((prevState) => prevState + 1);
}, []);
const deneme = useMemo(() => {
return { aaa: "aaa", bbb: "bbb" };
}, []);
return (
<div>
<p>{count}</p>
<Button deneme={deneme} handleClick={handleClick}></Button>
</div>
);
};
or
const deneme = {
aaa: "aaa",
bbb: "bbb"
};
const UseCallback = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount((prevState) => prevState + 1);
}, []);
return (
<div>
<p>{count}</p>
<Button handleClick={handleClick}></Button>
</div>
);
};
So only thing you are missing is to pass a compareFunction which will tell React.Memo exact condition when to rerender the component. You can see your code working here.
https://codesandbox.io/s/romantic-cherry-bv5y0?file=/src/App.js
import "./styles.css";
import React, { useState, useCallback } from "react";
const Button1 = ({ handleClick }) => {
console.log("Button - rerender");
return (
<div>
<button onClick={handleClick}>Sayacı artır</button>
</div>
);
};
const compareFunction = (prevProps, nextProps) => {
return true; //As props only have a function which we are sure doesn't change.
};
const Button = React.memo(Button1, compareFunction);
const UseCallback = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount((prevState) => prevState + 1);
}, []);
return (
<div>
<p>{count}</p>
<Button
deneme={{ aaa: "aaa", bbb: "bbb" }}
handleClick={handleClick}
></Button>
</div>
);
};
export default UseCallback;

Cannot get prevState value using userRef hook for array of objects

I am trying to compare old and new state values using custom hook usePrevious made with useRef hook where state consists of array of objects.
While printing the old value and current value, it returns current values in both cases, but when it's just the array of numbers or if it's the first render, it works well.
Also, https://codesandbox.io/s/4c4ie is the code for the test.
Is there any mistake I have done or there is something else to do to get old state and current state?
Below is the code I am using.
import React from 'react'
function usePrevious(value) {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
});
return ref.current;
}
function Playground() {
const [state, setState] = React.useState([{ value: 0 }]);
const prevState = usePrevious(state);
React.useEffect(() => {
console.log(prevState, state)
if (prevState !== state) {
try {
console.log(prevState[0].value)
console.log(state[0].value)
} catch (e) {
}
}
}, [JSON.stringify(state)])
// }, [state])
const _onClick = () => {
const tempState = [...state];
tempState[0].value = state[0].value + 1;
setState(tempState)
}
return (
<div>
<div>prevStateValue: {prevState ? prevState[0].value : 'undefined'}</div>
<div>stateValue: {state[0].value}</div>
<button onClick={_onClick}>click</button>
</div>
)
}
export default Playground
You were mutating state with: tempState[0].value = state[0].value + 1;
Here is a working snippet:
function usePrevious(value) {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
},[value]);//only set when value changed
return ref.current;
}
function App() {
const [state, setState] = React.useState([{ value: 0 }]);
const prevState = usePrevious(state);
React.useEffect(() => {
if (prevState !== state) {
try {
console.log(
'pref',
prevState[0].value,
'current',
state[0].value
);
} catch (e) {
console.log('not set yet');
}
}
}, [prevState, state]);
// }, [state])
const _onClick = () => {
const tempState = [...state];
//you were mutating state here
tempState[0] = {
...tempState[0],
value: tempState[0].value + 1,
};
setState(tempState);
};
return (
<div>
<div>
prevStateValue:{' '}
{prevState ? prevState[0].value : 'undefined'}
</div>
<div>stateValue: {state[0].value}</div>
<button onClick={_onClick}>click</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You are passing the reference of state to ref instead of the value. Cloning the object before assigning to ref.current will help.
function usePrevious(value) {
const ref = React.useRef();
React.useEffect(() => {
ref.current = JSON.parse(JSON.stringify(value));
});
return ref.current;
}

How to use callback with useState hook in react [duplicate]

This question already has answers here:
How to use `setState` callback on react hooks
(22 answers)
Closed 2 years ago.
I am using functional component with hooks. I need to update state in parent from a child. I am using a prop function in Parent.
All works fine except my prop function is getting the previous state and not the current state. My prop function gets executed before useState hook setting current state.
How can I can I wait for my call back function to be executed after useState call. I am looking for something like setState(state,callback) from class based components.
Here is the code snippet:
function Parent() {
const [Name, setName] = useState("");
getChildChange = getChildChange.bind(this);
function getChildChange(value) {
setName(value);
}
return <div> {Name} :
<Child getChildChange={getChildChange} ></Child>
</div>
}
function Child(props) {
const [Name, setName] = useState("");
handleChange = handleChange.bind(this);
function handleChange(ele) {
setName(ele.target.value);
props.getChildChange(collectState());
}
function collectState() {
return Name;
}
return (<div>
<input onChange={handleChange} value={Name}></input>
</div>);
}
You can use useEffect/useLayoutEffect to achieve this:
const SomeComponent = () => {
const [count, setCount] = React.useState(0)
React.useEffect(() => {
if (count > 1) {
document.title = 'Threshold of over 1 reached.';
} else {
document.title = 'No threshold reached.';
}
}, [count]);
return (
<div>
<p>{count}</p>
<button type="button" onClick={() => setCount(count + 1)}>
Increase
</button>
</div>
);
};
If you want to prevent the callback from running on first render, adjust the previous version:
const SomeComponent = () => {
const [count, setCount] = React.useState(0)
const didMount = React.useRef(false);
React.useEffect(() => {
if (!didMount.current) {
didMount.current = true;
return;
}
if (count > 1) {
document.title = 'Threshold of over 1 reached.';
} else {
document.title = 'No threshold reached.';
}
}, [count]);
return (
<div>
<p>{count}</p>
<button type="button" onClick={() => setCount(count + 1)}>
Increase
</button>
</div>
);
};
More about it over here.
setState(updater, callback) for useState
Following implementation comes really close to the original setState callback of classes.
Improvements made to accepted answer:
Callback execution is omitted on initial render - we only want to call it on state updates
Callback can be dynamic for each setState invocation, like with classes
Usage
const App = () => {
const [state, setState] = useStateCallback(0); // same API as useState
const handleClick = () => {
setState(
prev => prev + 1,
// second argument is callback, `s` being the *updated* state
s => console.log("I am called after setState, state:", s)
);
};
return <button onClick={handleClick}>Increment</button>;
}
useStateCallback
function useStateCallback(initialState) {
const [state, setState] = useState(initialState);
const cbRef = useRef(null); // init mutable ref container for callbacks
const setStateCallback = useCallback((state, cb) => {
cbRef.current = cb; // store current, passed callback in ref
setState(state);
}, []); // keep object reference stable, exactly like `useState`
useEffect(() => {
// cb.current is `null` on initial render,
// so we only invoke callback on state *updates*
if (cbRef.current) {
cbRef.current(state);
cbRef.current = null; // reset callback after execution
}
}, [state]);
return [state, setStateCallback];
}
TypeScript version
function useStateCallback<T>(
initialState: T
): [T, (state: T, cb?: (state: T) => void) => void] {
const [state, setState] = useState(initialState);
const cbRef = useRef<((state: T) => void) | undefined>(undefined); // init mutable ref container for callbacks
const setStateCallback = useCallback((state: T, cb?: (state: T) => void) => {
cbRef.current = cb; // store current, passed callback in ref
setState(state);
}, []); // keep object reference stable, exactly like `useState`
useEffect(() => {
// cb.current is `undefined` on initial render,
// so we only invoke callback on state *updates*
if (cbRef.current) {
cbRef.current(state);
cbRef.current = undefined; // reset callback after execution
}
}, [state]);
return [state, setStateCallback];
}
Further info: React Hooks FAQ: Is there something like instance variables?
Working example
const App = () => {
const [state, setState] = useStateCallback(0);
const handleClick = () =>
setState(
prev => prev + 1,
// important: use `s`, not the stale/old closure value `state`
s => console.log("I am called after setState, state:", s)
);
return (
<div>
<p>Hello Comp. State: {state} </p>
<button onClick={handleClick}>Click me</button>
</div>
);
}
function useStateCallback(initialState) {
const [state, setState] = useState(initialState);
const cbRef = useRef(null);
const setStateCallback = useCallback((state, cb) => {
cbRef.current = cb;
setState(state);
}, []);
useEffect(() => {
if (cbRef.current) {
cbRef.current(state);
cbRef.current = null;
}
}, [state]);
return [state, setStateCallback];
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
<div id="root"></div>
With React16.x and up, if you want to invoke a callback function on state change using useState hook, you can use the useEffect hook attached to the state change.
import React, { useEffect } from "react";
useEffect(() => {
props.getChildChange(name); // using camelCase for functions is recommended.
}, [name]); // this will call getChildChange on initial render and when ever name changes.
Actually, you should avoid using this when using react hooks. It causes side effects. That's why react team create react hooks.
If you remove codes that tries to bind this, you can just simply pass setName of Parent to Child and call it in handleChange. Cleaner code!
function Parent() {
const [Name, setName] = useState("");
return <div> {Name} :
<Child setName={setName} ></Child>
</div>
}
function Child(props) {
const [Name, setName] = useState("");
function handleChange(ele) {
setName(ele.target.value);
props.setName(ele.target.value);
}
return (<div>
<input onChange={handleChange} value={Name}></input>
</div>);
}
Moreover, you don't have to create two copies of Name(one in Parent and the other one in Child). Stick to "Single Source of Truth" principle, Child doesn't have to own the state Name but receive it from Parent. Cleanerer node!
function Parent() {
const [Name, setName] = useState("");
return <div> {Name} :
<Child setName={setName} Name={Name}></Child>
</div>
}
function Child(props) {
function handleChange(ele) {
props.setName(ele.target.value);
}
return (<div>
<input onChange={handleChange} value={props.Name}></input>
</div>);
}
we can write customise function which will call the callBack function if any changes in the state
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const useStateCallbackWrapper = (initilValue, callBack) => {
const [state, setState] = useState(initilValue);
useEffect(() => callBack(state), [state]);
return [state, setState];
};
const callBack = state => {
console.log("---------------", state);
};
function App() {
const [count, setCount] = useStateCallbackWrapper(0, callBack);
return (
<div className="App">
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>+</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
`
Another way to achieve this:
const [Name, setName] = useState({val:"", callback: null});
React.useEffect(()=>{
console.log(Name)
const {callback} = Name;
callback && callback();
}, [Name]);
setName({val:'foo', callback: ()=>setName({val: 'then bar'})})
you can utilize useCallback hook to do this.
function Parent() {
const [name, setName] = useState("");
const getChildChange = useCallback( (updatedName) => {
setName(updatedName);
}, []);
return <div> {name} :
<Child getChildChange={getChildChange} ></Child>
</div>
}
function Child(props) {
const [name, setName] = useState("");
function handleChange(ele) {
setName(ele.target.value);
props.getChildChange(ele.target.value);
}
function collectState() {
return name;
}
return (<div>
<input onChange={handleChange} value={name}></input>
</div>);
}
function Parent() {
const [Name, setName] = useState("");
getChildChange = getChildChange.bind(this);
function getChildChange(value) {
setName(value);
}
return <div> {Name} :
<Child getChildChange={getChildChange} ></Child>
</div>
}
function Child(props) {
const [Name, setName] = useState("");
handleChange = handleChange.bind(this);
collectState = collectState.bind(this);
function handleChange(ele) {
setName(ele.target.value);
}
function collectState() {
return Name;
}
useEffect(() => {
props.getChildChange(collectState());
});
return (<div>
<input onChange={handleChange} value={Name}></input>
</div>);
}
useEffect act as componentDidMount, componentDidUpdate, so after updating state it will work

Categories

Resources