React - Should we check if component is mounted before updating state? - reactjs

On unmountable components, is it necessary to confirm that the component is mounted before updating the state?
I mean, is this code OK?
function UnmountableScreen() {
const isMounted = useIsMounted();
const [isRefreshing, setIsRefreshing] = useState(false);
const handleOnRefresh = async () => {
if(!isMounted()) return;
setIsRefreshing(true);
await asyncOperation();
if(!isMounted()) return; // The component might be unmounted...
setIsRefreshing(true);
}
...
}

Deducing from the function name handleOnRefresh must be attached to a component
<Button onClick={handleOnRefresh} label="..."/>
UnmountableScreen must be mounted in order to render the above component, therefore those checks are meaningless.

Related

React native screens not re-rendering when custom hook state got changed

I am using a custom hook in app purchases.
const useInAppPurchase = () => {
const context = useContext(AuthGlobal)
const [isFullAppPurchased, setIsFullAppPurchased] = useState(false)
useEffect(() => {
console.log(`InAppPurchase useEffect is called`)
getProductsIAP()
return async () => {
try {
await disconnectAsync()
} catch (error) {}
}
}, [])
....
}
When I used this hook at AccountScreen (where I do the purchase) Account screen is getting re-rendered once the payment is done.
i.e. isFullAppPurchased is changing from false -> true
const AccountScreen = (props) => {
const width = useWindowDimensions().width
const {
isFullAppPurchased,
} = useInAppPurchase()
return (
// value is true after the purchase
<Text>{isFullAppPurchased}</Text>
)
}
But I am using the same hook in CategoryList screen and after the payment is done when I navigate to the CategoryList screen, The values (isFullAppPurchased) is not updated (still false).
But when I do the re-rendering manually then I get isFullAppPurchased as true.
const CategoryList = (props) => {
const navigation = useNavigation()
const { isFullAppPurchased } = useInAppPurchase()
return (
// still value is false
<Text>{isFullAppPurchased}</Text>
)
}
What is the reason for this behaviour ? How should I re-render CategoryList screen once the payment is done ?
Thank you.
I see hook make API request only on mount, if whole parent component didn't unmount and rendered a new, value of hook stays same.
E.g. dependencies array is empty - [] so hook doesn't request data again.
Probably better idea is to pass isFullAppPurchased via context or redux from top level.
And put state and function to update that state in same place.

How can we get the updated props in the same function, in a react functional component?

I'm using redux to manage my state. After making an API call, I would update my redux store, the component would receive the updated props from redux and I would handle update the state based on the props.
With class components I currently have a method that does this:
onEdit = async () => {
if(!this.props.item) {
await this.props.fetchItem();
}
this.setState({
item: this.props.item
});
}
The updated props would be used in the setState.
Here is an example of something similar with a functional component:
const Component = (item) => {
...
const onEdit = async () => {
if(!item) {
await this.props.fetchItem();
}
setState(item) // this doesn't work
};
...
}
Obviously the above doesn't work since item uses the same props as before.
I recognize that useEffect is probably the solution most people would go for, but I was just wondering if there was a similar solution to the class component method above, since the syntax is very nice.
Put the item into state instead of props, and use props as the parameter to the Component instead of item:
const Component = (props) => {
const [item, setItem] = useState();
const onEdit = () => {
if(!item) {
props.fetchItem().then(setItem).catch(handleError);
}
};
// ...
};

Best practice for marking hooks as not to be reused in multiple places

