I am using react useEffect hooks and checking if an object has changed and only then run the hook again.
My code looks like this.
const useExample = (apiOptions) => {
const [data, updateData] = useState([]);
useEffect(() => {
const [data, updateData] = useState<any>([]);
doSomethingCool(apiOptions).then(res => {
updateData(response.data);
})
}, [apiOptions]);
return {
data
};
};
Unfortunately it keeps running as the objects are not being recognised as being the same.
I believe the following is an example of why.
const objA = {
method: 'GET'
}
const objB = {
method: 'GET'
}
console.log(objA === objB)
Perhaps running JSON.stringify(apiOptions) works?
Use apiOptions as state value
I'm not sure how you are consuming the custom hook but making apiOptions a state value by using useState should work just fine. This way you can serve it to your custom hook as a state value like so:
const [apiOptions, setApiOptions] = useState({ a: 1 })
const { data } = useExample(apiOptions)
This way it's going to change only when you use setApiOptions.
Example #1
import { useState, useEffect } from 'react';
const useExample = (apiOptions) => {
const [data, updateData] = useState([]);
useEffect(() => {
console.log('effect triggered')
}, [apiOptions]);
return {
data
};
}
export default function App() {
const [apiOptions, setApiOptions] = useState({ a: 1 })
const { data } = useExample(apiOptions);
const [somethingElse, setSomethingElse] = useState('default state')
return <div>
<button onClick={() => { setApiOptions({ a: 1 }) }}>change apiOptions</button>
<button onClick={() => { setSomethingElse('state') }}>
change something else to force rerender
</button>
</div>;
}
Alternatively
You could write a deep comparable useEffect as described here:
function deepCompareEquals(a, b){
// TODO: implement deep comparison here
// something like lodash
// return _.isEqual(a, b);
}
function useDeepCompareMemoize(value) {
const ref = useRef()
// it can be done by using useMemo as well
// but useRef is rather cleaner and easier
if (!deepCompareEquals(value, ref.current)) {
ref.current = value
}
return ref.current
}
function useDeepCompareEffect(callback, dependencies) {
useEffect(
callback,
dependencies.map(useDeepCompareMemoize)
)
}
You can use it like you'd use useEffect.
I just found a solution which works for me.
You have to use usePrevious() and _.isEqual() from Lodash.
Inside the useEffect(), you put a condition if the previous apiOptions equals to the current apiOptions. If true, do nothing. If false updateData().
Example :
const useExample = (apiOptions) => {
const myPreviousState = usePrevious(apiOptions);
const [data, updateData] = useState([]);
useEffect(() => {
if (myPreviousState && !_.isEqual(myPreviousState, apiOptions)) {
updateData(apiOptions);
}
}, [apiOptions])
}
usePrevious(value) is a custom hook which create a ref with useRef().
You can found it from the Official React Hook documentation.
const usePrevious = value => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
If the input is shallow enough that you think deep equality would still be fast, consider using JSON.stringify:
const useExample = (apiOptions) => {
const [data, updateData] = useState([]);
const apiOptionsJsonString = JSON.stringify(apiOptions);
useEffect(() => {
const apiOptionsObject = JSON.parse(apiOptionsJsonString);
doSomethingCool(apiOptionsObject).then(response => {
updateData(response.data);
})
}, [apiOptionsJsonString]);
return {
data
};
};
Note it won’t compare functions.
If you're real sure that you cannot control apiOptions then just replace native useEffect with https://github.com/kentcdodds/use-deep-compare-effect.
It's reallllly so simple in some case!
const objA = {
method: 'GET'
}
const objB = {
method: 'GET'
}
console.log(objA === objB) // false
Why objA not equal with objB? Coz JS just compare their address right? They are two diffirent obj. That's we all know!
The same as React hooks does!
So, also as we all know, objA.method === objB.method right? Coz they are literal.
The answer comes out:
React.useEffect(() => {
// do your facy work
}, [obj.method])
You can use useDeepCompareEffect, useCustomCompareEffect or write your own hook.
https://github.com/kentcdodds/use-deep-compare-effect
https://github.com/sanjagh/use-custom-compare-effect
One other option, if you have the ability to modify doSomethingCool:
If know exactly which non-Object properties you need, you can limit the list of dependencies to properties that useEffect will correctly interpret with ===, e.g.:
const useExample = (apiOptions) => {
const [data, updateData] = useState([]);
useEffect(() => {
const [data, updateData] = useState<any>([]);
doSomethingCool(apiOptions.method).then(res => {
updateData(response.data);
})
}, [apiOptions.method]);
return {
data
};
};
Related
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]);
I have the following context:
import React, { createContext, useState } from "react";
const OtherUsersContext = createContext(null);
export default OtherUsersContext;
export function OtherUsersProvider({ children }) {
const [otherUsers, setOtherUsers] = useState(new Map([]));
const addUser = (userId, userData) => {
setOtherUsers(
(prevOtherUsers) => new Map([...prevOtherUsers, [userId, userData]])
);
};
const updateUser = (userId, userData, merge = true) => {
...
};
const getUser = (userId) => otherUsers.get(userId);
const resetUsers = () => {
setOtherUsers(new Map([]));
};
return (
<OtherUsersContext.Provider
value={{
addUser,
updateUser,
getUser,
resetUsers,
}}
>
{children}
</OtherUsersContext.Provider>
);
}
In my app, when a user signs out, I need to reset this context's map, using the function "resetUsers".
Currently this is working good, but there has no sense to reset the map if it has no values, so I have changed the "resetUsers" function to:
const resetUsers = () => {
if(otherUsers.size) {
setOtherUsers(new Map([]));
}
}
And, this is not working good, because inside resetUsers, otherUsers.size is always 0. Something which disturbs me because outside the function, the value is the correct one...
...
const resetUsers = () => {
console.log(otherUsers.size); // 0
setOtherUsers(new Map([]));
};
console.log(otherUsers.size); // 5
return ( ...
Any ideas?
The functional updates part of the hooks docs. says:
If the new state is computed using the previous state, you can pass a function to setState.
So instead of just passing the new value to your setter, you can pass a function that depends on the previous state.
This means that you can do:
const resetUsers = () => {
setOtherUsers(prevOtherUsers => prevOtherUsers.size ? new Map([]): prevOtherUsers);
}
One tip, if you are not getting the most updated state value inside a function, then wrap it inside an useCallback.
Try this:
const resetUsers = useCallback(() => {
if (otherUsers.size > 0) {
console.log(otherUsers.size); // 5
setOtherUsers(new Map([]));
}
}, [otherUsers]);
Want to understand, how it can be built with pure js.
I expect it something like that:
const useStatePureJs = () => {
let state = null;
const setState = (value) => {
state = value
};
return [
state,
setState
];
};
const [state, setState] = useStatePureJs();
setState(5);
console.log(state);
But ofc my console.log will return null, because of scope initialization.
How I can make it be updated with pure js implemetation? Value I mean.
Do I need a watcher or something?
What about this? 😊
Seems to work as you would expect.
const useStatePureJs = () => {
let state;
const getState = () => {
return state
}
const setState = (x) => {
state = x
}
return [ getState, setState ]
}
const [ getState, setState ] = useStatePureJs()
setState(1)
console.log(getState()) // 1
setState(2)
console.log(getState()) // 2
There's a fixed number of settings that determine whether the component should be visible, i.e.:
const restrictions = {
isLogged: true, // TRUE stands for: check if the condition is met
hasMoney: true,
didWinYesterday: false,
}
For each restrictions key, I've created a state with useState and initialized them all with false, like:
const [isUserLogged, setIsUserLogged] = useState(false)
const [hasUserMoney, setHasUserMoney] = useState(false)
const [didUserWinYday, setDidUserWinYday] = useState(false)
Next, I am checking against each condition with useEffect and updating the state accordingly:
useEffect(() => {
const checkIfUserIsLogged = async () => {
// calling an API to get boolean
const isLogged = await API.call()
setIsUserLogged(isLogged)
}
// If the restriction is set to false, ignore checking and set relevant state
if (!restrictions.isLogged) {
setIsUserLogged(true)
return
}
checkIfUserIsLogged()
}, [restrictions.isLogged])
Finally, I am checking if I should render the actual component or should I break early like so:
if (!isUserLogged) return <p>User not logged in.</p>
The useEffect code and the check above is repeated 3 times in total. Each of the repetition is making different API call and is updating different state, but the overall structure stays the same.
I wish I could do it more DRY but not sure how to get started. Any tips are welcome, thanks!
I'd refactor a single atom into a custom useRestrictionState atom that internally deals with the effect:
const restrictions = {
isLogged: true, // TRUE stands for: check if the condition is met
hasMoney: true,
didWinYesterday: false,
};
function useRestrictionState(restrictionFlag, apiFunc) {
const [flag, setFlag] = React.useState(undefined);
useEffect(() => {
if (!restrictionFlag) {
setFlag(true);
} else {
apiFunc().then((result) => setFlag(result));
}
}, [restrictionFlag]);
return flag;
}
function Component() {
const isUserLogged = useRestrictionState(restrictions.isLogged, getLoginState);
const hasUserMoney = useRestrictionState(restrictions.hasMoney, getUserMoney);
const didUserWinYday = useRestrictionState(restrictions.didWinYesterday, getUserDidWinYesterday);
}
If you always need all of them, you can naturally wrap this in another custom hook:
function useUserRestrictionState(restrictions) {
const isUserLogged = useRestrictionState(restrictions.isLogged, getLoginState);
const hasUserMoney = useRestrictionState(restrictions.hasMoney, getUserMoney);
const didUserWinYday = useRestrictionState(restrictions.didWinYesterday, getUserDidWinYesterday);
return { isUserLogged, hasUserMoney, didUserWinYday };
}
function Component() {
const { isUserLogged, hasUserMoney, didUserWinYday } = useUserRestrictionState(restrictions);
}
Here's my take:
import { useState, useEffect } from 'react'
const useCheck = (condition: boolean, performCheck: () => boolean): boolean => {
const [isConditionMet, setIsConditionMet] = useState<boolean>(false)
useEffect(() => {
const check = async () => {
const isConditionMet = await performCheck()
setIsConditionMet(isConditionMet)
}
if (!condition) {
setIsConditionMet(true)
}
check()
}, [condition, performCheck])
return isConditionMet
}
export default useCheck
Usage:
const isUserLogged = useCheck(restrictions.isLogged, () => true) // 2nd parameter should be an API call in my case
I'm trying to implement a data stream that has to use inner observables, where I use one from mergeMap, concatMap etc.
e.g.:
const output$$ = input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
);
output$$.subscribe(console.log);
This works fine when logging into console.
But when I try to use it in React like below utilizing useEffect and useState hooks to update some text:
function App() {
const input$ = new Subject<string>();
const input$$ = input$.pipe(share());
const output$$ = input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
);
output$$.subscribe(console.log);
// This works
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
useEffect(() => {
const subscription = input$$.subscribe(setInput);
return () => {
subscription.unsubscribe();
};
}, [input$$]);
useEffect(() => {
const subscription = output$$.subscribe(setOutput);
// This doesn't
return () => {
subscription.unsubscribe();
};
}, [output$$]);
return (
<div className="App">
<input
onChange={event => input$.next(event.target.value)}
value={input}
/>
<p>{output}</p>
</div>
);
}
it starts acting weird/unpredictable (e.g.: sometimes the text is updated in the middle of typing, sometimes it doesn't update at all).
Things I have noticed:
If the inner observable completes immediately/is a promise that
resolves immediately, it works fine.
If we print to console instead of useEffect, it works fine.
I believe this has to do something with the inner workings of useEffect and how it captures and notices outside changes, but cannot get it working.
Any help is much appreciated.
Minimal reproduction of the case:
https://codesandbox.io/s/hooks-and-observables-1-7ygd8
I'm not quite sure what you're trying to achieve, but I found a number of problems which hopefully the following code fixes:
function App() {
// Create these observables only once.
const [input$] = useState(() => new Subject<string>());
const [input$$] = useState(() => input$.pipe(share()));
const [output$$] = useState(() => input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
));
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
// Create the subscription to input$$ on component mount, not on every render.
useEffect(() => {
const subscription = input$$.subscribe(setInput);
return () => {
subscription.unsubscribe();
};
}, []);
// Create the subscription to output$$ on component mount, not on every render.
useEffect(() => {
const subscription = output$$.subscribe(setOutput);
return () => {
subscription.unsubscribe();
};
}, []);
return (
<div className="App">
<input
onChange={event => input$.next(event.target.value)}
value={input}
/>
<p>{output}</p>
</div>
);
}
I had a similar task but the goal was to pipe and debounce the input test and execute ajax call.
The simple answer that you should init RxJS subject with arrow function in the react hook 'useState' in order to init subject once per init.
Then you should useEffect with empty array [] in order to create a pipe once on component init.
import React, { useEffect, useState } from "react";
import { ajax } from "rxjs/ajax";
import { debounceTime, delay, takeUntil } from "rxjs/operators";
import { Subject } from "rxjs/internal/Subject";
const App = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [filterChangedSubject] = useState(() => {
// Arrow function is used to init Singleton Subject. (in a scope of a current component)
return new Subject<string>();
});
useEffect(() => {
// Effect that will be initialized once on a react component init.
// Define your pipe here.
const subscription = filterChangedSubject
.pipe(debounceTime(200))
.subscribe((filter) => {
if (!filter) {
setLoading(false);
setItems([]);
return;
}
ajax(`https://swapi.dev/api/people?search=${filter}`)
.pipe(
// current running ajax is canceled on filter change.
takeUntil(filterChangedSubject)
)
.subscribe(
(results) => {
// Set items will cause render:
setItems(results.response.results);
},
() => {
setLoading(false);
},
() => {
setLoading(false);
}
);
});
return () => {
// On Component destroy. notify takeUntil to unsubscribe from current running ajax request
filterChangedSubject.next("");
// unsubscribe filter change listener
subscription.unsubscribe();
};
}, []);
const onFilterChange = (e) => {
// Notify subject about the filter change
filterChangedSubject.next(e.target.value);
};
return (
<div>
Cards
{loading && <div>Loading...</div>}
<input onChange={onFilterChange}></input>
{items && items.map((item, index) => <div key={index}>{item.name}</div>)}
</div>
);
};
export default App;