How to call a function after setting state is complete in useEffect? - javascript

I would like to run customFunction only when customEffect has finished setting isReady state. And customFunction should only run once no matter if the isReady was set to false or true as long as it was ran after it was set.
import customFunction from 'myFile';
export const smallComponent = () => {
const [isReady, setIsReady] = useState(false);
useEffect(() => {
const customEffect = async () => {
try {
const response = await get(
`some-api.com`,
);
return setIsReady(response); // response can be true or false
} catch {
return null;
}
};
customEffect();
customFunction();
}, []);
return (
<>Hello World</>
)
}
I tried to add isReady as second useEffect argument, but then my customFunction is being run before the customEffect finishes and then again after the isReady is being set.
Also tried having in a separate useEffect, but still seems to run before the customEffect finishes.

Set initial value to null and use separate useEffect as Kevin suggested (only without checking isReady true/false).
In this case setIsReady will change isReady from null to true/false and the second useEffect will be called.
import customFunction from 'myFile';
export const smallComponent = () => {
const [isReady, setIsReady] = useState(null);
useEffect(() => {
const customEffect = async () => {
try {
const response = await get(
`some-api.com`,
);
return setIsReady(response);
} catch {
return null;
}
};
customEffect();
}, []);
useEffect(() => {
if (null === isReady) {
return;
}
customFunction();
}, [isReady]);
return (
<>Hello World</>
)
}

Since you want to cue an effect to run after the isReady state is set, and the value of isReady is irrelevant you can to use a second state value to indicate the first effect and state update has completed.
This will trigger the second effect to invoke customFunction but you don't want your component to remain in this state as from here any time the component rerenders the conditions will still be met. You'll want a third "state" to indicate the second effect has been triggered. Here you can use a React ref to indicate this.
export const smallComponent = () => {
const [readySet, setReadySet] = useState(false);
const [isReady, setIsReady] = useState(false);
const customFunctionRunRef = useRef(false);
useEffect(() => {
const customEffect = async () => {
try {
const response = await get(
`some-api.com`,
);
setReadySet(true); // to trigger second effect callback
return setIsReady(response); // response can be true or false
} catch {
return null;
}
};
customEffect();
}, []);
useEffect(() => {
if (readySet && !customFunctionRunRef.current) {
// won't run before readySet is true
// won't run after customFunctionRunRef true
customFunction();
customFunctionRunRef.current = true;
}
}, [readySet]);
return (
<>Hello World</>
);
}
Better solution borrowed from #p1uton. Use null isReady state to indicate customFunction shouldn't invoke yet, and the ref to keep it from being invoked after.
export const smallComponent = () => {
const [isReady, setIsReady] = useState(null);
const customFunctionRunRef = useRef(false);
useEffect(() => {
const customEffect = async () => {
try {
const response = await get(
`some-api.com`,
);
return setIsReady(response); // response can be true or false
} catch {
return null;
}
};
customEffect();
}, []);
useEffect(() => {
if (isReady !== null && !customFunctionRunRef.current) {
// won't run before isReady is non-null
// won't run after customFunctionRunRef true
customFunction();
customFunctionRunRef.current = true;
}
}, [isReady]);
return (
<>Hello World</>
);
}

I'm not sure if I understood you correctly, but this is how I would use a separate useEffect.
import customFunction from 'myFile';
export const smallComponent = () => {
const [isReady, setIsReady] = useState(false);
useEffect(() => {
const customEffect = async () => {
try {
const response = await get(
`some-api.com`,
);
return setIsReady(response);
} catch {
return null;
}
};
customEffect();
}, []);
useEffect(() => {
if (!isReady) {
return;
}
customFunction();
}, [isReady]);
return (
<>Hello World</>
)
}

Have you tried using this package, isMounted?
I used that in my projects.
import React, { useState, useEffect } from 'react';
import useIsMounted from 'ismounted';
import myService from './myService';
import Loading from './Loading';
import ResultsView from './ResultsView';
const MySecureComponent = () => {
const isMounted = useIsMounted();
const [results, setResults] = useState(null);
useEffect(() => {
myService.getResults().then(val => {
if (isMounted.current) {
setResults(val);
}
});
}, [myService.getResults]);
return results ? <ResultsView results={results} /> : <Loading />;
};
export default MySecureComponent;
https://www.npmjs.com/package/ismounted

Related

How to properly clear fetch timeout in React component?

