How can we implement componentWillUnmount using react hooks? - javascript

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.

Related

What is the correct way to memoize this useEffect dependency?

I use useEffect to listen for a change in location.pathname to auto-scroll back to the top of the page (router) when the route changes. As I have a page transition (that takes pageTransitionTime * 1000 seconds), I use a timer that waits for the page transition animation to occur before the reset takes place. However, on the first load/mount of the router (after a loading page), I do NOT want to wait for the animation as there is no page animation.
Observe the code below, which works exactly as intended:
useEffect(() => {
const timer = setTimeout(() => {
window.scrollTo(0,0)
}, firstVisit.app ? 0 : pageTransitionTime * 1000 )
return () => clearTimeout(timer)
}, [location.pathname, pageTransitionTime])
The error I face here is that firstVisit.app isn't in the dependency array. I get this error on Terminal:
React Hook useEffect has a missing dependency: 'firstVisit.app'. Either include it or remove the dependency array react-hooks/exhaustive-deps
firstVisit.app is a global redux variable that is updated in the same React component by another useEffect, setting it to false as soon as the router is mounted (this useEffect has no dependency array, so it achieves it purpose instantly).
// UPON FIRST MOUNT, SET firstVisit.app TO FALSE
useEffect(() => {
if (firstVisit.app) {
dispatch(setFirstVisit({
...firstVisit,
app: false
}))
}
})
The problem is, when I include firstVisit.app in the dependency array in the first useEffect, the page will auto-reset scroll to (0,0) after pageTransitionTime, affecting the UX.
A bit of Googling lead me to find that I may need to memoize firstVisit.app but I'm not entirely sure how or the logic behind doing so?
React's log message tried to say that in your first useEffect hook callback function body you used some value that excluded in the list of dependencies. That's correct, please refer: useEffect Hook
The array of dependencies is not passed as arguments to the effect
function. Conceptually, though, that’s what they represent: every
value referenced inside the effect function should also appear in the
dependencies array. In the future, a sufficiently advanced compiler
could create this array automatically.
We recommend using the exhaustive-deps rule as part of our
eslint-plugin-react-hooks package. It warns when dependencies are
specified incorrectly and suggests a fix.
So, add to the second array parameter of useEffect firstVisit.app, and then check whether this warning/error is gone or not.
Also, you don't need to memoize primitive values, like boolean values (true/false), React is smart enough to not run your callback function again, when your boolean dependency hasn't changed after re-rendering.
[Edit]
If you want to run some logic in useEffect except initial render, i.e. to skip the first time render, you can use:
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) {
doYourTransition();
} else {
isMounted.current = true;
}
}, [deps]);
More complicated one with custom hook, but can be helpful based on your needs and wishes:
const useEffectAfterMount = (cb, deps) => {
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) {
cb();
} else {
isMounted.current = true;
}
}, deps);
}
useEffectAfterMount(() => {
// the logic here runs always, but not on initial render
}, [firstVisit.app]);
I think, you got the idea. Of course, you can move these hooks into a separate file and to reuse them across the project and minimize the amount of code in your current component as well.

For some reason I am getting 4 responses when I should be getting one using axios? [duplicate]

