So I am re-writing a component with hooks, and I ran into an interesting challenge, I need to mimick some old behaviour of componentWillReceiveProps with the useEffect hook.
My old code looks like:
componentWillReceiveProps(nextProps: Props) {
const prevLateVal = get(`lateMinutes[${bookingId}].value`, this.props);
const nextLateVal = get(`lateMinutes[${bookingId}].value`, nextProps); //see here,
//we use next props
if (typeof nextLateVal !== 'undefined' && prevLateVal !== nextLateVal) {
client.connect(bookingId, nextLateVal === null ? 0 : nextLateVal);
}
}
You see, I am initiating a const based on nextProps, then in the if statement i do a couple checks based on that nextVal, now, I know that we can specify a second argument to useEffect to run it only when a prop changes, but what about those checks, how can i implement something similar to nextProps?
You can create custom hook:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.prevLateVal = value;
});
return ref.prevLateVal;
}
and get it used in useEffect()
const Component = (props) => {
const currentLateValue = get(`lateMinutes[${bookingId}].value`, props)
const prevLateVal = usePrevious(currentLateValue);
useEffect(() => {
if(prevLateVal !== currentLateValue) {
// process here
}
}, [currentLateValue]) // This will be executed only if currentLateValue changes.
}
You can use useRef to save the prev props, and use useEffect to run when props change, something like this :
function MyComponent(props) {
const prevProps = useRef(props);
useEffect(() => {
const prevLateVal = get(`lateMinutes[${bookingId}].value`, prevProps.current);
const nextLateVal = get(`lateMinutes[${bookingId}].value`, props);
if (typeof nextLateVal !== 'undefined' && prevLateVal !== nextLateVal) {
client.connect(bookingId, nextLateVal === null ? 0 : nextLateVal);
}
prevProps.current = props;
}, [props, bookingId]);
return (<div>...</div>)
}
With the current logic, you want to fire a side-effect only on lateMinutes[${bookingId}].value change:
const Component = props => {
useEffect(() => {
console.log('prop lateMinutes changed');
// ...
}, [props[`lateMinutes[${bookingId}].value`]);
return ...
};
You don’t need hooks to handle prerender lifecycle.
Just put the things in the functional component before returning JSX as the function itself is equivalent to render method of class based component.
Related
Need to update the boolean state value on map click. And based on that updated value, new component rendered.
const [layerClick, setLayerClick] = useState(false);
useEffect(() => {
if (streetsCurrentHeatStateMap !== null) {
streetsCurrentHeatStateMap.on('click', (e) => {
if (layerClick === false) {
popupToggler();
}
})
}
}, [
layerClick,
]);
And here is the popupToggler function
const popupToggler = async () => {
setLayerClick((layerClick) => !layerClick);
}
And it is used for the conditional rendering of the Popup Component.
{layerClick && (
<PopupContent />
)}
So problem is: It always call the popuptoggler function, mean the value of state not updated. Any help?
here is the context
I am invoking a function invoked(..) from another component. Inside invoked I setState but the state does not get set. It does not trigger the useEffect nor does it take the updated value in the useInterval.
Below is the code.
Component 1 = exampleComp.ts
const exampleComp = ({initValue: ExampleProps}) {
const [params, setParams] = useState<{field1: null | string}> ({field1: null});
const invoked = (x: string) => {
console.log("inside invoked"); // this gets printed
setParams({"field1": x});
};
useEffect(() => {
console.log(params); // does not come here :(
}, [params]);
const fetchData = {...};
useInterval(
() => {
if (!params.field1 || fetching) return;
console.log("in interval", params); // does not come here too :(
fetchData(params.field1);
},
params && !fetching ? 5000: null,
);
return {invoked, ...}
}
Component 2:
const newComp = ({initValue: ExampleProps}) {
const {invoked, ..} = useExampleComp({...}); //Example comp is the above component.
useEffect(() => {
invoked(x);
}, []);
}
Any help will be appreciated! thanks.
Turns out that the component was unmounting too early. This happens when the scope of the component is too narrow.
If you have a similar problem, try logging the component to see if this is indeed happening.
How to get previoius state value in react hook?
Here is my code. I used useRef hook also but didnot work.
const [ category, setCategory] = useState([]);
function updateMarkers(data) {
let newCategory= [];
let {name, age} = data.payload;
//Here need to check previous cateogory has same age or not.
category && category.map((cate, i) => {
if (cate.age!== age) {
newCategory[i] = data.payload
}
})
}
setCategory((prevCategory) => [...prevCategory, ...newCategory]) }
let prevCategory = usePrevious(category);
console.log(' prevcategory', prevCategory);
You can create usePrevious like this:
export default function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
And using:
const previousState = usePrevious(state)
you can access previous state doing the following
setState(prevState => prevState)
I have a mystery. Consider the following custom React hook that fetches data by time period and stores the results in a Map:
export function useDataByPeriod(dateRanges: PeriodFilter[]) {
const isMounted = useMountedState();
const [data, setData] = useState(
new Map(
dateRanges.map(dateRange => [
dateRange,
makeAsyncIsLoading({ isLoading: false }) as AsyncState<MyData[]>
])
)
);
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(new Map(data.set(period, asyncState)));
}
},
[setData, data, isMounted]
);
useEffect(() => {
if (dateRanges.length === 0) {
return;
}
const loadData = () => {
const client = makeClient();
dateRanges.map(dateRange => {
updateData(dateRange, makeAsyncIsLoading({ isLoading: true }));
return client
.getData(dateRange.dateFrom, dateRange.dateTo)
.then(periodData => {
updateData(dateRange, makeAsyncData(periodData));
})
.catch(error => {
const errorString = `Problem fetching ${dateRange.displayPeriod} (${dateRange.dateFrom} - ${dateRange.dateTo})`;
console.error(errorString, error);
updateData(dateRange, makeAsyncError(errorString));
});
});
};
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dateRanges /*, updateData - for some reason when included this triggers infinite renders */]);
return data;
}
The useEffect is being repeatedly triggered when updateData is added as a dependency. If I exclude it as a dependency then everything works / behaves as expected but eslint complains I'm violating react-hooks/exhaustive-deps.
Given updateData has been useCallback-ed I'm at a loss to understand why it should repeatedly trigger renders. Can anyone shed any light please?
The problem lies in the useCallback/useEffect used in combination. One has to be careful with dependency arrays in both useCallback and useEffect, as the change in the useCallback dependency array will trigger the useEffect to run.
The “data” variable is used inside useCallback dependency array, and when the setData is called react will rerun function component with new value for data variable and that triggers a chain of calls.
Call stack would look something like this:
useEffect run
updateData called
setState called
component re-renders with new state data
new value for data triggers useCallback
updateData changed
triggers useEffect again
To solve the problem you would need to remove the “data” variable from the useCallback dependency array. I find it to be a good practice to not include a component state in the dependency arrays whenever possible.
If you need to change component state from the useEffect or useCallback and the new state is a function of the previous state, you can pass the function that receives a current state as parameter and returns a new state.
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(existingData => new Map(existingData.set(period, asyncState)));
}
},
[setData, isMounted]
);
In your example you need the current state only to calculate next state so that should work.
This is what I now have based on #jure's comment above:
I think the problem is that the "data" variable is included in the dependency array of useCallback. Every time you setData, the data variable is changed that triggers useCallback to provide new updateData and that triggers useEffect. Try to implement updateData without a dependecy on the data variable. you can do something like setData(d=>new Map(d.set(period, asyncState)) to avoid passing "data" variable to useCallback
I adjusted my code in the manners suggested and it worked. Thanks!
export function useDataByPeriod(dateRanges: PeriodFilter[]) {
const isMounted = useMountedState();
const [data, setData] = useState(
new Map(
dateRanges.map(dateRange => [
dateRange,
makeAsyncIsLoading({ isLoading: false }) as AsyncState<MyData[]>
])
)
);
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(existingData => new Map(existingData.set(period, asyncState)));
}
},
[setData, isMounted]
);
useEffect(() => {
if (dateRanges.length === 0) {
return;
}
const loadData = () => {
const client = makeClient();
dateRanges.map(dateRange => {
updateData(dateRange, makeAsyncIsLoading({ isLoading: true }));
return client
.getData(dateRange.dateFrom, dateRange.dateTo)
.then(traffic => {
updateData(dateRange, makeAsyncData(traffic));
})
.catch(error => {
const errorString = `Problem fetching ${dateRange.displayPeriod} (${dateRange.dateFrom} - ${dateRange.dateTo})`;
console.error(errorString, error);
updateData(dateRange, makeAsyncError(errorString));
});
});
};
loadData();
}, [dateRanges , updateData]);
return data;
}
I am using ReactHooks. I am trying to access ref of User component in useEffect function, but I am getting elRef.current value as null, though I passed elRef.current as second argument to useEffect. I am supposed to get reference to an element, but outside (function body) of useEffect, ref value is available. Why is that ? How can I get elRef.current value inside useEffect?
code
import React, { Component, useState, useRef, useEffect } from "react";
const useFetch = url => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(
() => {
setIsLoading(true);
fetch(url)
.then(response => {
if (!response.ok) throw Error(response.statusText);
return response.json();
})
.then(json => {
setIsLoading(false);
setData(json.data);
})
.catch(error => {
setIsLoading(false);
setError(error);
});
},
[url]
);
return { data, isLoading, error };
};
const User = ({ id }) => {
const elRef = useRef(null);
const { data: user } = useFetch(`https://reqres.in/api/users/${id}`);
useEffect(() => {
console.log("ref", elRef.current);
}, [elRef.current]);
if (!user) return null;
return <div ref={elRef}>{user.first_name + " " + user.last_name}</div>;
};
class App extends Component {
state = {
userId: 1
};
handleNextClick = () => {
this.setState(prevState => ({
userId: prevState.userId + 1
}));
};
handlePrevNext = () => {
this.setState(prevState => ({
userId: prevState.userId - 1
}));
};
render() {
return (
<div>
<button
onClick={() => this.handlePrevClick()}
disabled={this.state.userId === 1}
>
Prevoius
</button>
<button onClick={() => this.handleNextClick()}>Next</button>
<User id={this.state.userId} />
</div>
);
}
}
export default App;
Codesandbox link
Thanks !
You should use useCallback instead of useRef as suggested in the reactjs docs.
React will call that callback whenever the ref gets attached to a different node.
Replace this:
const elRef = useRef(null);
useEffect(() => {
console.log("ref", elRef.current);
}, [elRef.current]);
with this:
const elRef = useCallback(node => {
if (node !== null) {
console.log("ref", node); // node = elRef.current
}
}, []);
It's a predictable behaviour.
As mentioned #estus you faced with this because first time when it's called on componentDidMount you're getting null (initial value) and get's updated only once on next elRef changing because, actually, reference still being the same.
If you need to reflect on every user change, you should pass [user] as second argument to function to make sure useEffect fired when user is changed.
Here is updated sandbox.
Hope it helped.
When you use a function as a ref, it is called with the instance when it is ready. So the easiest way to make the ref observable is to use useState instead of useRef:
const [element, setElement] = useState<Element | null>(null);
return <div ref={setElement}></div>;
Then you can use it in dependency arrays for other hooks, just like any other const value:
useEffect(() => {
if (element) console.log(element);
}, [element]);
See also How to rerender when refs change.
useEffect is used as both componentDidMount and componentDidUpdate,
at the time of component mount you added a condition:
if (!user) return null;
return <div ref={elRef}>{user.first_name + " " + user.last_name}</div>;
because of the above condition at the time of mount, you don't have the user, so it returns null and div is not mounted in the DOM in which you are adding ref, so inside useEffect you are not getting elRef's current value as it is not rendered.
And on the click of next as the div is mounted in the dom you got the value of elRef.current.
The assumption here is that useEffect needs to detect changes to ref.current, so needs to have the ref or ref.currentin the dependencies list. I think this is due to es-lint being a bit over-pedantic.
Actually, the whole point of useEffect is that it guarantees not to run until the rendering is complete and the DOM is ready to go. That is how it handles side-effects.
So by the time useEffect is executed, we can be sure that elRef.current is set.
The problem with your code is that you don't run the renderer with <div ref={elRef}...> until after user is populated. So the DOM node you want elRef to reference doesn't yet exist. That is why you get the null logging - nothing to do with dependencies.
BTW: one possible alternative is to populate the div inside the effect hook:
useEffect(
() => {
if(!user) return;
elRef.current.innerHTML = `${user.first_name} ${user.last_name}`;
}, [user]
);
...
//if (!user) return null;// Remove this line
return <div ref={elRef}></div>; //return div every time.
That way the if (!user) return null; line in the User component is unnecessary. Remove it, and elRef.current is guaranteed to be populated with the div node from the very beginning.
set a useEffect on the elem's.current:
let elem = useRef();
useEffect(() => {
// ...
}, [elem.current]);