How to differentiate context values if their state changed in React useEffect - reactjs

const someFunction = () = {
...
const { data, setData, activeIndex, setActiveIndex } = useContext(MusicContext);
useEffect(() => {
console.log(" useEffect called");
// another component called setData or setActiveIndex which resulted here
// how to compare data to its prevState if it changed
// how to compare activeIndex to its prevState if it changed
}, [activeIndex, data]);
...
}
Above is some function which has a useEffect for two different context variables
data is an object proptype {} and activeIndex is a number
how do i compare data to its prevState if it changed?
how do i compare activeIndex to its prevState if it changed?
can i do it in a single useEffect block and need to open multiple?

You can useRef to store the last seen value. This is often extracted into a "usePrevious" custom hook.
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Now in your component, you can check against the previous value.
const SomeFunction = () = {
...
const { data, setData, activeIndex, setActiveIndex } = useContext(MusicContext);
const prevData = usePrevious( data );
const prevActiveIndex = usePrevious( activeIndex );
useEffect(() => {
if ( data !== prevData ) {
// do something
}
if ( activeIndex !== prevActiveIndex ) {
// do something
}
}, [activeIndex, data]);
...
}

Related

The updated value from the store does not change inside the function

I have some functional component. Inside component I get value from redux store (I am using redux-toolkit). Also I have handler inside this component.
The value of variable from store set after request to api via RTK Query. So, the variable first has a default value, and then changes to value from the api.
Problem:
The value of variable from redux store doesn't updated inside handler.
const SomeContainer = () => {
const dispatch = useDispatch();
const variableFromStore = useSelector(someSelectors.variableFromStore);
console.log(variableFromStore) **// correct value (updated)**
const handleSomeAction = () => {
console.log(variableFromStore) **// default value of init store (not updated)**
};
return <SomeComponent onSomeAction={handleSomeAction} />;
};
SomeComponent
const SomeComponent = (props) => {
const { list, onSomeAction } = props;
const moreRef = useRef(null);
const loadMore = () => {
if (moreRef.current) {
const scrollMorePosition = moreRef.current.getBoundingClientRect().bottom;
if (scrollMorePosition <= window.innerHeight) {
onSomeAction(); // Call handler from Container
}
}
};
useEffect(() => {
window.addEventListener('scroll', loadMore);
return () => {
window.removeEventListener('scroll', loadMore);
};
}, []);
return (
...
);
};
How is it possible? What do I not understand?)
The problem is you're unintentionally creating a closure around the original version of handleSomeAction:
useEffect(() => {
window.addEventListener('scroll', loadMore);
return () => {
window.removeEventListener('scroll', loadMore);
}
}, []);
The dependencies array here is empty, which means this effect only runs the first time that your component mounts, hence capturing the value of loadMore at the time the component mounts (which itself captures the value of onSomeAction at the time the component mounts).
The "easy fix" is to specify loadMore as a dependency for your effect:
useEffect(() => {
window.addEventListener('scroll', loadMore);
return () => {
window.removeEventListener('scroll', loadMore);
}
}, [loadMore]);
BUT! This will now create a new problem - handleSomeAction is recreated on every render, so your effect will now also run on every render!
So, without knowing more details about what you're actually trying to do, I'd use a ref to store a reference to the onSomeAction, and the inline the loadMore into your effect:
// A simple custom hook that updates a ref to whatever the latest value was passed
const useLatest = (value) => {
const ref = useRef();
ref.current = value;
return ref;
}
const SomeComponent = (props) => {
const { list, onSomeAction } = props;
const moreRef = useRef(null);
const onSomeActionRef = useLatest(onSomeAction);
useEffect(() => {
const loadMore = () => {
if (!moreRef.current) return;
const scrollMorePosition = moreRef.current.getBoundingClientRect().bottom;
if (scrollMorePosition <= window.innerHeight) {
onSomeActionRef.current();
}
}
window.addEventListener('scroll', loadMore);
return () => window.removeEventListener('scroll', loadMore);
}, []);
return (
...
);
};