It seems a lot of my custom React Hooks don't work well, or seem to cause a big performance overhead if they are reused in multiple places. For example:
A hook that is only called in the context provider and sets up some context state/setters for the rest of the app to use
A hook that should only be called in a root component of a Route to setup some default state for the page
A hook that checks if a resource is cached and if not, retrieves it from the backend
Is there any way to ensure that a hook is only referenced once in a stack? Eg. I would like to trigger a warning or error when I call this hook in multiple components in the same cycle.
Alternatively, is there a pattern that I should use that simply prevents it being a problem to reuse such hooks?
Example of hook that should not be reused (third example). If I would use this hook in multiple places, I would most likely end up making unnecessary API calls.
export function useFetchIfNotCached({id}) {
const {apiResources} = useContext(AppContext);
useEffect(() => {
if (!apiResources[id]) {
fetchApiResource(id); // sets result into apiResources
}
}, [apiResources]);
return apiResources[id];
}
Example of what I want to prevent (please don't point out that this is a contrived example, I know, it's just to illustrate the problem):
export function Parent({id}) {
const resource = useFetchIfNotCached({id});
return <Child id={id}>{resource.Name}</Child>
}
export function Child({id}) {
const resource = useFetchIfNotCached({id}); // <--- should not be allowed
return <div>Child: {resource.Name}</div>
}
You need to transform your custom hooks into singleton stores, and subscribe to them directly from any component.
See reusable library implementation.
const Comp1 = () => {
const something = useCounter(); // is a singleton
}
const Comp2 = () => {
const something = useCounter(); // same something, no reset
}
To ensure that a hook called only once, you only need to add a state for it.
const useCustomHook = () => {
const [isCalled, setIsCalled] = useState(false);
// Your hook logic
const [state, setState] = useState(null);
const onSetState = (value) => {
setIsCalled(true);
setState(value);
};
return { state, setState: onSetState, isCalled };
};
Edit:
If you introduce a global variable in your custom hook you will get the expected result. Thats because global variables are not tied to component's lifecycle
let isCalledOnce = false;
const useCustomHook = () => {
// Your hook logic
const [state, setState] = useState(null);
const onSetState = (value) => {
if (!isCalledOnce) {
isCalledOnce = true;
setState(false);
}
};
return { state, setState: onSetState, isCalled };
};

React Hook - useCustomHook to set outside useState and useRef