I'm fetching Dogs from my API through a JavaScript timeout. It works fine, except it fails to clear the timeout sometimes:
import { useState, useEffect, useCallback } from 'react';
const DogsPage = () => {
const [dogs, setDogs] = useRef([]);
const timeoutId = useRef();
const fetchDogs = useCallback(
async () => {
const response = await fetch('/dogs');
const { dogs } = await response.json();
setDogs(dogs);
timeoutId.current = setTimeout(fetchDogs, 1000);
},
[]
);
useEffect(
() => {
fetchDogs();
return () => clearTimeout(timeoutId.current);
},
[fetchDogs]
);
return <b>Whatever</b>;
};
It looks like the problem is that sometimes I unmount first, while the code is still awaiting for the Dogs to be fetched. Is this a common issue and if so, how would I prevent this problem?
One idea would be to use additional useRef() to keep track of whether the component has been unmounted in between fetch:
const DogsPage = () => {
const isMounted = useRef(true);
const fetchDogs = useCallback(
async () => {
// My fetching code
if (isMounted.current) {
timeoutId.current = setTimeout(fetchDogs, 1000);
}
},
[]
);
useEffect(
() => {
return () => isMounted.current = false;
},
[]
);
// The rest of the code
};
But perhaps there is a cleaner way?
You can assign a sentinel value for timeoutId.current after clearing it, then check for that value before starting a new timer:
import { useState, useEffect, useCallback } from 'react';
const DogsPage = () => {
const [dogs, setDogs] = useRef([]);
const timeoutId = useRef();
const fetchDogs = useCallback(
async () => {
const response = await fetch('/dogs');
const { dogs } = await response.json();
setDogs(dogs);
if (timeoutId.current !== -1)
timeoutId.current = setTimeout(fetchDogs, 1000);
},
[]
);
useEffect(
() => {
fetchDogs();
return () => void (clearTimeout(timeoutId.current), timeoutId.current = -1);
},
[fetchDogs]
);
return <b>Whatever</b>;
};

React component rendered from object does not get unmounted

I have the following code, where I need to run clean-up when unmounting each component step. I've set a useEffect on each Step to check if the component has been unmounted. When the parent gets a new currentStep it swaps the currently active component but the clean-up never runs. I'm wondering if this has to do with the nature of the component being rendered from an object
const Step1 = () => {
useEffect(() => {
console.log("doing things here");
return () => {
console.log("clean-up should happen here but this won't print")
}
}, []}
}
const StepMap = {
step1: <Step1/>
step2: <Step2/>
step3: <Step3/>
}
const Parent = ({ currentStep }) => {
return (
<div>
{ StepMap[currentStep] }
</div>
)
}
Alternatively this piece of code does run the clean-up, but I do find the former cleaner
const Parent = ({ currentStep }) => {
return (
<div>
{ currentStep === "step1" && StepMap[currentStep]}
{ currentStep === "step2" && StepMap[currentStep]}
</div>
)
}
Why does the first approach not work? is there a way to make it work like the second while keeping a cleaner implementation?
if you want to write javascript inside jsx we have write it inside {} curly braces like this:
import React, { useEffect, useState } from "react";
const Step1 = () => {
useEffect(() => {
console.log("Step1 doing things here");
return () => {
console.log("Step1 clean-up should happen here but this won't print");
};
}, []);
return <div>stepOne</div>;
};
const Step2 = () => {
useEffect(() => {
console.log("Step2 doing things here");
return () => {
console.log("Step2 clean-up should happen here but this won't print");
};
}, []);
return <div>steptw0</div>;
};
const Step3 = () => {
useEffect(() => {
console.log("Step3 doing things here");
return () => {
console.log("Step3 clean-up should happen here but this won't print");
};
}, []);
return <div>stepthree</div>;
};
export const StepMap = {
step1: <Step1 />,
step2: <Step2 />,
step3: <Step3 />,
};
export const Parent = ({ currentStep }) => {
return <div>{StepMap[currentStep]}</div>;
};
const App = () => {
const [steps, setSteps] = React.useState("step1");
React.useEffect(() => {
setTimeout(() => setSteps("step2"), 5000);
setTimeout(() => setSteps("step3"), 15000);
}, []);
return <Parent currentStep={steps} />;
};
export default App;

Callback in Custom React Hooks

