Difference between usePreviousDistinct with useEffect and without - javascript

I needed a hook to get the previous distinct value of a specific state. It looks like this and it seems to work:
function usePreviousDistinct(state) {
const prevRef = useRef();
useEffect(() => {
prevRef.current = state;
}, [state]);
return prevRef.current;
}
I've also seen there is a usePreviousDistinct hook in the react-use package but the approach is different than mine.
import { useRef } from 'react';
import { useFirstMountState } from './useFirstMountState';
export type Predicate<T> = (prev: T | undefined, next: T) => boolean;
const strictEquals = <T>(prev: T | undefined, next: T) => prev === next;
export default function usePreviousDistinct<T>(value: T, compare: Predicate<T> = strictEquals): T | undefined {
const prevRef = useRef<T>();
const curRef = useRef<T>(value);
const isFirstMount = useFirstMountState();
if (!isFirstMount && !compare(curRef.current, value)) {
prevRef.current = curRef.current;
curRef.current = value;
}
return prevRef.current;
}
I wonder if I have not understood something or am missing something. Is my version also correct?
In my test I could not find a difference:
https://codesandbox.io/s/distracted-mayer-zpym8?file=/src/App.js

useEffect() together with useRef() (your version) does not show the latest value.
useRef() without useEffect() gives you the correct value, because it runs synchronously, but doesn't have the advantages that come with asynchronicity.
useEffect() together with useState() gives you the correct value, but might trigger unnecessary renders (adds potentially unnecessary overhead).
Your version looks like it works as expected, because the old value that is shown is the one that you expect to see as the new value. But the actual new value is not the one you want.
Example:
import React, { useState, useEffect, useRef } from 'react';
export const MyComponent = function(props){
const [state, setState ] = useState(0);
return <React.Fragment>
state: { state },<br />
with useEffect: { usePreviousDistinctUE( state ) },<br />
w/o useEffect: { usePreviousDistinctR( state ) },<br />
<button onClick={ function(){
setState( state + 1 );
} }>
increment
</button>
</React.Fragment>;
};
const usePreviousDistinctUE = function( value ){
const prevRef = useRef();
useEffect(() => {
prevRef.current = value;
console.log('with useEffect, prev:', prevRef.current, ', current:', value);
}, [value]);
return prevRef.current;
};
const usePreviousDistinctR = function( value ){
const prevRef = useRef();
const curRef = useRef( value );
if( curRef.current !== value ){
prevRef.current = curRef.current;
curRef.current = value;
}
console.log('w/o useEffect, prev:', prevRef.current, ', current:', curRef.current);
return prevRef.current;
};
The values shown on the page are the same, but in the console they are different. That means the value in the useEffect() version is changed, it is only not yet shown on the page.
If you just add another hook that updates anything unrelated (leaving everything else unchanged), then the page (might*) magically show the updated value again, because the page is re-rendered and the previously already changed value is shown. The value is now wrong in your eyes, but it is not changed, only shown:
// ...
with useEffect: { usePreviousDistinctUE( state ) },<br />
w/o useEffect: { usePreviousDistinctR( state ) },<br />
anything updated: { useAnythingUpdating( state ) },<br />
// ...
const useAnythingUpdating = function(state){
const [result, setResult ] = useState(0);
useEffect(() => {
setResult( state );
console.log('anything updated');
});
return result;
};
*But you shouldn't rely on something else triggering a re-render. I'm not even sure this would update as expected under all circumstances.
more Details:
useEffect() is triggered at some time when react decides that the prop must have been changed. The ref is changed then, but react will not 'get informed' about a ref change, so it doesn't find it necessary to re-render the page to show the changed value.
In the example without useEffect() the change happens synchronously.
React doesn't 'know' about this change either, but (if everything else runs as expected) there will be always a re-render when necessary anyway (you will have called that function from another function that is rendered at the end).
(Not informing react about the change is basically the point in using useRef(): sometimes you want just a value, under your own control, without react doing magic things with it.)

Related

React how can I change a state based on a different state, in the same function