React custom hook causes infinite loops

any idea why this custom hook with SWR causes an infinite loop?
export const useOrganization = () => {
const [data, setData] = useState<OrganizationModel | undefined>();
const { organizationId } = useParams();
const { data: dataSWR } = useSWRImmutable<
AxiosResponse<Omit<OrganizationModel, 'id'>>
>(`organizations/${organizationId}`, api);
useEffect(() => {
if (dataSWR?.data && organizationId) {
setData({ id: organizationId, ...dataSWR.data });
console.log({ id: organizationId, ...dataSWR.data });
}
});
return data;
};
I need to fetch data from API and add missing id from URL param. If I use setData(dataSWR.data), everything is fine. The problem occurs when setData({...dataSWR.data}) is used -> loop.
You need to use useEffect based on the scenario. When dataSWR changed the useEffect call again with new data.
You can add the dataSWR as dependencies argument in useEffect hook.
useEffect(() => { do something... }, [dataSWR])
Example:
export const useOrganization = () => {
const [data, setData] = useState<OrganizationModel | undefined>();
const { organizationId } = useParams();
const { data: dataSWR } = useSWRImmutable<AxiosResponse<Omit<OrganizationModel, 'id'>>>(`organizations/${organizationId}`, API);
useEffect(() => {
if (dataSWR?.data && organizationId) {
setData({ id: organizationId, ...dataSWR.data });
console.log({ id: organizationId, ...dataSWR.data });
};
},[dataSWR]);
return data;
};
Usage of hook:
const data = useOrganization()
Dependencies argument of useEffect is useEffect(callback, dependencies)
Let's explore side effects and runs:
Not provided: the side-effect runs after every rendering.
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Runs after EVERY rendering
});
}
An empty array []: the side-effect runs once after the initial rendering.
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Runs ONCE after initial rendering
}, []);
}
Has props or state values [prop1, prop2, ..., state1, state2]: the side-effect runs only when any dependency value changes.
import { useEffect, useState } from 'react';
function MyComponent({ prop }) {
const [state, setState] = useState('');
useEffect(() => {
// Runs ONCE after initial rendering
// and after every rendering ONLY IF `prop` or `state` changes
}, [prop, state]);
}
I found the solution - useMemo hook:
export const useOrganization = () => {
const { organizationId } = useParams();
const { data } = useSWRImmutable<
AxiosResponse<Omit<OrganizationModel, 'id'>>
>(`organizations/${organizationId}`, api);
const result = useMemo(() => {
if (data && organizationId) {
return { id: organizationId, ...data.data };
}
}, [data, organizationId]);
console.log('useOrganization');
return result;
};

How to cache react component fetches?