I have the following hooks:
function useLogin(state, url, loginMessage, callback) {
const history = useHistory();
const logged_in = state.user.authenticated;
useEffect(() => {
if (!logged_in) {history.push(url); loginMessage();}
else callback();
}, [logged_in])
return logged_in;
}
function useGroupAuth(state, url, loginMessage) {
const history = useHistory();
let has_group_auth = false;
state.user.available_teams.forEach(function(currentValue) {
if (currentValue.toString().toLowerCase() === teamname.toString().toLowerCase()) {
has_group_auth = true;
}
})
useEffect(() => {
if (!has_group_auth) {
if (state.user.available_teams.length != 0) {
history.push(url); loginMessage();
}
else
history.push("/"); loginMessage();
} else {
callback();
}
}, [has_group_auth])
return has_group_auth;
}
and they're used as
let loggedin = useLogin(state, "/accounts/login", teamhome2_message);
let properauth = useGroupAuth(state, ("/team/" + state.user.available_teams[0]), teamhome3_message);
useEffect(() => {
if (loggedin)
if (properauth)
checkteamexists(teamname);
}, []);
The problem is that, even though the code compiles, it's not behaving as I wanted it to. I only want if (properauth) to execute if loggedin is true.
My previous implementation worked because I was simply using callback without any custom hooks, as such:
useEffect(() => {
checklogin(function() {
checkauth(function() {
checkteamexists(teamname);
})
})
}, []);
How can I ensure that properauth won't execute unless loggedin is true, as described in the initial, hook-less useEffect hook?
Thanks in advance.
In your case, you can't update the useGroupAuth value. because it's returning only one value send one more variable(callback) to update/check whenever you need it. something like useState
Hook
function useGroupAuth(state, url, loginMessage) {
const history = useHistory();
const [has_group_auth, setAuth] = useState(false);
const validate = () => {
setAuth(
state.user.available_teams.some(
(currentValue) =>
currentValue.toString().toLowerCase() ===
teamname.toString().toLowerCase()
)
);
};
useEffect(validate, []);
useEffect(() => {
if (!has_group_auth) {
if (state.user.available_teams.length != 0) {
history.push(url);
loginMessage();
} else history.push("/");
loginMessage();
} else {
callback();
}
}, [has_group_auth]);
return [has_group_auth, validate];
}
Use
let [properauth, reValidate] = useGroupAuth(state, ("/team/" + state.user.available_teams[0]), teamhome3_message);
useEffect(() => {
if (loggedin){
// Do something
reValidate();
}
}, []);
It seems you are missing dependencies in your useEffect hook. Both loggedin and properauth (teamname as well, really) are referenced in the effect callback, so they should be included in the effect's dependencies.
const loggedin = useLogin(state, "/accounts/login", teamhome2_message);
const properauth = useGroupAuth(state, ("/team/" + state.user.available_teams[0]), teamhome3_message);
useEffect(() => {
if (loggedin && properauth && teamname) {
checkteamexists(teamname);
}
}, [loggedin, properauth, teamname]);

Have a javascript function pass a reference to itself in to another function