import React, {useState, useEffect} from 'react';
const Test = ( {numar}) => {
const [likeStatus, setLikeStatus] = useState(true);
const [likeNumber, setLikeNumber] = useState(100);
const onLikeHandler = () => {
setLikeStatus(prevState => !prevState);
if(likeStatus){
setLikeNumber(prevState=> prevState +1)
} else {
setLikeNumber(prevState=>prevState-1);
}
}
console.log(likeStatus);
console.log(likeNumber);
return <button className={`like ${likeStatus ? 'liked' : ""}`} onClick={onLikeHandler}>{`Like | ${ likeNumber}`}</button>
}
export default Test;
I am trying to make a like button that likes/unlikes based on the click.
How can I make the second change state function wait for my first state function to finish? I tried using an use effect hook, and I am using the likeStatus in the dependecy array, but for some reason "the unlike" function triggers twice upon refresh"
State update is async, and state is being accessed from closure which recreates only after rerender - meaning you can not set state and then in next line to expect that you will get that new state immediately, you will get new state but only in next function call(after element rerenders). Solution is to preserve new state in temp variable.
Rewrite to this:
const onLikeHandler = () => {
const newLikeStatus = !likeStatus; // Here you are preserving that new state in temp variable
setLikeStatus(newLikeStatus);
if(newLikeStatus){
setLikeNumber(prevState=> prevState +1)
} else {
setLikeNumber(prevState=>prevState-1);
}
}

React component rendering even when there is no change in the state value

