Initialise helper class in a react functional component - javascript

The class methods which are passed as args from the functional component, are kept 'in memory' and doest not reflect the updated state. I can reinitialise on state changes but wish to avoid it.
const MyFunctional = (props) => {
const [state,setState] = useState(0);
const helper = useRef();
useEffect(()=>{
helper.current = new HelperClass(onSuccess,onFailure);
},[])
/* wish to avoid */
useEffect(()=>{
helper.current = new HelperClass(onSuccess,onFailure);
},[state])
const onSuccess = (result) =>{
/* Here state == 0 */
}
const onFailure = (error) =>{
/* Here state == 0 */
}
}

You'll need an additional ref to be able to use the latest values in an async callback.
Either
grab react-use's useLatest hook,
write one yourself according to the docs,
or steal this trivial reference implementation:
function useLatest(value) {
const ref = useRef(value);
ref.current = value;
return ref;
};
const MyFunctional = (props) => {
const [state, setState] = useState(0);
const latestStateRef = useLatest(state);
const helper = useRef();
useEffect(() => {
helper.current = new HelperClass(onSuccess, onFailure);
}, []);
const onSuccess = (result) => {
console.log(latestStateRef.current);
};
const onFailure = (error) => {
console.log(latestStateRef.current);
};
};

Related

How do I access latest state in useEffect (when updating state) without including dependency

I'm trying to access the latest state in other setState function, cant figure out the correct way of doing it for a functional component
without accessing the latest state setMoviesListset state as undefined and causes issues
state
const [movies, setMoviesList] = useState();
const [currentGenre, setcurrentGenre] = useState();
const [page, setPage] = useState(1);
const [genreList, setGenreList] = useState();
const [nextPage, setNextPage] = useState(false);
const [previousMovieList, setPreviousMovieList] = useState();
useEffect(() => {
async function getMovies(currentGenre, page) {
if (currentGenre) {
const data = await rawAxios.get(
`https://api.themoviedb.org/3/discover/movie?api_key=f4872214e631fc876cb43e6e30b7e731&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=${page}&with_genres=${currentGenre}`
);
setPreviousMovieList((previousMovieList) => {
if (!previousMovieList) return [data.data];
else {
if (nextPage) {
console.log(previousMovieList);
setNextPage(false);
return [...previousMovieList, data.data];
}
}
});
setMoviesList(previousMovieList.results);
} else {
const data = await rawAxios.get(
`https://api.themoviedb.org/3/discover/movie?api_key=f4872214e631fc876cb43e6e30b7e731&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=${page}`
);
if (!previousMovieList) {
console.log('!previousMovieList', previousMovieList);
console.log('!data', data.data);
setPreviousMovieList(previousMovieList)
} else {
if (nextPage) {
console.log('else', previousMovieList);
setNextPage(false);
setPreviousMovieList([...previousMovieList, data.data])
// return [...previousMovieList, data.data];
}
}
setMoviesList(previousMovieList.results);
}
}
getMovies(currentGenre, page);
}, [currentGenre, page, setMoviesList, nextPage]);
want to access latest previousMovieList here
setMoviesList(previousMovieList.results);
You need to include previousMovieList in your useEffect dependency array as follows:
useEffect(()=>
{...},
[currentGenre, page, setMoviesList, nextPage, previousMovieList]
);
Without including it, you will have a stale closure and latest value will not be reflected in your function. This is causing the initial previousMovieList value of undefined to never update within your useEffect logic.
If you dont want it in your useEffect deps, you can use a ref:
const previousMovieList = useRef();
//then in your useEffect
setMoviesList(previousMovieList.current.results)
//and to set it
previousMovieList.current = ... // whatever you want to store
Or you can do something like this:
setPreviousMovieList((previousMovieList) => {
if (!previousMovieList) return [data.data];
else {
if (nextPage) {
console.log(previousMovieList);
setNextPage(false);
return [...previousMovieList, data.data];
}
}
setMoviesList(previousMovieList.results);
});
Basically move setMoviesList to within the setPreviousMovieList function where you do have access to previousMovieList.
Kind of hard to tell what you're trying to do, but generally when you want to store the previous value of state, you would use the ref approach. Like usePrevious for example
Add previousMovieList in useEffect dependency array, which allows react to know that its a dependency and reload when the dependency changes
you can use optional chaining to access the data coming from the api
enter code here
const [movies, setMoviesList] = useState();
const [currentGenre, setcurrentGenre] = useState();
const [page, setPage] = useState(1);
const [genreList, setGenreList] = useState();
const [nextPage, setNextPage] = useState(false);
const [previousMovieList, setPreviousMovieList] = useState();
useEffect(() => {
async function getMovies(currentGenre, page) {
if (currentGenre) {
const data = await rawAxios.get(
`https://api.themoviedb.org/3/discover/movie?api_key=f4872214e631fc876cb43e6e30b7e731&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=${page}&with_genres=${currentGenre}`
);
setPreviousMovieList((previousMovieList) => {
if (!previousMovieList) return [data?.data];
else {
if (nextPage) {
console.log(previousMovieList);
setNextPage(false);
return [...previousMovieList, data?.data];
}
}
});
setMoviesList(previousMovieList?.results);
} else {
const data = await rawAxios.get(
`https://api.themoviedb.org/3/discover/movie?api_key=f4872214e631fc876cb43e6e30b7e731&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=${page}`
);
if (!previousMovieList) {
console.log('!previousMovieList', previousMovieList);
console.log('!data', data?.data);
setPreviousMovieList(previousMovieList)
} else {
if (nextPage) {
console.log('else', previousMovieList);
setNextPage(false);
setPreviousMovieList([...previousMovieList, data?.data])
// return [...previousMovieList, data?.data];
}
}
setMoviesList(previousMovieList.results);
}
}
getMovies(currentGenre, page);
}, [currentGenre, page, setMoviesList, nextPage]);