I found myself continuously writing the same shape of code for asynchronous calls so I tried to wrap it up in something that would abstract some of the details. What I was hoping was that in my onError callback I could pass a reference of the async function being executed so that some middleware could implement retry logic if it was necessary. Maybe this is a code smell that I'm tackling this the wrong way but I'm curious if it's possible or if there are other suggestions for handling this.
const runAsync = (asyncFunc) => {
let _onBegin = null;
let _onCompleted = null;
let _onError = null;
let self = this;
return {
onBegin(f) {
_onBegin = f;
return this;
},
onCompleted(f) {
_onCompleted = f;
return this;
},
onError(f) {
_onError = f;
return this;
},
async execute() {
if (_onBegin) {
_onBegin();
}
try {
let data = await asyncFunc();
if (_onCompleted) {
_onCompleted(data);
}
} catch (e) {
if (_onError) {
_onError(e ** /*i'd like to pass a function reference here as well*/ ** );
}
return Promise.resolve();
}
},
};
};
await runAsync(someAsyncCall())
.onBegin((d) => dispatch(something(d)))
.onCompleted((d) => dispatch(something(d)))
.onError((d, func) => dispatch(something(d, func)))
.execute()
I'm thinking you could use a custom hook. Something like -
import { useState, useEffect } from 'react'
const useAsync = (f) => {
const [state, setState] =
useState({ loading: true, result: null, error: null })
const runAsync = async () => {
try {
setState({ ...state, loading: false, result: await f })
}
catch (err) {
setState({ ...state, loading: false, error: err })
}
}
useEffect(_ => { runAsync() }, [])
return state
}
Now we can use it in a component -
const FriendList = ({ userId }) => {
const response =
useAsync(UserApi.fetchFriends(userId)) // <-- some promise-returning call
if (response.loading)
return <Loading />
else if (response.error)
return <Error ... />
else
return <ul>{response.result.map(Friend)}</ul>
}
The custom hook api is quite flexible. The above approach is naive, but we can think it through a bit more and make it more usable -
import { useState, useEffect } from 'react'
const identity = x => x
const useAsync = (runAsync = identity, deps = []) => {
const [loading, setLoading] = useState(true)
const [result, setResult] = useState(null)
const [error, setError] = useState(null)
useEffect(_ => {
Promise.resolve(runAsync(...deps))
.then(setResult, setError)
.finally(_ => setLoading(false))
}, deps)
return { loading, error, result }
}
Custom hooks are dope. We can make custom hooks using other custom hooks -
const fetchJson = (url = "") =>
fetch(url).then(r => r.json()) // <-- stop repeating yourself
const useJson = (url = "") => // <-- another hook
useAsync(fetchJson, [url]) // <-- useAsync
const FriendList = ({ userId }) => {
const { loading, error, result } =
useJson("some.server/friends.json") // <-- dead simple
if (loading)
return <Loading .../>
if (error)
return <Error .../>
return <ul>{result.map(Friend)}</ul>
}

Can't perform a React state update on an unmounted component theme provider

I need help because I get the following error: Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method. in createCategory (at themeProvider.js:39)
/* Imports */
import React, { useContext, useState, useEffect } from 'react';
import AsyncStorage from '#react-native-community/async-storage';
import THEMES from '#app/theme/themes.json';
/* /Imports/ */
const STORAGE_KEY = 'THEME_ID';
const ThemeContext = React.createContext();
/* Exports */
export const ThemeContextProvider = ({ children }) => {
const [themeID, setThemeID] = useState();
useEffect(() => {
(async () => {
const storedThemeID = await AsyncStorage.getItem(STORAGE_KEY);
if (storedThemeID) setThemeID(storedThemeID);
else setThemeID(THEMES[1].key);
})();
}, []);
return (
<ThemeContext.Provider value={{ themeID, setThemeID }}>
{!!themeID ? children : null}
</ThemeContext.Provider>
);
};
export function withTheme(Component) {
function TargetComponent(props) {
const { themeID, setThemeID } = useContext(ThemeContext);
const getTheme = themeID => THEMES.find(theme => theme.key === themeID);
const setTheme = themeID => {
AsyncStorage.setItem(STORAGE_KEY, themeID);
setThemeID(themeID);
};
return (
<Component
{...props}
themes={THEMES}
theme={getTheme(themeID)}
setTheme={setTheme}
/>
);
}
TargetComponent.navigationOptions = Component.navigationOptions;
return TargetComponent;
}
/* /Exports/ */
If you don't already know - you can return a function at the end of your useEffect hook. That function will be called whenever that effect is fired again (e.g. when the values of its dependencies have changed), as well as right before the component unmounts. So if you have a useEffect hook that looks like this:
useEffect(() => {
// logic here
return () => {
// clean up
};
}, []); // no dependencies!
Is equivalent to this:
class SomeComponent extends React.Component {
componentDidMount() {
// logic here
}
componentWillUnmount() {
// clean up
}
}
So in your code I'd add this:
useEffect(() => {
let isCancelled = false;
const fetchData = async () => {
try {
// fetch logic omitted...
const data = await AsyncStorage.getItem(STORAGE_KEY);
if (storedThemeID) setThemeID(storedThemeID);
else setThemeID(THEMES[1].key);
} catch (e) {
throw new Error(e)
}
};
fetchData();
return () => {
isCancelled = true;
};
}, [themeID]);
Try this
let unmounted = false;
useEffect(() => {
(async () => {
const storedThemeID = await AsyncStorage.getItem(STORAGE_KEY);
if (!unmounted) {
if (storedThemeID) setThemeID(storedThemeID);
else setThemeID(THEMES[1].key);
}
})();
return () => {
unmounted = true;
};
}, []);

Categories