From the React Docs, what I have learnt is that the component will re-render only if there is a change in the value of a state.
For instance
import React, { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
console.log("I am rendering");
const handleButtonClick = () => {
setCount(0);
};
return (
<>
<button onClick={handleButtonClick}>Increment</button>
Count value is: {count}
</>
);
}
The message I am rendering is printed only once even if we click the button because the setCount function is setting the value to 0 which is the present value of count
Since there is no change in the present and future value therefore, the Component does not re-render.
Unexpected Behaviour
However, the similar behaviour is not observed when we add an extra line setCount(1) before setCount(0)
import React, { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
console.log("I am rendering");
const handleButtonClick = () => {
setCount(1); //this line has been added extra
setCount(0);
};
return (
<>
<button onClick={handleButtonClick}>Increment</button>
Count value is: {count}
</>
);
}
In principle, there is no change in the output of the final count value. However, if we click the button, the component re-renders and prints the message I am rendering
I could not find an explanation for this behaviour. Is this behaviour on expected lines?.
Shouldn't the component re-render only when the final value of the state is different from the current value ?
Sometimes, Reacts needs another render phase to decide if it needs a bailout. By the way, when we saying "bailout" meaning bailing out the Reconciliation process.
Notice the documentation on Bailing out a state update:
Note that React may still need to render that specific component again before bailing out.
Here is another example of such case demonstrating the idea:
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
const App = () => {
const [state, setState] = React.useState(0);
useEffect(() => {
console.log("B");
}, [state]);
console.log("A");
return (
<>
<h1>{state}</h1>
<button onClick={() => setState(42)}>Click</button>
</>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
You notice the next logs and their explanations:
A // First render
B // Mount
A // State change from 0 -> 42, triggers render
B // useEffect dep array change, triggers callback
A // **Our issue**, React needs another render
The value does change when you press the button. First, it changes to 1 then to 0 but this runs very fast that you don't get to see it.
to see this, you could add a setTimeout
const handleButtonClick = () => {
setCount(1); //this line has been added extra
setTimeout(() => {
setCount(0);
}, 500);
};

Prevent infinite loop on function component with default parameter and useEffect

When creating a function component in React and setting a default parameter everything works like expected and the component will be rendered once. But as soon as you add a hook like useEffect and use this parameter in the dependency array the component rerenders forever.
I've created a simple demo here: https://codesandbox.io/s/infinite-useeffect-loop-on-default-value-tv7hj?file=/src/TestComponent.jsx
The reason is quite obvious, because when using an object as default parameter, it will be created again and will not be equal to the previous one. And of course this doesn't happen on primitive default parameter values like number or string.
Is there any better way to avoid this side effect besides using defaultProps?
Yes, instead of setting the default value of value to being an object, just set it to false. Then check if value is truthy, if it is, then access the correct properties, otherwise, just show a default value. New code.
It would be something like:
import { useEffect, useState } from "react";
const TestComponent = ({ value = false }) => {
const [calcValue, setCalcValue] = useState(0);
useEffect(() => {
setCalcValue((cur) => cur + 1);
}, [value]);
return (
<div>
{value ? value.name : "Test"}:{calcValue}
</div>
);
};
The reason you get infinite loops is because the reference of value keeps changing.
The first time the component is rendered, it sees a new reference to value, which triggers the useEffect, which in turns modifies the state of the component, and this leads to a new render, which causes value to be re-created once again because the old reference to that variable has changed.
The easiest way to deal with this is to just create a default value outside the component and use that (basically the same as the defaultProps solution):
import { useEffect, useState } from "react";
const defaultValue = {name: "Test"}; // <-- default here
const TestComponent = ({ value = defaultValue }) => {
const [calcValue, setCalcValue] = useState(0);
useEffect(() => {
setCalcValue((cur) => cur + 1);
}, [value]);
return (
<div>
{value.name}:{calcValue}
</div>
);
};
Doing this will ensure that each time the component renders, it sees the same reference for value, therefore the useEffect hook only runs once.
Another way of dealing with this is to first wrap your component with memo, then create a new state variable which takes on the original value, and make your useEffect hook depend on this new state variable:
const TestComponent = React.memo(({ value = {name: "Test"} }) => {
const [calcValue, setCalcValue] = useState(0);
const [myValue, setMyValue] = useState(value);
useEffect(() => {
setCalcValue((cur) => cur + 1);
}, [myValue]);
return (
<div>
{myValue.name}:{calcValue}
</div>
);
});
The reason why we wrap the component with memo is so that it only re-renders after a state change if the prop had changed in value (instead of reference). You can change the way memo detects props changes by providing a custom comparison function as a second parameter.

React watch imported class property

I'm importing a plain class to my react (functional) component and want to be notified when an imported class property is set/updated. I've tried setting my imported class with just new, as a state variable with useState, as a ref with useRef - and have tried passing each one as a parameter to useEffect, but none of them are triggering the useEffect function when the property is updated a second time.
I've excluded all other code to drill down to the problem. I'm using Typescript, so my plain vanilla MyClass looks like this:
class MyClass {
userId: string
user: User?
constructor(userId: string){
this.userId = userId
// Do a network call to get the user
getUser().then(networkUser => {
// This works because I tried a callback here and can console.log the user
this.user = networkUser
}).catch(() => {})
}
}
And then in my component:
// React component
import { useEffect } from 'react'
import MyClass from './MyClass'
export default () => {
const myClass = new MyClass(userId)
console.log(myClass.user) // undefined here
useEffect(() => {
console.log(myClass.user) // undefined here and never called again after myClass.user is updated
}, [myClass.user])
return null
}
Again, this is greatly simplified. But the problem is that React is not re-rendering my component when the instance user object is updated from undefined to a User. This is all client side. How do I watch myClass.user in a way to trigger a re-render when it finally updates?
Let me guess you want to handle the business logic side of the app with OOP then relay the state back to functional React component to display.
You need a mechanism to notify React about the change. And the only way for React to be aware of a (view) state change is via a call to setState() somewhere.
The myth goes that React can react to props change, context change, state change. Fact is, props and context changes are just state change at a higher level.
Without further ado, I propose this solution, define a useWatch custom hook:
function useWatch(target, keys) {
const [__, updateChangeId] = useState(0)
// useMemo to prevent unnecessary calls
return useMemo(
() => {
const descriptor = keys.reduce((acc, key) => {
const internalKey = `##__${key}__`
acc[key] = {
enumerable: true,
configurable: true,
get() {
return target[internalKey]
},
set(value) {
if (target[internalKey] !== value) {
target[internalKey] = value
updateChangeId(id => id + 1) // <-- notify React about the change,
// the value's not important
}
}
}
return acc
}, {})
return Object.defineProperties(target, descriptor)
},
[target, ...keys]
)
}
Usage:
// React component
import { useEffect } from 'react'
import { useWatch } from './customHooks'
import MyClass from './MyClass'
export default () => {
const myClass = useMemo(() => new MyClass(userId), [userId])
useWatch(myClass, ['user'])
useEffect(() => {
console.log(myClass.user)
}, [myClass, myClass.user])
return null
}
Side Note
Not related to the question per se, but there're a few words I want to add about that myth I mentioned. I said:
props and context changes are just state change at a higher level
Examples:
props change:
function Mom() {
const [value, setValue] = useState(0)
setTimeout(() => setValue(v => v+1), 1000)
return <Kid value={value} />
}
function Dad() {
let value = 0
setTimeout(() => value++, 1000)
return <Kid value={value} />
}
function Kid(props) {
return `value: ${props.value}`
}
context change:
const Context = React.createContext(0)
function Mom() {
const [value, setValue] = useState(0)
setTimeout(() => setValue(v => v+1), 1000)
return (<Context.Provider value={value}>
<Kid />
</Context.Provider>)
}
function Dad() {
let value = 0
setTimeout(() => value++, 1000)
return (<Context.Provider value={value}>
<Kid />
</Context.Provider>)
}
function Kid() {
const value = React.useContext(Context)
return `value: ${value}`
}
In both examples, only <Mom /> can get <Kid /> to react to changes.
You can pass this.user as props and use props,.user in useEffeect. You could do that from the place getUser called.
A wholesome solution would be using a centralized state solution like redux or context API. Then you need to update store in getUser function and listen globalstate.user.
Conclusion
You need to pass this.user to the component one way or another. You need to choose according to the project.

How to force a functional React component to render?

I have a function component, and I want to force it to re-render.
How can I do so?
Since there's no instance this, I cannot call this.forceUpdate().
🎉 You can now, using React hooks
Using react hooks, you can now call useState() in your function component.
useState() will return an array of 2 things:
A value, representing the current state.
Its setter. Use it to update the value.
Updating the value by its setter will force your function component to re-render,
just like forceUpdate does:
import React, { useState } from 'react';
//create your forceUpdate hook
function useForceUpdate(){
const [value, setValue] = useState(0); // integer state
return () => setValue(value => value + 1); // update state to force render
// A function that increment 👆🏻 the previous state like here
// is better than directly setting `setValue(value + 1)`
}
function MyComponent() {
// call your hook here
const forceUpdate = useForceUpdate();
return (
<div>
{/*Clicking on the button will force to re-render like force update does */}
<button onClick={forceUpdate}>
Click to re-render
</button>
</div>
);
}
You can find a demo here.
The component above uses a custom hook function (useForceUpdate) which uses the react state hook useState. It increments the component's state's value and thus tells React to re-render the component.
EDIT
In an old version of this answer, the snippet used a boolean value, and toggled it in forceUpdate(). Now that I've edited my answer, the snippet use a number rather than a boolean.
Why ? (you would ask me)
Because once it happened to me that my forceUpdate() was called twice subsequently from 2 different events, and thus it was reseting the boolean value at its original state, and the component never rendered.
This is because in the useState's setter (setValue here), React compare the previous state with the new one, and render only if the state is different.
Update react v16.8 (16 Feb 2019 realease)
Since react 16.8 released with hooks, function components have the ability to hold persistent state. With that ability you can now mimic a forceUpdate:
function App() {
const [, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []);
console.log("render");
return (
<div>
<button onClick={forceUpdate}>Force Render</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
<div id="root"/>
Note that this approach should be re-considered and in most cases when you need to force an update you probably doing something wrong.
Before react 16.8.0
No you can't, State-Less function components are just normal functions that returns jsx, you don't have any access to the React life cycle methods as you are not extending from the React.Component.
Think of function-component as the render method part of the class components.
Official FAQ now recommends this way if you really need to do it:
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
function handleClick() {
forceUpdate();
}
Simplest way 👌
if you want to force a re-render, add a dummy state you can change to initiate a re-render.
const [rerender, setRerender] = useState(false);
...
setRerender(!rerender); //whenever you want to re-render
And this will ensure a re-render, And you can call setRerender(!rerender) anywhere, whenever you want :)
I used a third party library called
use-force-update
to force render my react functional components. Worked like charm.
Just use import the package in your project and use like this.
import useForceUpdate from 'use-force-update';
const MyButton = () => {
const forceUpdate = useForceUpdate();
const handleClick = () => {
alert('I will re-render now.');
forceUpdate();
};
return <button onClick={handleClick} />;
};
Best approach - no excess variables re-created on each render:
const forceUpdateReducer = (i) => i + 1
export const useForceUpdate = () => {
const [, forceUpdate] = useReducer(forceUpdateReducer, 0)
return forceUpdate
}
Usage:
const forceUpdate = useForceUpdate()
forceUpdate()
If you already have a state inside the function component and you don't want to alter it and requires a re-render you could fake a state update which will, in turn, re-render the component
const [items,setItems] = useState({
name:'Your Name',
status: 'Idle'
})
const reRender = () =>{
setItems((state) => [...state])
}
this will keep the state as it was and will make react into thinking the state has been updated
This can be done without explicitly using hooks provided you add a prop to your component and a state to the stateless component's parent component:
const ParentComponent = props => {
const [updateNow, setUpdateNow] = useState(true)
const updateFunc = () => {
setUpdateNow(!updateNow)
}
const MyComponent = props => {
return (<div> .... </div>)
}
const MyButtonComponent = props => {
return (<div> <input type="button" onClick={props.updateFunc} />.... </div>)
}
return (
<div>
<MyComponent updateMe={updateNow} />
<MyButtonComponent updateFunc={updateFunc}/>
</div>
)
}
The accepted answer is good.
Just to make it easier to understand.
Example component:
export default function MyComponent(props) {
const [updateView, setUpdateView] = useState(0);
return (
<>
<span style={{ display: "none" }}>{updateView}</span>
</>
);
}
To force re-rendering call the code below:
setUpdateView((updateView) => ++updateView);
None of these gave me a satisfactory answer so in the end I got what I wanted with the key prop, useRef and some random id generator like shortid.
Basically, I wanted some chat application to play itself out the first time someone opens the app. So, I needed full control over when and what the answers are updated with the ease of async await.
Example code:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// ... your JSX functional component, import shortid somewhere
const [render, rerender] = useState(shortid.generate())
const messageList = useRef([
new Message({id: 1, message: "Hi, let's get started!"})
])
useEffect(()=>{
async function _ () {
await sleep(500)
messageList.current.push(new Message({id: 1, message: "What's your name?"}))
// ... more stuff
// now trigger the update
rerender(shortid.generate())
}
_()
}, [])
// only the component with the right render key will update itself, the others will stay as is and won't rerender.
return <div key={render}>{messageList.current}</div>
In fact this also allowed me to roll something like a chat message with a rolling .
const waitChat = async (ms) => {
let text = "."
for (let i = 0; i < ms; i += 200) {
if (messageList.current[messageList.current.length - 1].id === 100) {
messageList.current = messageList.current.filter(({id}) => id !== 100)
}
messageList.current.push(new Message({
id: 100,
message: text
}))
if (text.length === 3) {
text = "."
} else {
text += "."
}
rerender(shortid.generate())
await sleep(200)
}
if (messageList.current[messageList.current.length - 1].id === 100) {
messageList.current = messageList.current.filter(({id}) => id !== 100)
}
}
If you are using functional components with version < 16.8. One workaround would be to directly call the same function like
import React from 'react';
function MyComponent() {
const forceUpdate = MyComponent();
return (
<div>
<button onClick={forceUpdate}>
Click to re-render
</button>
</div>
);
}
But this will break if you were passing some prop to it. In my case i just passed the same props which I received to rerender function.
For me just updating the state didn't work. I am using a library with components and it looks like I can't force the component to update.
My approach is extending the ones above with conditional rendering. In my case, I want to resize my component when a value is changed.
//hook to force updating the component on specific change
const useUpdateOnChange = (change: unknown): boolean => {
const [update, setUpdate] = useState(false);
useEffect(() => {
setUpdate(!update);
}, [change]);
useEffect(() => {
if (!update) setUpdate(true);
}, [update]);
return update;
};
const MyComponent = () => {
const [myState, setMyState] = useState();
const update = useUpdateOnChange(myState);
...
return (
<div>
... ...
{update && <LibraryComponent />}
</div>
);
};
You need to pass the value you want to track for change. The hook returns boolean which should be used for conditional rendering.
When the change value triggers the useEffect update goes to false which hides the component. After that the second useEffect is triggered and update goes true which makes the component visible again and this results in updating (resizing in my case).

Categories

Resources