I have the main component along with local state using useState and useRef, I also another custom hook, inside the custom hook I would like to reset my main component's state and ref, am I doing correctly by below?
// custom hook
const useLoadData = ({startLoad, setStartLoad, setLoadCompleted, setUserNameRef}) => {
useEffect(() => {
const fetchUser = async() => { await fetchFromApi(...); return userName;};
if (startLoad) {
const newUserName = fetchUser();
setStartLoad(false);
setLoadCompleted(true);
setUserNameRef(newUserName);
}
}, [startLoad]);
}
// main component
const myMainComp = () {
const [startLoad, setStartLoad] = useState(false);
const [loadCompleted, setLoadCompleted] = useState(false);
const userNameRef = useRef("");
const setUserNameRef = (username) => { this.userNameRef.current = username; }
useLoadData(startLoad, setStartLoad, setLoadCompleted, setUserNameRef);
refreshPage = (userId) => {
setStartLoad(true);
}
}
Am I using the custom hook correctly like by passing all external state value and setState method in? Also I found even I don't use useEffect in my customHook, it also works as expected, so do I need to use useEffect in my custom hook? Any review and suggestion is welcome!
First, I think isn't a good approach you use component methods inside custom hook (like "set" methods provided by useState). You are binding the hook with the main component's internal logic. If the purpose of custom hook is fetch data from API, it need to provide to main component the vars that can be able the main component to manipulate its state by itself (like return isFetching, error, data, etc. and don't call any main component set method inside hook).

getSnapshotBeforeUpdate using react hooks

How can I implement the same logic that getSnapshotBeforeUpdate gives me using react hooks?
As per the React Hooks FAQ, there isn't a way to implement getSnapshotBeforeUpdate and ComponentDidCatch lifecycle method with hooks yet
Do Hooks cover all use cases for classes?
Our goal is for Hooks to cover all use cases for classes as soon as
possible. There are no Hook equivalents to the uncommon
getSnapshotBeforeUpdate and componentDidCatch lifecycles yet, but we
plan to add them soon.
It is a very early time for Hooks, so some integrations like DevTools
support or Flow/TypeScript typings may not be ready yet. Some
third-party libraries might also not be compatible with Hooks at the
moment.
We cannot get the snapshot data in any of the hooks (useLayoutEffect or useEffect) as both will give the updated DOM values by the time they are triggered, the best place to capture the data is just before the setting the state. for example here I am capturing the scroll position before setting the state.
function ChatBox(props){
const [state, setState] = useState({chatFetched:[],isFetching:false});
const listRef = useRef();
const previousScrollDiff = useRef(0);
// on mount
useEffect(()=>{
getSomeMessagesApi().then(resp=>{
const chatFetched = [...state.chatFetched,...resp];
setState({chatFetched});
})
},[]);
useLayoutEffect(()=>{
// use the captured snapshot here
listRef.current.scrollTop = listRef.current.scrollHeight - previousScrollDiff.current;
},[state.chatFetched])
useEffect(()=>{
// don't use captured snapshot here ,will cause jerk effect in scroll
},[state.chatFetched]);
const onScroll = (event) => {
const topReached = (event.target.scrollTop === 0);
if(topReached && !state.isFetching){
setState({...state, isFetching:true});
getSomeMessagesApi().then(resp=>{
const chatFetched = [...resp,...state.chatFetched];
// here I am capturing the data ie.., scroll position
previousScrollDiff.current = listRef.current.scrollHeight -listRef.current.scrollTop;
setState({chatFetched, isFetching:false});
})
}
}
return (
<div className="ui container">
<div
className="ui container chat list"
style={{height:'420px', width:'500px',overflow:'auto'}}
ref={listRef}
onScroll={onScroll}
>
{state.chatFetched.map((message)=>{
return <ChatLi data ={message} key ={message.key}></ChatLi>
})}
</div>
</div>
);
};
we can also useMemo to capture the data before dom update happens,
function ChatBox(props){
const [state, setState] = useState({chatFetched:[],isFetching:false});
const listRef = useRef();
const previousScrollDiff = useRef(0);
// on mount
useEffect(()=>{
getSomeMessagesApi().then(resp=>{
const chatFetched = [...state.chatFetched,...resp];
setState({chatFetched});
})
},[]);
useLayoutEffect(()=>{
// use the captured snapshot here
listRef.current.scrollTop = listRef.current.scrollHeight - previousScrollDiff.current;
},[state.chatFetched])
useEffect(()=>{
// don't use captured snapshot here ,will cause jerk effect in scroll
},[state.chatFetched]);
useMemo(() => {
// caputure dom info in use effect
if(scrollUl.current){
previousScrollDiff.current = scrollUl.current.scrollHeight - scrollUl.current.scrollTop;
}
}, [state.chatFetched]);
const onScroll = (event) => {
const topReached = (event.target.scrollTop === 0);
if(topReached && !state.isFetching){
setState({...state, isFetching:true});
getSomeMessagesApi().then(resp=>{
const chatFetched = [...resp,...state.chatFetched];
setState({chatFetched, isFetching:false});
})
}
}
return (
<div className="ui container">
<div
className="ui container chat list"
style={{height:'420px', width:'500px',overflow:'auto'}}
ref={listRef}
onScroll={onScroll}
>
{state.chatFetched.map((message)=>{
return <ChatLi data ={message} key ={message.key}></ChatLi>
})}
</div>
</div>
);
};
In the above examples I am trying to do the same that is shown here in the getSnapshotBeforeUpdate react doc
You can use useMemo() instead of getSnapshotBeforeUpdate(). Read more here about how to memoize calculations with React Hooks.
Here a simple example:
It always that the user types (onChange) an irrelevant state in point of view of the List Component is changed and because of this it re-rendering and can re-rendering more than 50 times it depends on the user typing, so it's used useMemo() to memoize the List Component and it stated that just todoList listens.
import List from './List'
const todo = (props) => {
const [inputIsValid, setInputIsValid] = useState(false)
const inputValidationHandler = (event) => {
if(event.target.value.trim() === '') {
setInputIsValid(false)
} else {
setInputIsValid(true)
}
}
return <React.Fragment>
<input
type="text"
placeholder="Todo"
onChange={inputValidationHandler}
/>
{
useMemo(() => (
<List items={todoList} onClick={todoRemoveHandler} />
), [todoList])
}
</React.Fragment>
}
export default todo
Short answer: There isn't a react hook for it! But we can create a custom one!
That's using useEffect() and useLayoutEffect()! As they are the key elements!
The final example is all last! So make sure to check it (Our custom hooks equivalent).
useEffect() and useLayoutEffect()
useEffect => useEffect runs asynchronously and after a render is painted to the screen.
You cause a render somehow (change state, or the parent re-renders)
React renders your component (calls it)
The screen is visually updated
THEN useEffect runs
useEffect() => render() => dom mutation => repaint => useEffect() [access dom new state] (changing dom directly) => repaint
==> Meaning useEffect() is like comonentDidUpdate()!
useLayoutEffect => useLayoutEffect, on the other hand, runs synchronously after a render but before the screen is updated. That goes:
You cause a render somehow (change state, or the parent re-renders)
React renders your component (calls it)
useLayoutEffect runs, and React waits for it to finish.
The screen is visually updated
useLayoutEffect() => render => dom mutation [detached] => useLayoutEffec() [access dom new state] (mutate dom) => repaint (commit, attach)
===> Meaning useLayoutEffect() run like getSnapshotBeforeUpdate()
Knowing this! We can create our custom hooks that allow us to do things like with getSnapshotBeforeUpdate() and didComponentUpdate().
Such an example would be updating the scrolling for auto update in chat applications!
usePreviousPropsAndState()
Similar to the usePrevious() hook mentionned in "how to get previous prop and state"
Here an hook implementation for saving and getting previous props and state!
const usePrevPropsAndState = (props, state) => {
const prevPropsAndStateRef = useRef({ props: null, state: null })
const prevProps = prevPropsAndStateRef.current.props
const prevState = prevPropsAndStateRef.current.state
useEffect(() => {
prevPropsAndStateRef.current = { props, state }
})
return { prevProps, prevState }
}
We can see how we need to pass the props and state object!
What you pass is what you get! So it's easy to work with! An object will do well!
useGetSnapshotBeforeUpdate & useComponentDidUpdate
Here the total solution or implementation
const useGetSnapshotBeforeUpdate = (cb, props, state) => {
// get prev props and state
const { prevProps, prevState } = usePrevPropsAndState(props, state)
const snapshot = useRef(null)
// getSnapshotBeforeUpdate (execute before the changes are comitted for painting! Before anythingg show on screen) - not run on mount + run on every update
const componentJustMounted = useRef(true)
useLayoutEffect(() => {
if (!componentJustMounted.current) { // skip first run at mount
snapshot.current = cb(prevProps, prevState)
}
componentJustMounted.current = false
})
// ________ a hook construction within a hook with closure __________
const useComponentDidUpdate = cb => {
// run after the changes are applied (commited) and apparent on screen
useEffect(() => {
if (!componentJustMounted.current) { // skip first run at mount
cb(prevProps, prevState, snapshot.current)
}
})
}
// returning the ComponentDidUpdate hook!
return useComponentDidUpdate
}
You can notice how we constructed the hook within the other hook! Make use of closure! And accessing elements directly! And linking the two hooks!
pre-commit phase and commit phase (and effects hooks)
I used those terms! What does it actually means ?
class example
From the doc
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// Are we adding new items to the list?
// Capture the scroll position so we can adjust scroll later.
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// If we have a snapshot value, we've just added new items.
// Adjust scroll so these new items don't push the old ones out of view.
// (snapshot here is the value returned from getSnapshotBeforeUpdate)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}
Our custom hooks equivalent
const App = props => {
// other stuff ...
const useComponentDidUpdate = useGetSnapshotBeforeUpdate(
(prevProps, prevState) => {
if (prevProps.list.length < props.list.length) {
const list = listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
},
props,
state
)
useComponentDidUpdate((prevProps, prevState, snapshot) => {
if (snapshot !== null) {
const list = listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
})
// rest ...
}
useEffectLayout() in useGetSnapshotBeforeUpdate hook will execute first!
useEffect() in useComponentDidUpdate will execute after!
As just was shown in the lifecyle schema!

Resources