Way to invoke function again while not setting different value in state

So I have built app which takes value from input -> set it to the state-> state change triggers functions in useEffect (this part is in custom hook) -> functions fetch data from api -> which triggers functions in useEffect in component to store data in array. The thing is that there are two problems that I am trying to solve :
When user is putting the same value in input and setting it in state it's not triggering useEffect functions (I solved it by wrapping value in object but I am looking for better solution).
When user uses the same value in short period of time api will send the same data which again makes problem with triggering function with useEffect (I tried to solved with refresh state that you will see in code below, but it looks awful)
The question is how can I actually do it properly? Or maybe the solutions I found aren't as bad as I think they are. Thanks for your help.
component
const [nextLink, setNextLink] = useState({ value: "" });
const isMounted = useRef(false);
const inputRef = useRef(null);
const { shortLink, loading, error, refresh } = useFetchLink(nextLink);
const handleClick = () => {
setNextLink({ value: inputRef.current.value });
};
useEffect(() => {
setLinkArr((prev) => [
...prev,
{
id: prev.length === 0 ? 1 : prev[prev.length - 1].id + 1,
long: nextLink.value,
short: shortLink,
},
]);
if (isMounted.current) {
scrollToLink();
} else {
isMounted.current = true;
}
inputRef.current.value = "";
}, [refresh]);
custom hook
const useFetchLink = (linkToShorten) => {
const [shortLink, setShortLink] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [refresh, setRefresh] = useState(false);
const isMounted = useRef(false);
const fetchLink = async (link) => {
setLoading(true);
try {
const response = await fetch(
`https://api.shrtco.de/v2/shorten?url=${link}`
);
if (response.ok) {
const data = await response.json();
setShortLink(data.result.short_link);
setRefresh((prev) => !prev);
} else {
throw response.status;
}
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (isMounted.current) {
if (checkLink(linkToShorten.value)) {
setError(checkLink(linkToShorten.value));
} else {
fetchLink(linkToShorten.value);
}
} else {
isMounted.current = true;
}
}, [linkToShorten]);
const value = { shortLink, loading, error, refresh };
return value;
};
export default useFetchLink;

Refactoring a class component to Functional, ReferenceError

