I want to run a callback when a component mounts, but not on each render. Using useEffect and an empty dependency array created warnings. I read on a different thread which I now can't find that this can be achieved by creating a custom hook:
import {useRef} from "react";
const useInitialise = (callback) => {
//TODO this is running every render!
const runOnce = useRef(false);
if (!runOnce.current) {
callback();
runOnce.current = true;
}
}
export default useInitialise;
Usage:
useInitialise(() => {
//callback
});
This is instead of using:
useEffect(() => {
//callback
}, []);
As this generates warnings. I understand that the hook will be called on each render, but why is the runOnce.current not preventing the callback being run twice?
Found the thread!
I should use a state to track mounted/unmounted, then useEffect to call the callback only if either mounted or callback change:
import {useEffect, useState} from "react";
const useInitialise = (callback) => {
const [mounted, setMounted] = useState(false)
useEffect(() => {
if (!mounted) {
setMounted(true);
callback();
}
}, [mounted, callback]);
}
export default useInitialise;
Related
Trying to get the API functional component that updates Context values in useEffect to execute within another App functional component (on import) when mounted to allow render when values exist/update.
The API function at API.js appears to be working correctly as a stand alone component. The problem (due to lack of understanding) is upon the import to App.js where the API function should execute.
API.js
import { useState, useContext, useEffect } from 'react'
import { AppContext } from './contexts/AppContext';
export const API = () => {
const {updateOrder, updateCustomer} = useContext(AppContext);
useEffect(() => {
const fetchData = async () => {
const orderDetails = await fetch('url.com/order');
const customerDetails = await fetch('url.com/customer');
updateOrder(await orderDetails.json())
updateCustomer(await customerDetails .json())
}
fetchData();
}, [])
}
App.js
import React, { useContext, useEffect } from 'react';
import { AppContext } from './contexts/AppContext';
import { API } from './components/API';
const App = ({ isLoading }) => {
const {order, customer} = useContext(AppContext);
useEffect(() => {
isLoading && <API />
}, []) // eslint-disable-line react-hooks/exhaustive-deps
if (order && customer) {
return <SomeComponent/>
}
}
The expected outcome is to be able to use API within the initial mount (as API dependency) and conditionally return/render content in App.
I've tried changing the API component into function and exporting with default, however context is not supported outside of component.
A component is a function that renders something in the UI. You API component is not really a component - it doesn't have any branch that returns any JSX - although that's not always required.
You should look into building a custom hook instead. Try something like this:
App.js
import React, { useContext, useEffect } from "react";
import { AppContext } from "./contexts/AppContext";
// Hooks should start with 'use'
export const useAPI = () => {
const { updateOrder, updateCustomer } = useContext(AppContext);
useEffect(() => {
const fetchData = async () => {
// Fetching data from both endpoints in parallel
const res = await Promise.all([fetch("url.com/order"), fetch("url.com/customer")]);
// Converting both payloads to JSON in parallel
const data = await Promise.all([res[0].json(), res[1].json()]);
updateOrder(data[0]);
updateCustomer(data[1]);
};
fetchData();
}, []);
};
const App = ({ isLoading }) => {
// Calling the custom hook
useAPI();
const { order, customer } = useContext(AppContext);
if (order && customer) {
return <SomeComponent />;
}
};
I'm checking if a component is unmounted, in order to avoid calling state update functions.
This is the first option, and it works
const ref = useRef(false)
useEffect(() => {
ref.current = true
return () => {
ref.current = false
}
}, [])
....
if (ref.current) {
setAnswers(answers)
setIsLoading(false)
}
....
Second option is using useState, which isMounted is always false, though I changed it to true in component did mount
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
return () => {
setIsMounted(false)
}
}, [])
....
if (isMounted) {
setAnswers(answers)
setIsLoading(false)
}
....
Why is the second option not working compared with the first option?
I wrote this custom hook that can check if the component is mounted or not at the current time, useful if you have a long running operation and the component may be unmounted before it finishes and updates the UI state.
import { useCallback, useEffect, useRef } from "react";
export function useIsMounted() {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
Usage
function MyComponent() {
const [data, setData] = React.useState()
const isMounted = useIsMounted()
React.useEffect(() => {
fetch().then((data) => {
// at this point the component may already have been removed from the tree
// so we need to check first before updating the component state
if (isMounted()) {
setData(data)
}
})
}, [...])
return (...)
}
Live Demo
Please read this answer very carefully until the end.
It seems your component is rendering more than one time and thus the isMounted state will always become false because it doesn't run on every update. It just run once and on unmounted. So, you'll do pass the state in the second option array:
}, [isMounted])
Now, it watches the state and run the effect on every update. But why the first option works?
It's because you're using useRef and it's a synchronous unlike asynchronous useState. Read the docs about useRef again if you're unclear:
This works because useRef() creates a plain JavaScript object. The only difference between useRef() and creating a {current: ...} object yourself is that useRef will give you the same ref object on every render.
BTW, you do not need to clean up anything. Cleaning up the process is required for DOM changes, third-party api reflections, etc. But you don't need to habit on cleaning up the states. So, you can just use:
useEffect(() => {
setIsMounted(true)
}, []) // you may watch isMounted state
// if you're changing it's value from somewhere else
While you use the useRef hook, you are good to go with cleaning up process because it's related to dom changes.
This is a typescript version of #Nearhuscarl's answer.
import { useCallback, useEffect, useRef } from "react";
/**
* This hook provides a function that returns whether the component is still mounted.
* This is useful as a check before calling set state operations which will generates
* a warning when it is called when the component is unmounted.
* #returns a function
*/
export function useMounted(): () => boolean {
const mountedRef = useRef(false);
useEffect(function useMountedEffect() {
mountedRef.current = true;
return function useMountedEffectCleanup() {
mountedRef.current = false;
};
}, []);
return useCallback(function isMounted() {
return mountedRef.current;
}, [mountedRef]);
}
This is the jest test
import { render, waitFor } from '#testing-library/react';
import React, { useEffect } from 'react';
import { delay } from '../delay';
import { useMounted } from "./useMounted";
describe("useMounted", () => {
it("should work and not rerender", async () => {
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
callback(isMounted())
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
expect(callback.mock.calls).toEqual([[true]])
unmount();
expect(callback.mock.calls).toEqual([[true]])
})
it("should work and not rerender and unmount later", async () => {
jest.useFakeTimers('modern');
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
(async () => {
await delay(10000);
callback(isMounted());
})();
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
await waitFor(() => expect(callback).toBeCalledTimes(0));
jest.advanceTimersByTime(5000);
unmount();
jest.advanceTimersByTime(5000);
await waitFor(() => expect(callback).toBeCalledTimes(1));
expect(callback.mock.calls).toEqual([[false]])
})
})
Sources available in https://github.com/trajano/react-hooks-tests/tree/master/src/useMounted
This cleared up my error message, setting a return in my useEffect cancels out the subscriptions and async tasks.
import React from 'react'
const MyComponent = () => {
const [fooState, setFooState] = React.useState(null)
React.useEffect(()=> {
//Mounted
getFetch()
// Unmounted
return () => {
setFooState(false)
}
})
return (
<div>Stuff</div>
)
}
export {MyComponent as default}
If you want to use a small library for this, then react-tidy has a custom hook just for doing that called useIsMounted:
import React from 'react'
import {useIsMounted} from 'react-tidy'
function MyComponent() {
const [data, setData] = React.useState(null)
const isMounted = useIsMounted()
React.useEffect(() => {
fetchData().then((result) => {
if (isMounted) {
setData(result)
}
})
}, [])
// ...
}
Learn more about this hook
Disclaimer I am the writer of this library.
Near Huscarl solution is good, but there is problem with using these hook with react router, because if you go from example news/1 to news/2 useRef value is set to false because of unmount, but value keep false. So you need init ref value to true on each mount.
import {useRef, useCallback, useEffect} from "react";
export function useIsMounted(): () => boolean {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
isMountedRef.current = true;
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
It's hard to know without the larger context, but I don't think you even need to know whether something has been mounted. useEffect(() => {...}, []) is executed automatically upon mounting, and you can put whatever needs to wait until mounting inside that effect.
React is complaining about code below, saying it useState and useEffect are being called conditionally. The code work's fine without typescript:
import React, { useState, useEffect } from "react";
const useScrollPosition = () => {
if (typeof window === "undefined") return 500;
// Store the state
const [scrollPos, setScrollPos] = useState(window.pageYOffset);
// On Scroll
const onScroll = () => {
setScrollPos(window.pageYOffset);
};
// Add and remove the window listener
useEffect(() => {
window.addEventListener("scroll", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
};
});
};
export default useScrollPosition;
You have an early return so it is being called conditionally if (typeof window === "undefined") return 500;
You need to move the hooks before the early return. For a detail explanation read this https://reactjs.org/docs/hooks-rules.html
move the return to bottom of function and pass empty array to second parameter of useEffect for exists code in the useEffect run only once time.
import React, {useState, useEffect} from "react";
const useScrollPosition = () => {
// Store the state
const [scrollPos, setScrollPos] = useState(window.pageYOffset);
// On Scroll
const onScroll = () => {
setScrollPos(window.pageYOffset);
};
// Add and remove the window listener
useEffect(() => {
window.addEventListener("scroll", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
};
}, []); // set an empty array for run once existing code in the useEffect
return typeof window === "undefined" ? return 500 : <></>; // a value must be returned
};
export default useScrollPosition;
I have a mutation which I use to login on my application. I want to rerun that login mutation every 5 minutes to check for updates to their profile. useQuery has a pollingInterval options but useMutation does not.
I tried using a hook to run the mutation on an interval but that doesn't really work because the useAuth hook is used in multiple components at the same time so it ends up creating an interval for each component.
function useInterval(callback: () => any, delay: number | null) {
const savedCallback = useRef<any>();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
function useAuth(){
const [login,{data:loginData} = useMutation(gql`
...
`);
useInterval(() => login(Cookies.get(TOKEN_NAME),1000*60*5);
return login;
}
function App(){
const login = useAuth();
useEffect(() => {
login(Cookies.get(TOKEN_NAME));
},[login]);
...
}
What you want to do is create a context with a Provider that sits high up in your react component tree. Here is the documentation, context docs.
First you need to create the provider, probably in the same location you create the useAuth hook.
import React, {useContext, createContext} from 'react';
const AuthContext = createContext();
function AuthProvider({ children }) {
const [login,{data:loginData} = useMutation(gql`
...
`);
useInterval(() => login(Cookies.get(TOKEN_NAME),1000*60*5);
return (
<AuthContext.Provider value={login}>{children}</AuthContext.Provider>
)
}
function useAuth(){
const context = useContext(AuthContext);
return context;
}
Then change the App definition like so,
function App() {
return (
...
<AuthProvider>
...
</AuthProvider>
...
);
}
By placing it in a provider (that is high in the tree), the useEffect will likely only run on app load and when the useInterval triggers. If the code is just in a functional hook, it can change frequently (component mount/unmount, prop changes, etc).
Can I use handleStatusChange event handler outside of the useEffect block, and directly in the FriendStatus component?
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
The handleStatusChange is used with the ChatAPI, an external service that is not managed by react (unlike onClick for example).
You need to subscribe/unsubscribe a handler to the ChatAPI service whenever a friend.id changes.
Using the ChatAPI directly in the component's body would cause subscribe and unsubscribe to be called on each render. However, the event handler would never unsubscribe because you won't have a reference to the registered function.
The useEffect hook solves this problems:
We can configure the hook's dependency to only call the hook when the friend.id changes (see updated code).
The unsubscribe function always has a reference to the subscribed function, because it's defined in the same closure.
Updated code:
import React, { useState, useEffect } from 'react';
function FriendStatus({ friend }) {
const { id } = friend;
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(id, handleStatusChange);
};
}, [id]); // id is now used as a dependency
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}