How to pass state value to a callback function using React Hooks - javascript

Am creating a panel using react-modal, everything seems to be working fine.
But once the modal is opened, I need to set the top value of my modal. I am using hooks, and whenever am trying to update the value, getting into error, hooks can be called only inside the React functions. I tried all the possible ways but not able to solve the issue.
Expectation:
- OnAfterOpen should be called, and the latest state value should be passed to pos prop.
This is what I have tried.
function useTop() {
const [top, setTop] = useState('100px');
useEffect(() => {
const handleResize = () => {
const hasRefClientHeight = contentRef.current && contentRef.current.clientHeight;
hasRefClientHeight < window.innerHeight ? setTop(`calc(100% - ${hasRefClientHeight}px)`) : setTop('100px');
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return top;
}
const Panel = () => {
// const newTop = useTop(); -- This is not working as modal content is not ready. Modal Content will be ready only if `onAfterOpen` triggers.
<Modal pos={useTop} onAfterOpen={useTop}><div ref={contentRef}>Test</div></Modal>
};

Indeed, the rules of hooks says:
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. 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. (If you’re curious, we’ll explain this in depth below.)
This is necessary for React to be sure that hooks will be called on exact the same order in every render cycle.
It seems that Modal would call useTop only after mounted (opened), i.e. it will probably have skipped the first render, violating the rule of hooks and messing with the order.
One thing you can try:
Pass the ref for your useTop hook and handle it inside of it. If it's null, don't do anything, if it's already mounted, attach the listener and return the cleanup function.
const Panel = () => {
const newTop = useTop(contentRef);
<Modal pos={newTop} onAfterOpen={null}><div ref={contentRef}>Test</div></Modal>
};

Along with newTop you can return a function that triggers recalculation of the newTop. Similar to how useState does. And invoke it onAfterOpen.

Related

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"/>

How do I cause re-rendering to the component

How do I cause re-rendering to the component
function Bookmarks({ data }: any) {
const bookmarkedBlogs = useBookmarks().getBookmarkedBlogs(data.allMdx.nodes);
.....
}
when bookmarks change in the hook
function useBookmarks() {
const [bookmarks, setBookmarks, accessDenied] = useLocalStorage<BlogType['id'][]>('bookmarks', []);
const getBookmarkedBlogs = (blogs: BlogType[]) => {
return blogs.filter(checkIsBookmarked)
};
because as of now, even if I toggle bookmarks, the getBookmarkedBlogs function doesn't execute except in the initial render of the component.
How the implementation of useLocalStorage, and how you toggle bookmarks?
localStorage changes don't notify your every hooks except your make a observer model
if you don't make observer model, toggle bookmark in other hooks or other ways wouldn't notify your this hook, so it don't rerun
Your hook doesn't actually work.
It seems like you want a hook that subscribes to updates from some source outside the React render tree.
Here's a simple example of something that works like that:
function MyComp() {
let eventValue = useEventListener()
return (
<div>
{eventValue}
</div>
)
}
function useEventListener() {
let [ value, setValue ] = React.useState(1)
React.useEffect(() => {
setTimeout(() => setValue(5), 1000)
}, [])
return value
}
What makes it work is that the custom hook invokes a built-in hook when data should change. The React Hooks framework handles the rest.
Instead of a setTimeout, you could subscribe to an event stream, or listen for IPC messages, or something else. But:
The custom hook has to actually return something
The custom hook can only trigger a re-render by invoking a builtin hook
The custom hook must set up its subscribe-to-changes logic on its first invocation, which is often best handled by useEffect configured to run once

How do I avoid this stale closure?

I am working on building a simple React slider which will expose internal methods up to its parent via a ref and I am having trouble with what I suspect to be a stale closure, but I can't fully understand what is actually happening. Hoping someone can help me understand here.
Here is a simplified version of the code that I want to work:
const Slider = forwardRef((props, ref) => {
const sliderRef = useRef();
const [slides, dispatchSlides] = useReducer(reducer, []);
sliderRef.current = {
countSlides: () => {
return slides.length
},
};
useImperativeHandle(ref, () => sliderRef.current);
return null;
After this component mounts, its children will render and fill up the slides reducer with information on their positioning and visibility using IntersectionObserver. This part works, so I have kept it out of this example for simplicity. For our sake, just assume that slides is immediately populated with objects after mount, and that a user will manually call countObjects from the parent component much later after slides has been populated.
In the parent component, if I execute countSlides from the ref, I will always see slides.length === 0, no matter how many slides are actually present. I assume this is because the original countSlides method is a stale closure.
Now, what I don't fully understand, is that if I adjust this line:
useImperativeHandle(ref, () => sliderRef.current);
to this:
useImperativeHandle(ref, () => () => {
countSlides: () => sliderRef.current.countSlides()
});
the stale closure is fixed and everything works as intended. But this is duplicative code and I'm just not sure what is even different between the two cases. I do not want to repeat myself redefining many methods within the useImperativeHandle hook, but much more importantly, I want to understand what the difference is between the two examples above.
Thank you!
EDIT Adding full example:
https://codesandbox.io/s/ssr-slider-6ywf9
As you commented that the problem arose only when writing like onClick={ slider?.current?.prev } instead of onClick={() => { slider?.current?.prev() }}
I have tried with my sandbox that I provided and got the same problem.
There're a few things here:
useRef doesn't trigger re-renders itself, which means even a ref is updated, no re-renders follow.
Without re-renders, what's bound to onClick will not be updated.
So, if we write like onClick={slider?.current?.prev}, what happens is:
The ref is initially undefined, which means onClick is undefined as well
No re-render is triggered, so, even if ref is updated with a new value, onClick stays undefined
But, if we write like onClick={() => { slider?.current?.prev() }}, what happens is:
slider?.current?.prev is initially undefined
onClick is bound to that anonymous function
slider?.current?.prev is updated, we have the expected function
When the button is clicked, the function is called, which triggers the latest value of slider?.current?.prev

How can we implement componentWillUnmount using react hooks?

The method componentWillUnmount() is invoked immediately before a component is unmounted and destroyed. If we use useEffect with an empty array ([]) as the second argument and put our function in return statement it will be executed after the component is unmounted and even after another component will be mounted. This is done for performance reasons as far as I understand. In order not to delay rendering.
So the question is - how can we call some function using hooks before a component gets unmounted?
What I am trying to do is an application which saves user's input as he types (without submitting form). I use setInterval to save updated text every N seconds. And I need to force save updates before the component will unmount. I don't want to use prompt by react router before navigating. This is an electron application. I appreciate any thoughts or advice on how to implement such functionality.
Update
Unfortunately, Effects with Cleanup run after letting the browser paint. More details can be found here: So What About Cleanup?. It basically means that cleanup is run after a component is unmounted and it is not the same as executing code in componentWillUnmount(). I can clearly see the sequence of calls if I put console.log statements in the cleanup code and in another component. The question is whether we can execute some code before a component is unmounted using hooks.
Update2
As I can see I should better describe my use case. Let's imagine a theoretical app which holds its data in a Redux store. And we have two components with some forms. For simplicity, we don't have any backend or any async logic. We use only Redux store as data storage.
We don't want to update Redux store on every keystroke. So we keep actual values in the local component's state which we initialize with values from the store when a component mounts. We also create an effect which sets up a setInterval for 1s.
We have the following process. A User types something. Updates are stored in the local component state until our setInterval callback is called. The callback just puts data in the store (dispatches action). We put our callback in the useEffect return statement to force save to store when the component gets unmounted because we want to save data to store in this case as soon as possible.
The problem comes when a user types something in the first component and immediately goes to the second component (faster than 1s). Since the cleanup in our first component will be called after re-rendering, our store won't be updated before the second component gets mounted. And because of that, the second component will get outdated values to its local state.
If we put our callback in componentWillUnmount() it will be called before unmounting and the store will be updated before the next component mounts. So can we implement this using hooks?
componentWillUnmount can be simulated by returning a function inside the useEffect hook. The returned function will be called just before every rerendering of the component. Strictly speaking, this is the same thing but you should be able to simulate any behaviour you want using this.
useEffect(() => {
const unsubscribe = api.createSubscription()
return () => unsubscribe()
})
Update
The above will run every time there is a rerender. However, to simulate the behaviour only on mounting and unmounting (i.e. componentDidMount and componentWillUnmount). useEffect takes a second argument which needs to be an empty array.
useEffect(() => {
const unsubscribe = api.createSubscription()
return () => unsubscribe()
}, [])
See a more detailed explanation of the same question here.
Since the introduction of the useLayoutEffect hook, you can now do
useLayoutEffect(() => () => {
// Your code here.
}, [])
to simulate componentWillUnmount. This runs during unmount, but before the element has actually left the page.
The question here is how do you run code with hooks BEFORE unmount? The return function with hooks runs AFTER unmount and whilst that doesn’t make a difference for most use cases, their are some where it is a critical difference.
Having done a bit of investigation on this, I have come to the conclusion that currently hooks simply does not provide a direct alternative to componentWillUnmount. So if you have a use case that needs it, which is mainly for me at least, the integration of non-React libs, you just have to do it the old way and use a component.
Update: see the answer below about UseLayoutEffect() which looks like it may solve this issue.
I agree with Frank, but the code needs to look like this otherwise it will run only on the first render:
useLayoutEffect(() => {
return () => {
// Your code here.
}
}, [])
This is equivalent to ComponentWillUnmount
Similar to #pritam's answer, but with an abstracted code example. The whole idea of useRef is to allow you to keep track of the changes to the callback and not have a stale closure at the time of execution. Hence, the useEffect at the bottom can have an empty dependency array to ensure it only runs when the component unmounts. See the code demo.
Reusable hook:
type Noop = () => void;
const useComponentWillUnmount = (callback: Noop) => {
const mem = useRef<Noop>();
useEffect(() => {
mem.current = callback;
}, [callback]);
useEffect(() => {
return () => {
const func = mem.current as Noop;
func();
};
}, []);
};
After a bit of research, found that - you could still accomplish this. Bit tricky but should work.
You can make use of useRef and store the props to be used within a closure such as render useEffect return callback method
function Home(props) {
const val = React.useRef();
React.useEffect(
() => {
val.current = props;
},
[props]
);
React.useEffect(() => {
return () => {
console.log(props, val.current);
};
}, []);
return <div>Home</div>;
}
DEMO
However a better way is to pass on the second argument to useEffect so that the cleanup and initialisation happens on any change of desired props
React.useEffect(() => {
return () => {
console.log(props.current);
};
}, [props.current]);
I got in a unique situation where the useEffect(() => () => { ... }, []); answers did not work for me. This is because my component never got rendered — I was throwing an exception before I could register the useEffect hook.
function Component() {
useEffect(() => () => { console.log("Cleanup!"); }, []);
if (promise) throw promise;
if (error) throw error;
return <h1>Got value: {value}</h1>;
}
In the above example, by throwing a Promise<T> that tells react to suspend until the promise is resolved. However, once the promise is resolved, an error is thrown. Since the component never gets rendered and goes straight to an ErrorBoundary, the useEffect() hook is never registered!
If you're in a similar situation as myself, this little code may help:
To solve this, I modified my ErrorBoundary code to run a list of teardowns once it was recovered
export default class ErrorBoundary extends Component {
// ...
recover() {
runTeardowns();
// ...
}
// ...
}
Then, I created a useTeardown hook which would add teardowns that needed to be ran, or make use of useEffect if possible. You'll most likely need to modify it if you have nesting of error boundaries, but for my simple usecase, it worked wonderfully.
import React, { useEffect, useMemo } from "react";
const isDebugMode = import.meta.env.NODE_ENV === "development";
const teardowns: (() => void)[] = [];
export function runTeardowns() {
const wiped = teardowns.splice(0, teardowns.length);
for (const teardown of wiped) {
teardown();
}
}
type Teardown = { registered?: boolean; called?: boolean; pushed?: boolean } & (() => unknown);
/**
* Guarantees a function to run on teardown, even when errors occur.
*
* This is necessary because `useEffect` only runs when the component doesn't throw an error.
* If the component throws an error before anything renders, then `useEffect` won't register a
* cleanup handler to run. This hook **guarantees** that a function is called when the component ends.
*
* This works by telling `ErrorBoundary` that we have a function we would like to call on teardown.
* However, if we register a `useEffect` hook, then we don't tell `ErrorBoundary` that.
*/
export default function useTeardown(onTeardown: () => Teardown, deps: React.DependencyList) {
// We have state we need to maintain about our teardown that we need to persist
// to other layers of the application. To do that, we store state on the callback
// itself - but to do that, we need to guarantee that the callback is stable. We
// achieve this by memoizing the teardown function.
const teardown = useMemo(onTeardown, deps);
// Here, we register a `useEffect` hook to run. This will be the "happy path" for
// our teardown function, as if the component renders, we can let React guarantee
// us for the cleanup function to be ran.
useEffect(() => {
// If the effect gets called, that means we can rely on React to run our cleanup
// handler.
teardown.registered = true;
return () => {
if (isDebugMode) {
// We want to ensure that this impossible state is never reached. When the
// `runTeardowns` function is called, it should only be ran for teardowns
// that have not been able to be hook into `useEffect`.
if (teardown.called) throw new Error("teardown already called, but unregistering in useEffect");
}
teardown();
if (isDebugMode) {
// Because `teardown.registered` will already cover the case where the effect
// handler is in charge of running the teardown, this isn't necessary. However,
// this helps us prevent impossible states.
teardown.called = true;
}
};
}, deps);
// Here, we register the "sad path". If there is an exception immediately thrown,
// then the `useEffect` cleanup handler will never be ran.
//
// We rely on the behavior that our custom `ErrorBoundary` component will always
// be rendered in the event of errors. Thus, we expect that component to call
// `runTeardowns` whenever it deems it appropriate to run our teardowns.
// Because `useTeardown` will get called multiple times, we want to ensure we only
// register the teardown once.
if (!teardown.pushed) {
teardown.pushed = true;
teardowns.push(() => {
const useEffectWillCleanUpTeardown = teardown.registered;
if (!useEffectWillCleanUpTeardown) {
if (isDebugMode) {
// If the useEffect handler was already called, there should be no way to
// re-run this teardown. The only way this impossible state can be reached
// is if a teardown is called multiple times, which should not happen during
// normal execution.
const teardownAlreadyCalled = teardown.called;
if (teardownAlreadyCalled) throw new Error("teardown already called yet running it in runTeardowns");
}
teardown();
if (isDebugMode) {
// Notify that this teardown has been called - useful for ensuring that we
// cannot reach any impossible states.
teardown.called = true;
}
}
});
}
}
It does not matter wether the returned function from useEffect gets called before or after the component unmounted: You still have access to the states valuey through the closure:
const [input, setInput] = useState(() => Store.retrieveInput());
useEffect(() => {
return () => Store.storeInput(input); // < you can access "input" here, even if the component unmounted already
}, []);
If you don't manage the input in the components state, your whole structure is broken and should be changed to manage state at the right place. In your case, you should lift the shared input state of the components to the parent.
ReactJS docs on hooks specify this:
Effects may also optionally specify how to “clean up” after them by
returning a function.
So any function you return in your useEffect hook, will be executed when the component unmounts, as well as before re-running the effect due to a subsequent render.

What is the intention of using React's useCallback hook in place of useEffect?

I'm trying to understand what the use case is for using React's useCallback hook in place of the useEffect hook.
They both appear to act as a listener for state changes of their inputs (examples taken from the React Docs):
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
But, the useEffect hook gives the additional benefit of cleaning up resources where you would have previously with componentWillUnmount.
So, what is a good use case for using useCallback? And, what am I missing here?
useEffect has very specific timing aspects related to it that you can read about here. The function specified will be executed after rendering is complete and the DOM has been updated. This will happen after each rendering where any of the values specified in the second-argument array change.
useCallback doesn't automatically execute anything. It returns a function that can be executed by whatever code needs to trigger it. There is no listening to changes that causes an execution of the callback. The array values just control what instance of the function is returned. The array values do not control the timing of the function execution.
A key use case is to pass this function as a prop to a child component to use as an event handler. useCallback allows you to define an inline function to use as an event handler (thus it has access to any other variables in the context where the function is defined) without the downside of passing a unique prop to the child every render. So long as the values in the second-argument array have not changed, the same function will be returned as was returned the previous rendering. So if the child component is a pure component, it will not be forced to re-render simply because of always receiving a unique event handler function.
without useCallback
const Parent = ()=> {
const [a, setA] = useState(null);
const eventHandler = ()=> {
// every render creates a unique instance of eventHandler
// even though it always does the same thing so long as 'a' hasn't changed
doSomethingWithA(a);
}
return <Child onClick={eventHandler}/>
}
with useCallback
const Parent = ()=> {
const [a, setA] = useState(null);
const eventHandler = useCallback(()=> {
// A unique function instance is passed in to useCallback on every render, but
// eventHandler will be set to the first instance of this function
// (i.e. potentially an instance of the function that was passed to useCallback
// on a previous rendering) that was passed to useCallback
// for the current value of 'a'.
doSomethingWithA(a);
}, [a]);
return <Child onClick={eventHandler}/>
}
This article provides a bit more detail than the React docs on the use case for useCallback and other hooks.
Related answer: Trouble with simple example of React Hooks useCallback

Categories

Resources