Let's say you have a component like this:
function MyComponent({ index }) {
const [data, setData] = useState('');
useEffect(() => {
(async function() {
const result = await fetchData(index);
setData(result);
})();
}, [index]);
return <h1>{data}</h1>;
}
Whenever the index changes, we will re-fetch the data and render it. How can I cache this so that we don't have to re-fetch the same index from before?
You want to memoize your results, like:
const cache = {};
async function memoFetch(index) {
if (cache[index]) {
return cache[index];
}
const data = fetchData(index);
cache[index] = data;
return data;
}
function MyComponent({ index }) {
const [data, setData] = useState('');
useEffect(() => {
(async function() {
const result = await memoFetch(index);
setData(result);
})();
}, [index]);
return <h1>{data}</h1>;
}
create a component cache Context:
const MyComponentCacheContext = React.createContext() ;
2.Create a component Cache Provider and give cache an intial Value of { } (empty Object) :
function MyComponentCacheProvider (props){
//we create this in step 3
const [cache,dispatch]= React.useReducer(MyComponentCacheReducer,{})
const value =[cache,dispatch]
return <MyComponentCacheContext value={value} {...props}/>
}
3.we will make the cache look like this => {{index:data},{index:data},...}so create a component cache Reducer that return the desired shape
:
function MyComponentCacheReducer (state,action) {
switch(action.type){
case 'ADD-TO-CACHE':
return {...state,[action.index]:action.data}
default :{
throw new Error (`unhandled action type ${action.type}`)
}
}
}
4.let's make all the code above within a custome hook so we can make it reusable:
function useMyComponentCache(){
const MyContext = React.useContext(MyComponentCacheContext)
if(!MyContext){
throw new Error (`useMyComponentCache must be within a MyComponentCacheProvider`)
}
return MyContext
}
5.let's customize your Component function so we can use the code above :
function MyComponent({ index }) {
const [data, setData] = useState('');
const [cache ,dispatch]= useMyComponentCache()
useEffect(() => {
if(cache[index]){
return setData(cache[index])
}else{
//here we fetch data --then--> we store it into the cache immedialty
const dataFetcher=fetchData(index).then(
data =>{
dispatch({type:'ADD-TO-CACHE',index,data})
return data
}
)
// updating state
const PromiseHandler =React.useCallback(
promise=>{
promise.then(
data => {
setData(data)
},
)
}
,[setData])
// Execution-Time
PromiseHandler(dataFetcher)
}
}, [cache,dispatch,index,PromiseHandler,setData]); // i assume that fetchData is
//a stable internal module so i didn t put it to the dependecies list
return <h1>{data}</h1>;
}
6.rendering step :put your component within CacheProvider !!!
function App(){
return ( <MyComponentCacheProvider>
<MyComponent index={0} />
</MyComponentCacheProvider>
)
}

React useEffect Invalid hook call. Hooks can only be called inside of the body of a function component

I always get this problem when using useEffect but I see others do it just fine. Here is my Code.
Component 1 (Parent):
const UsePrevious = (value)=> {
const ref = useRef(0);
useEffect(() => {
console.log(value)
ref.current = value;
});
return ref.current;
}
const FetchAndHighlight = (index) => {
const prevAmount = UsePrevious(index);
useEffect(() => {
if(prevAmount !== index) {
getNote(props.data.company_id)
.then(data=>{
return setNotes(data)
})
.catch(err=>console.error(err))
}
}, [index])
Component 2 (child)
function handleDelete (row){
console.log(filteredItems)
const index = filteredItems.findIndex(r=>r._id===row);
props.FetchAndHighlight(index);
deleteNote(row)
console.log(row)
toast.info('Note deleted sucessfully.')
if(props.combined === 0){props.setCombined(1)} else{props.setCombined(0)}
}
You can't pass hooks through props and more importantly, you must use hooks on top level and not in functions like handleDelete instead import the hook and use it:
// export
export const useFetchAndHighlight = (index) => {...}
// usage
import { useFetchAndHighlight } from './useFetchAndHighlight.js';
// Don't use inside function
useFetchAndHighlight(index);

Should I memoize the returned object of a custom React hook?

If I used useCallback for the methods of my custom hook, should I also memoize the returned object? I would assume that it would not create a new object everytime since it's composed with memoized methods and primitives.
export const useToggle = (args: UseToggleProps) => {
const initialState=
args.initialState !== undefined ? args.initialState : false
const [active, setActive] = useState(initialState)
const toggleOff = useCallback(() => {
setActive(false)
}, [])
const toggleOn = useCallback(() => {
setActive(true)
}, [])
const toggle = useCallback(() => {
setActive((active) => !active)
}, [])
// Should this also be memoized with useMemo?
return {
active,
toggle,
toggleOff,
toggleOn
}
}
You don't need to memoize the returned object unless you are passing the Object directly to function of useEffect in which case reference will fail
You don't need to add an extra layer of memoization over useCallback if you use it like this:
const Comp = () => {
const { toggleOn, toggleOff } = useToggle();
useEffect(() => {
console.log('Something here')
}, [toggleOn]);
return (
<Child toggleOn={toggleOn} toggleOff={toggleOff} />
)
}
However the usages like below code will need memoization of the returned object
const Comp = () => {
const toggle = useToggle();
useEffect(() => {
console.log('Something here')
}, [toggle]);
return (
<Child toggleHandlers={toggle} />
)
}

Resources