I am trying to refactor a class component, but in the class one, there is a state with map and I tried changing it to Functional and used useState but it keeps giving me this error
ReferenceError: Cannot access 'rules' before initialization
it happens when I'm trying to refactor the State of rules(which I'm not sure how), with map, to useState. Is it even the correct way of assigning state for map and how can I fix it?
the class component :
import Rule from "./Rule";
class Game extends Component {
state = {
dices: Array(this.props.nOfDices).fill(1),
locked: Array(this.props.nOfDices).fill(false),
rotation: Array(this.props.nOfDices).fill(0),
rollsRemaining: 3,
isRolling: false,
rules: this.props.rules.map( r => ({...r})),
score: 0,
bestScore: window.localStorage.getItem("bestScore") || "0"
};
componentDidMount() {
this.roll();
};
my refactored functional component :
const Game = ({ nOfDices }) => {
const [isRolling, setisRolling] = useState(false);
const [score, setScore] = useState(0);
const [rollsRemaining, setRollsRemaining] = useState(3);
const [dices, setDices] = useState([Array(nOfDices).fill(1)]);
const [rules, setRules] = useState(rules.map(r => ({ ...r })));
const [bestScore, setBestScore] = useState(window.localStorage.getItem("bestScore") || "0");
const [locked, setLocked] = useState([Array(nOfDices).fill(false)]);
const [rotation, setRotation] = useState([Array(nOfDices).fill(0)]);
useEffect(() => {
roll();
//eslint-disable-next-line
}, []);
You are currently setting rules to a map of itself...
const [rules, setRules] = useState(rules.map(r => ({ ...r })));
should it be coming from props as it is in the original?
state = {
// ...
rules: this.props.rules.map( r => ({...r})),
// ...
}
If so you'll need to also destructure it out of props in the parameter declaration. (Here renaming it to avoid collision with the the state name Game = ({rules: _rules, nOfDices}) => ...)
Something like...
const Game = ({ rules: _rules, nOfDices }) => {
const [isRolling, setisRolling] = useState(false);
const [score, setScore] = useState(0);
const [rollsRemaining, setRollsRemaining] = useState(3);
const [bestScore, setBestScore] = useState(window.localStorage.getItem('bestScore') || '0');
// nOfDices
const [dices, setDices] = useState([Array(nOfDices).fill(1)]);
const [locked, setLocked] = useState([Array(nOfDices).fill(false)]);
const [rotation, setRotation] = useState([Array(nOfDices).fill(0)]);
// rules
const [rules, setRules] = useState(_rules.map((r) => ({ ...r })));
// update state if `nOfDices` changes in props
useEffect(() => {
setDices([Array(nOfDices).fill(1)]);
setLocked([Array(nOfDices).fill(false)]);
setRotation([Array(nOfDices).fill(0)]);
}, [nOfDices]);
// update state if `_rules` changes in props
useEffect(() => {
setRules(_rules.map((r) => ({ ...r })));
}, [_rules]);
useEffect(() => {
roll();
//eslint-disable-next-line
}, []);

How can I redirect to another component in react and pass the state that I set in the previous component?

I have a component I want to redirect to using react router. How can I set the state of the new component with a string that I chose on the original component? All of my redirects using react router are working and this component that is being redirected to isn't working. It is a html button when clicked should render this new components with initial data.
const Posts = (props) => {
const dispatch = useDispatch();
const getProfile = async (member) => {
console.log(member)
props.history.push('/member', { user: member});
console.log('----------- member------------')
}
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
return (
<div>
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
<button onClick={() => getProfile(p.publisher)}>Profile</button>
</tr>})}
</div>
)
}
export default Posts;
This is the component I am trying to redirect to on click.
const Member = (props)=> {
const [user, setUser] = useState({});
const { state } = this.props.history.location;
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
useEffect(async()=>{
try {
await setUser(state.user)
console.log(user)
console.log(user)
const p = await incidentsInstance.usersProfile(state.user, { from: accounts[0] });
const a = await snInstance.getUsersPosts(state.user, { from: accounts[0] });
} catch (e) {
console.error(e)
}
}, [])
I get the following error in the console.
TypeError: Cannot read property 'props' of undefined
Member
src/components/profiles/member.js:16
13 | const [posts, setPosts] = useState([]);
14 | const [snInstance, setsnInstance] = useState({});
15 | const [accounts, setsAccounts] = useState({});
> 16 | const { state } = this.props.history.location;
If you need to send some route state then the push method takes an object.
const getProfile = (member) => {
console.log(member)
props.history.push({
pathname: '/member',
state: {
user: member,
},
});
console.log('----------- member------------')
}
Additionally, Member is a functional component, so there is no this, just use the props object.
The route state is on the location prop, not the history object.
const Member = (props)=> {
const [user, setUser] = useState({});
const { state } = props.location;
// access state.user
Also additionally, useEffect callbacks can't be async as these imperatively return a Promise, interpreted as an effect cleanup function. You should declare an internal async function to invoke. On top of this, the setuser function isn't async so it can't be awaited on.
The following is what I think should be the effects for populating the user state and issuing side-effects:
// update user state when route state updates
useEffect(() => {
if (state && state.user) {
setUser(state.user);
}
}, [state]);
// run effect when user state updates
useEffect(() => {
const doEffects = async () => {
try {
const p = await incidentsInstance.usersProfile(state.user, { from: accounts[0] });
const a = await snInstance.getUsersPosts(state.user, { from: accounts[0] });
} catch (e) {
console.error(e)
}
}
doEffects();
}, [user]);

Executing async code on update of state with react-hooks

I have something like:
const [loading, setLoading] = useState(false);
...
setLoading(true);
doSomething(); // <--- when here, loading is still false.
Setting state is still async, so what's the best way to wait for this setLoading() call to be finished?
The setLoading() doesn't seem to accept a callback like setState() used to.
an example
class-based
getNextPage = () => {
// This will scroll back to the top, and also trigger the prefetch for the next page on the way up.
goToTop();
if (this.state.pagesSeen.includes(this.state.page + 1)) {
return this.setState({
page: this.state.page + 1,
});
}
if (this.state.prefetchedOrders) {
const allOrders = this.state.orders.concat(this.state.prefetchedOrders);
return this.setState({
orders: allOrders,
page: this.state.page + 1,
pagesSeen: [...this.state.pagesSeen, this.state.page + 1],
prefetchedOrders: null,
});
}
this.setState(
{
isLoading: true,
},
() => {
getOrders({
page: this.state.page + 1,
query: this.state.query,
held: this.state.holdMode,
statuses: filterMap[this.state.filterBy],
})
.then((o) => {
const { orders } = o.data;
const allOrders = this.state.orders.concat(orders);
this.setState({
orders: allOrders,
isLoading: false,
page: this.state.page + 1,
pagesSeen: [...this.state.pagesSeen, this.state.page + 1],
// Just in case we're in the middle of a prefetch.
prefetchedOrders: null,
});
})
.catch(e => console.error(e.message));
},
);
};
convert to function-based
const getNextPage = () => {
// This will scroll back to the top, and also trigger the prefetch for the next page on the way up.
goToTop();
if (pagesSeen.includes(page + 1)) {
return setPage(page + 1);
}
if (prefetchedOrders) {
const allOrders = orders.concat(prefetchedOrders);
setOrders(allOrders);
setPage(page + 1);
setPagesSeen([...pagesSeen, page + 1]);
setPrefetchedOrders(null);
return;
}
setIsLoading(true);
getOrders({
page: page + 1,
query: localQuery,
held: localHoldMode,
statuses: filterMap[filterBy],
})
.then((o) => {
const { orders: fetchedOrders } = o.data;
const allOrders = orders.concat(fetchedOrders);
setOrders(allOrders);
setPage(page + 1);
setPagesSeen([...pagesSeen, page + 1]);
setPrefetchedOrders(null);
setIsLoading(false);
})
.catch(e => console.error(e.message));
};
In the above, we want to run each setWhatever call sequentially. Does this mean we need to set up many different useEffect hooks to replicate this behavior?
useState setter doesn't provide a callback after state update is done like setState does in React class components. In order to replicate the same behaviour, you can make use of the a similar pattern like componentDidUpdate lifecycle method in React class components with useEffect using Hooks
useEffect hooks takes the second parameter as an array of values which React needs to monitor for change after the render cycle is complete.
const [loading, setLoading] = useState(false);
...
useEffect(() => {
doSomething(); // This is be executed when `loading` state changes
}, [loading])
setLoading(true);
EDIT
Unlike setState, the updater for useState hook doesn't have a callback, but you can always use a useEffect to replicate the above behaviour. However you need to determine the loading change
The functional approach to your code would look like
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const prevLoading = usePrevious(isLoading);
useEffect(() => {
if (!prevLoading && isLoading) {
getOrders({
page: page + 1,
query: localQuery,
held: localHoldMode,
statuses: filterMap[filterBy],
})
.then((o) => {
const { orders: fetchedOrders } = o.data;
const allOrders = orders.concat(fetchedOrders);
setOrders(allOrders);
setPage(page + 1);
setPagesSeen([...pagesSeen, page + 1]);
setPrefetchedOrders(null);
setIsLoading(false);
})
.catch(e => console.error(e.message));
}
}, [isLoading, preFetchedOrders, orders, page, pagesSeen]);
const getNextPage = () => {
// This will scroll back to the top, and also trigger the prefetch for the next page on the way up.
goToTop();
if (pagesSeen.includes(page + 1)) {
return setPage(page + 1);
}
if (prefetchedOrders) {
const allOrders = orders.concat(prefetchedOrders);
setOrders(allOrders);
setPage(page + 1);
setPagesSeen([...pagesSeen, page + 1]);
setPrefetchedOrders(null);
return;
}
setIsLoading(true);
};
Wait until your component re-render.
const [loading, setLoading] = useState(false);
useEffect(() => {
if (loading) {
doSomething();
}
}, [loading]);
setLoading(true);
You can improve clarity with something like:
function doSomething() {
// your side effects
// return () => { }
}
function useEffectIf(condition, fn) {
useEffect(() => condition && fn(), [condition])
}
function App() {
const [loading, setLoading] = useState(false);
useEffectIf(loading, doSomething)
return (
<>
<div>{loading}</div>
<button onClick={() => setLoading(true)}>Click Me</button>
</>
);
}
Created a custom useState hook which works similar to the normal useState hook except that the state updater function for this custom hook takes a callback that will be executed after the state is updated and component rerendered.
Typescript Solution
import { useEffect, useRef, useState } from 'react';
type OnUpdateCallback<T> = (s: T) => void;
type SetStateUpdaterCallback<T> = (s: T) => T;
type SetStateAction<T> = (newState: T | SetStateUpdaterCallback<T>, callback?: OnUpdateCallback<T>) => void;
export function useCustomState<T>(init: T): [T, SetStateAction<T>];
export function useCustomState<T = undefined>(init?: T): [T | undefined, SetStateAction<T | undefined>];
export function useCustomState<T>(init: T): [T, SetStateAction<T>] {
const [state, setState] = useState<T>(init);
const cbRef = useRef<OnUpdateCallback<T>>();
const setCustomState: SetStateAction<T> = (newState, callback?): void => {
cbRef.current = callback;
setState(newState);
};
useEffect(() => {
if (cbRef.current) {
cbRef.current(state);
}
cbRef.current = undefined;
}, [state]);
return [state, setCustomState];
}
Javascript solution
import { useEffect, useRef, useState } from 'react';
export function useCustomState(init) {
const [state, setState] = useState(init);
const cbRef = useRef();
const setCustomState = (newState, callback) => {
cbRef.current = callback;
setState(newState);
};
useEffect(() => {
if (cbRef.current) {
cbRef.current(state);
}
cbRef.current = undefined;
}, [state]);
return [state, setCustomState];
}
Usage
const [state, setState] = useCustomState(myInitialValue);
...
setState(myNewValueOrStateUpdaterCallback, () => {
// Function called after state update and component rerender
})
you can create a async state hooks
const useAsyncState = initialState => {
const [state, setState] = useState(initialState);
const asyncSetState = value => {
return new Promise(resolve => {
setState(value);
setState((current) => {
resolve(current);
return current;
});
});
};
return [state, asyncSetState];
};
then
const [loading, setLoading] = useAsyncState(false)
const submit = async () => {
await setLoading(true)
dosomething()
}
I have a suggestion for this.
You could possibly use a React Ref to store the state of the state variable. Then update the state variable with the react ref. This will render a page refresh, and then use the React Ref in the async function.
const stateRef = React.useRef().current
const [state,setState] = useState(stateRef);
async function some() {
stateRef = { some: 'value' }
setState(stateRef) // Triggers re-render
await some2();
}
async function some2() {
await someHTTPFunctionCall(stateRef.some)
stateRef = null;
setState(stateRef) // Triggers re-render
}
Pass a function to the setter instead of value!
instead of giving a new value to the setter directly, pass it an arrow function that takes the current state value and returns the new value.
it will force it to chain the state updates and after it's done with all of them, it will rerender the component.
const [counter, setCounter] = useState(0);
const incrementCount = () => {
setCounter( (counter) => { return counter + 1 } )
}
now every time incrementCount is called, it will increase the count by one and it will no longer be stuck at 1.

Categories