Can I get away with not using useEffect in this instance? - javascript

I'm still learning React and the proper way to do things. In this React project I am using the TinyMCE editor: https://www.tiny.cloud/docs/tinymce/6/react-cloud/
I want to display a warning message about unsaved changes once the editor has been made "dirty" that is that the user has modified the default content you get on reload.
The component that contains the editor has the following code:
<Editor
apiKey='asdf'
onInit={(_evt, editor) => editorRef.current = editor}
onDirty={(_evt, _editor) => setEditorWasModified(true)}
...
</Editor>
Finally I have just put the code directly into the React component itself where it overwrites the window.onbeforeunload event:
const [editorWasModfied, setEditorWasModified] = useState(false);
const editorRef = useRef<TinyMCEEditor | null>(null);
window.onbeforeunload = () => {
if (editorWasModfied) {
return "You have unsaved changes. Are you sure you want to leave?";
}
};
This code seems to run and work well.
So is my question here is this the correct approach to do this? Or do I need to use an useEffect hook or something similar? If I understand useEffect correctly, it runs on every render which is not something I want. I know there is a second argument, but since it refers to the editorWasModified variable, I get a warning that the dependency array should contain it, which is false since I only want the useEffect once to update the window.onbeforeunload event.

What you have can work, though I'd consider it to be inelegant - every time there's a re-render, you're attaching a new beforeunload listener (and removing the previous one), no matter whether it's necessary or not. You're also not removing the listener when the component dismounts (which would be something to keep in mind if the page this is on has could exist without this component being mounted). After this component is unmounted, it'd be best for the listener with the editorWasModfied check to be removed as well.
If I understand useEffect correctly, it runs on every render which is not something I want.
Well, you're attaching it on every render currently anyway.
I know there is a second argument, but since it refers to the editorWasModified variable, I get a warning that the dependency array should contain it, which is false since I only want the useEffect once to update the window.onbeforeunload event.
The usual way to fix this lint issue is to attach a new listener whenever the state values it references changes. Using useEffect will also let you remove the listener when the component unmounts.
useEffect(() => {
const handler = () => {
if (editorWasModfied) { // perhaps fix typo: editorWasModfied -> editorWasModified
return "You have unsaved changes. Are you sure you want to leave?";
}
};
window.addEventListener('beforeunload', handler);
return () => window.removeEventListener('beforeunload', handler);
}, [editorWasModfied]);

You are correct that useEffect runs on every render (if no dependency array was specified), but so does your code without useEffect.
Here are the three different useEffect "settings":
Runs on every render:
useEffect(() => {
})
Run once:
useEffect(() => {
}, [])
Runs everytime editorWasModfied changes:
useEffect(() => {
}, [editorWasModfied])
Sometimes we only want the useEffect to run once, meaning we don't want any dependencies in our dependency array (second argument), but the linter complains about it.
WARNING Don't use the following as an easy escape from the linter warnings. You should always try to write code following linter rules!
With that said, to suppress this linter warning, you can use // eslint-disable-next-line, but only do this if you are absolutely sure that what you are doing is correct (suppressing the linter is an edge case and is not recommended unless absolutely necessary):
useEffect(() => {
// eslint-disable-next-line
}, [])

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.

Why does React/Typescript complain want me to use my return function in useEffect dependency array?