I have a counter and a console.log() in an useEffect to log every change in my state, but the useEffect is getting called two times on mount. I am using React 18. Here is a CodeSandbox of my project and the code below:
import { useState, useEffect } from "react";
const Counter = () => {
const [count, setCount] = useState(5);
useEffect(() => {
console.log("rendered", count);
}, [count]);
return (
<div>
<h1> Counter </h1>
<div> {count} </div>
<button onClick={() => setCount(count + 1)}> click to increase </button>
</div>
);
};
export default Counter;
useEffect being called twice on mount is normal since React 18 when you are in development with StrictMode. Here is an overview of what they say in the documentation:
In the future, we’d like to add a feature that allows React to add and remove sections of the UI while preserving state. For example, when a user tabs away from a screen and back, React should be able to immediately show the previous screen. To do this, React will support remounting trees using the same component state used before unmounting.
This feature will give React better performance out-of-the-box, but requires components to be resilient to effects being mounted and destroyed multiple times. Most effects will work without any changes, but some effects do not properly clean up subscriptions in the destroy callback, or implicitly assume they are only mounted or destroyed once.
To help surface these issues, React 18 introduces a new development-only check to Strict Mode. This new check will automatically unmount and remount every component, whenever a component mounts for the first time, restoring the previous state on the second mount.
This only applies to development mode, production behavior is unchanged.
It seems weird, but in the end, it's so we write better React code, bug-free, aligned with current guidelines, and compatible with future versions, by caching HTTP requests, and using the cleanup function whenever having two calls is an issue. Here is an example:
/* Having a setInterval inside an useEffect: */
import { useEffect, useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount((count) => count + 1), 1000);
/*
Make sure I clear the interval when the component is unmounted,
otherwise, I get weird behavior with StrictMode,
helps prevent memory leak issues.
*/
return () => clearInterval(id);
}, []);
return <div>{count}</div>;
};
export default Counter;
In this very detailed article called Synchronizing with Effects, React team explains useEffect as never before and says about an example:
This illustrates that if remounting breaks the logic of your application, this usually uncovers existing bugs. From the user’s perspective, visiting a page shouldn’t be different from visiting it, clicking a link, and then pressing Back. React verifies that your components don’t break this principle by remounting them once in development.
For your specific use case, you can leave it as it's without any concern. And you shouldn't try to use those technics with useRef and if statements in useEffect to make it fire once, or remove StrictMode, because as you can read on the documentation:
React intentionally remounts your components in development to help you find bugs. The right question isn’t “how to run an Effect once”, but “how to fix my Effect so that it works after remounting”.
Usually, the answer is to implement the cleanup function. The cleanup function should stop or undo whatever the Effect was doing. The rule of thumb is that the user shouldn’t be able to distinguish between the Effect running once (as in production) and a setup → cleanup → setup sequence (as you’d see in development).
/* As a second example, an API call inside an useEffect with fetch: */
useEffect(() => {
const abortController = new AbortController();
const fetchUser = async () => {
try {
const res = await fetch("/api/user/", {
signal: abortController.signal,
});
const data = await res.json();
} catch (error) {
if (error.name !== "AbortError") {
/* Logic for non-aborted error handling goes here. */
}
}
};
fetchUser();
/*
Abort the request as it isn't needed anymore, the component being
unmounted. It helps avoid, among other things, the well-known "can't
perform a React state update on an unmounted component" warning.
*/
return () => abortController.abort();
}, []);
You can’t “undo” a network request that already happened, but your cleanup function should ensure that the fetch that’s not relevant anymore does not keep affecting your application.
In development, you will see two fetches in the Network tab. There is nothing wrong with that. With the approach above, the first Effect will immediately get cleaned... So even though there is an extra request, it won’t affect the state thanks to the abort.
In production, there will only be one request. If the second request in development is bothering you, the best approach is to use a solution that deduplicates requests and caches their responses between components:
function TodoList() {
const todos = useSomeDataFetchingLibraryWithCache(`/api/user/${userId}/todos`);
// ...
Update: Looking back at this post, slightly wiser, please do not do this.
Use a ref or make a custom hook without one.
import type { DependencyList, EffectCallback } from 'react';
import { useEffect } from 'react';
const useClassicEffect = import.meta.env.PROD
? useEffect
: (effect: EffectCallback, deps?: DependencyList) => {
useEffect(() => {
let subscribed = true;
let unsub: void | (() => void);
queueMicrotask(() => {
if (subscribed) {
unsub = effect();
}
});
return () => {
subscribed = false;
unsub?.();
};
}, deps);
};
export default useClassicEffect;

Why is React state not updated inside functions? [duplicate]

This question already has answers here:
React - useState - why setTimeout function does not have latest state value?
(2 answers)
Closed 7 months ago.
I have a component that renders a table with objects. This component shows a button that, when pressed, sends the parent a specific object. The parent must set it in the state to display some graphical stuff. The rendering is working correctly, what I don't understand is why I am getting an outdated value after setting the state correctly.
It's not a race condition, React is simply ignoring the updated value of a variable, even when it re-renders the component correctly.
A minimal example:
import { useState } from "react";
import { SomeComponent } from "./SomeComponent";
export default function App() {
const [currentID, setCurrentID] = useState(null);
function getData() {
console.log("Getting data of: ", currentID); // PROBLEM: this is null
}
function setAndRetrieveData(value) {
setCurrentID(value);
// Just to show the problem and discard race conditions.
setTimeout(() => {
getData();
}, 1500);
}
return (
<div className="App">
<h1>Current ID: {currentID}</h1> {/* This works fine */}
<SomeComponent getInfoFor={setAndRetrieveData} />
</div>
);
}
SomeComponent:
export function SomeComponent(props) {
const randomID = 45;
return <button onClick={() => props.getInfoFor(randomID)}>Get info</button>;
}
Even with solutions like useStateCallback the problem persists.
Is there a way to do this without having to use the awful useEffect which is not clear when reading the code? Because the logic of the system is "when this button is pressed, make a request to obtain the information", using the hook useEffect the logic becomes "when the value of currentID changes make a request", if at some point I want to change the state of that variable and perform another action that is not to obtain the data from the server then I will be in trouble.
Thanks in advance
I think this is an issue with the way Javascript closures work.
When you execute a function, it gets bundled with all the data that pertains to it and then gets executed.
The issue is that you call this:
setTimeout(() => {
getData();
}, 1500);
inside setAndRetrieveData(value).
Even though it's inside a setTimeout, the getData() function has been bundled with the information it needs (currentID) at that point in time, not when it actually runs. So it gets bundled with the currentId before the state update takes place
Unfortunately, I would recommend using useEffect. This is the best way to ensure you avoid issues like this and any potential race conditions. Hopefully someone else can provide a different approach!
when setAndRetrieveData is called it sets a state that leads to the component being rerendered to reflect the new state. When the timeout finishes The function getData was created in the previous render. And thus only has access to the state variable from the previous render. That now is undefined.
what you could try is using a useEffect hook that that listens to changes of
currentID.
useEffect(() => {
const timeoutId = setTimeout(() => {
// Do something with the updated value
},1000);
return () => {
// if the data updates prematurely
// we cancel the timeout and start a new one
clearTimeout(timeoutId);
}
},[currentID])

Why useEffect running twice and how to handle it well in React?

I have a counter and a console.log() in an useEffect to log every change in my state, but the useEffect is getting called two times on mount. I am using React 18. Here is a CodeSandbox of my project and the code below:
import { useState, useEffect } from "react";
const Counter = () => {
const [count, setCount] = useState(5);
useEffect(() => {
console.log("rendered", count);
}, [count]);
return (
<div>
<h1> Counter </h1>
<div> {count} </div>
<button onClick={() => setCount(count + 1)}> click to increase </button>
</div>
);
};
export default Counter;
useEffect being called twice on mount is normal since React 18 when you are in development with StrictMode. Here is an overview of what they say in the documentation:
In the future, we’d like to add a feature that allows React to add and remove sections of the UI while preserving state. For example, when a user tabs away from a screen and back, React should be able to immediately show the previous screen. To do this, React will support remounting trees using the same component state used before unmounting.
This feature will give React better performance out-of-the-box, but requires components to be resilient to effects being mounted and destroyed multiple times. Most effects will work without any changes, but some effects do not properly clean up subscriptions in the destroy callback, or implicitly assume they are only mounted or destroyed once.
To help surface these issues, React 18 introduces a new development-only check to Strict Mode. This new check will automatically unmount and remount every component, whenever a component mounts for the first time, restoring the previous state on the second mount.
This only applies to development mode, production behavior is unchanged.
It seems weird, but in the end, it's so we write better React code, bug-free, aligned with current guidelines, and compatible with future versions, by caching HTTP requests, and using the cleanup function whenever having two calls is an issue. Here is an example:
/* Having a setInterval inside an useEffect: */
import { useEffect, useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount((count) => count + 1), 1000);
/*
Make sure I clear the interval when the component is unmounted,
otherwise, I get weird behavior with StrictMode,
helps prevent memory leak issues.
*/
return () => clearInterval(id);
}, []);
return <div>{count}</div>;
};
export default Counter;
In this very detailed article called Synchronizing with Effects, React team explains useEffect as never before and says about an example:
This illustrates that if remounting breaks the logic of your application, this usually uncovers existing bugs. From the user’s perspective, visiting a page shouldn’t be different from visiting it, clicking a link, and then pressing Back. React verifies that your components don’t break this principle by remounting them once in development.
For your specific use case, you can leave it as it's without any concern. And you shouldn't try to use those technics with useRef and if statements in useEffect to make it fire once, or remove StrictMode, because as you can read on the documentation:
React intentionally remounts your components in development to help you find bugs. The right question isn’t “how to run an Effect once”, but “how to fix my Effect so that it works after remounting”.
Usually, the answer is to implement the cleanup function. The cleanup function should stop or undo whatever the Effect was doing. The rule of thumb is that the user shouldn’t be able to distinguish between the Effect running once (as in production) and a setup → cleanup → setup sequence (as you’d see in development).
/* As a second example, an API call inside an useEffect with fetch: */
useEffect(() => {
const abortController = new AbortController();
const fetchUser = async () => {
try {
const res = await fetch("/api/user/", {
signal: abortController.signal,
});
const data = await res.json();
} catch (error) {
if (error.name !== "AbortError") {
/* Logic for non-aborted error handling goes here. */
}
}
};
fetchUser();
/*
Abort the request as it isn't needed anymore, the component being
unmounted. It helps avoid, among other things, the well-known "can't
perform a React state update on an unmounted component" warning.
*/
return () => abortController.abort();
}, []);
You can’t “undo” a network request that already happened, but your cleanup function should ensure that the fetch that’s not relevant anymore does not keep affecting your application.
In development, you will see two fetches in the Network tab. There is nothing wrong with that. With the approach above, the first Effect will immediately get cleaned... So even though there is an extra request, it won’t affect the state thanks to the abort.
In production, there will only be one request. If the second request in development is bothering you, the best approach is to use a solution that deduplicates requests and caches their responses between components:
function TodoList() {
const todos = useSomeDataFetchingLibraryWithCache(`/api/user/${userId}/todos`);
// ...
Update: Looking back at this post, slightly wiser, please do not do this.
Use a ref or make a custom hook without one.
import type { DependencyList, EffectCallback } from 'react';
import { useEffect } from 'react';
const useClassicEffect = import.meta.env.PROD
? useEffect
: (effect: EffectCallback, deps?: DependencyList) => {
useEffect(() => {
let subscribed = true;
let unsub: void | (() => void);
queueMicrotask(() => {
if (subscribed) {
unsub = effect();
}
});
return () => {
subscribed = false;
unsub?.();
};
}, deps);
};
export default useClassicEffect;

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

Categories

Resources