I'm making a checkbox component for my project, and I have it set so that whenever is isChecked state changes, useEffect will run a returnFunction (defined in the props), like so:
export function JCheckbox({ returnFunction }: JCheckboxProps) {
const [isChecked, setIsChecked] = React.useState(defaultState)
React.useEffect(() => {
returnFunction(isChecked)
}, [isChecked])
When I do this, my compiler wants me to include returnFunction in the useEffect dependency array. I'm confused why I would need to track that, it's not used at all anywhere else in the function, and it doesn't change either. The code works perfectly fine without it.
You should add your function (returnFunction) to useEffect dependency because your function reference (returnFunction) may be changed, So react ask you to add your function (returnFunction) in useEffect dependency to recognize that and update them by new function reference.
returnFunction is certainly used as it is called inside the effect, and it could certainly change as the parent component could pass in different props, in which case your component would continue to call the previously passed in function. So the linter is correct, returnFunction is a dependency. However adding it makes it slightly more difficult to detect changes of isChecked, as changing returnFunction would also trigger the effect. Thus it seems way more elegant to call returnFunction from within setIsChecked:
const [isChecked, _setIsChecked] = useState(false);
function setIsChecked(value: boolean) {
returnFunction(value);
_setIsChecked(value);
}
However in the case given it seems that the state should rather be maintained by the parent component, consider lifting it up.
returnFunction is read from props so you probably must include it in the array of dependencies.
You should almost always include every variable and function you declare inside React and use in useEffect callback in the array of dependencies.
Eslint does not expect you to include setState functions and refs that you create in the same component that you execute useEffect.
Edit:
If you have a handleChange event handler and isChecked is toggled whenever the onChange method is invoked, you probably do not need useEffect. Call returnFunction function with the new isChecked value.

React hook - useEffect missing dependencies warning

I am not sure if this is a valid warning by using useEffect around the dependency array, it seems like whenever variable, method or dispatch inside the useEffect are giving warning that
React Hook useEffect has missing dependencies: 'active', 'retrieveUser', and 'dispatch'. Either include them or remove the dependency array of following example, if I just leave it as blank array to perform the componentDidMount functionality
useEffect(() => {
setActive(active);
await retrieveUser(param1, param2);
dispatch(someAction);
}, []). // warning: React Hook useEffect has missing dependencies: 'active', 'retrieveUser', 'param1', 'param2', and 'dispatch'. Either include them or remove the dependency array, but here I just want to perform componentDidMount concept so my dependency list has to be empty
or
useEffect(() => {
await retrieveUser(param1, param2);
dispatch(someAction);
}, [userId]). // warning: React Hook useEffect has missing dependencies: 'retrieveUser', 'param1', 'param2', and 'dispatch'. Either include them or remove the dependency array
Are those valid warning? Especially that I just want to monitor on specific data field, what's the point of adding all inside dispatch or method into the dependency array if I can't add anything into the dependency list(for componentDidMOunt) or I just want to monitor userId, not param1, param2, etc
React is giving this warning because you are using part of your component's state or props in useEffect but have told it not to run the useEffect callback when that state/props changes. React assumes you will want to run that useEffect() whenever the referenced states change so it is giving you a warning. If you were only referencing variables outside of state or props, you would not get this warning. See below for info about conditionally running useEffect.
Since your goal is to only run useEffect only sometimes, you're likely going to need to restructure your component so that different data is in or out of state. It's difficult to recommend more specific solutions without knowing more about the component; but, a good goal is to have the state you utilize in useEffect match up with the changes that you want to trigger a rerender.
There might be other issues with state in your component. For example, I notice that you call setActive(active);. If active is part of the component's state, that means you are setting active to itself. Which isn't necessary. (this is assuming you are following typical naming patterns and likely have a line at the top of your component like:
const [active, setActive] = useState(initialValue);
Conditionally running useEffect
When you provide a second argument to useEffect(), it limits the situations when useEffect runs the callback function on rerenders. This is for performance reasons as explained in the React docs.
In the first useEffect where you provide an empty array, you are telling React to only run those actions (setActive(active)...etc.) when the component mounts and unmounts.
In the second useEffect where you provide an array with userId, you are telling React to only run the callback on mount, unmount, and whenever userId changes.
React is giving you that warning because it knows it won't run useEffect if active...etc or other values change. This can cause different parts of your component to be referencing different states. For example, one part of your component might be using userId = 1 and another part is using userId = 2. This is not how React is designed to work.
Create a hook that executes only once:
const useEffectOnce = effect => {
useEffect(effect, []);
};
use this hook inside your component
useEffectOnce(() => {
const request = async () => {
const result = await retrieveSum(param1, param2);
setResult(result);
};
request();
});
Now, even if your props (param1, param2) change, the effect is called only once. Be very careful when doing this, the component is probably buggy unless your initial props are truly special. I don't see how this is different from disabling the rule with
// eslint-disable-next-line react-hooks/exhaustive-deps
Demo: https://codesandbox.io/s/trusting-satoshi-fngrw?file=/src/App.js

Why is my RXJS epic in an infinite loop on mount but only called once on button click?

please take a look at this code below
basically what is happening my action is being dispatched here:
useEffect(() => {
fetchData()
setLoaded(true)
}, [])
but for some reason this is infinite looping and causing my action to be dispatched continuously
export const fetchData = () => ({ type: 'GET_USER_DATA' })
and this is triggering my epic
const getUserData = (action$, state$) =>
action$.pipe(
ofType('GET_USER_DATA'),
mergeMap(
(action) =>
ajax
.getJSON(
`myurlishere`,
)
.pipe(map((response) => fetchUserFulfilled(response))),
)
)
which trigger this:
const fetchUserFulfilled = (payload) => ({ type: 'GET_DATA_SUCCESS', data: payload })
this code all works but it's continuously calling it in an infinite loop
however, if I move the code from useEffect to a button call like so:
<button onClick={fetchData}>fetch</button>
it only calls it once, which is what I want
but I need the data to be called onmount. so how do I fix it?
please note I have tried adding various things to the second argument of useEffect but it's having no effect
useEffect(() => {
fetchData()
setLoaded(true)
}, [user.id])
Based solely off the provided code, I don't see any issues. My gut suggest a few options, even though you mentioned some of them I would triple check:
Missing dependencies array as second arg to useEffect, or you're using a variable for it but the variable has an undefined value which would have the same problem.
If you are using useEffect dependencies, perhaps one of them is constantly changing unknowingly. e.g. objects often change in identity between renders, {} !== {}
There is code not shown that is also dispatching the same action, and in fact that useEffect is only running once.
Some parent is rendering one of the ancestors or this component with a key={something} and the value provided changes on each render. If that happens, the component is torn down and recreated every time from scratch.
If you are 100% positive you are providing useEffect(work, []), an empty array as second argument, but the effect is in fact confirmed to be running in an infinite loop, synchronously, then the forth possibility is likely.
If you typed these code examples in the question by hand when posting this, do not trust that you implemented them the same way as what you think your app is doing. Triple check. Ideally have someone else check who didn't write the code. Often the problem is what we think we've told our code to do is not what we've actually told it to do. If you haven't already, your best bet is to step through the code with a debugger so you can see what's happening.
Hope this helps!
this is the code now working:
export const getUserDataEpic = () => {
return ajax
.getJSON(myurl)
.pipe(
map((response) => fetchUserFulfilled(response)),
)
}
I know I'm not listening to the action fired now, but why would that be causing an infinite loop?

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.

Categories

Resources