Skip to content
This repository has been archived by the owner on Dec 3, 2022. It is now read-only.

useFocusEffect / useIsFocused #43

Merged
merged 8 commits into from Oct 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
77 changes: 74 additions & 3 deletions README.md
@@ -1,7 +1,10 @@
# React Navigation Hooks

[![npm version](https://badge.fury.io/js/react-navigation-hooks.svg)](https://badge.fury.io/js/react-navigation-hooks) [![npm downloads](https://img.shields.io/npm/dm/react-navigation-hooks.svg)](https://www.npmjs.com/package/react-navigation-hooks) [![CircleCI badge](https://circleci.com/gh/react-navigation/hooks/tree/master.svg?style=shield)](https://circleci.com/gh/react-navigation/hooks/tree/master) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://reactnavigation.org/docs/contributing.html)

🏄‍♀️ Surfing the wave of React Hook hype with a few convenience hooks for `@react-navigation/core` v3. Destined to work on web, server, and React Native. Contributions welcome!
🏄‍♀️ Surfing the wave of React Hook hype with a few convenience hooks for `@react-navigation/core` v3/v4. Destined to work on web, server, and React Native. Contributions welcome!

**IMPORTANT**: [react-navigation v5](https://github.com/react-navigation/navigation-ex) is already on its way and is a full rewrite (including hooks). This project will not live past v4, and will try to make the migration path from v4 to v5 easy by not introducing any new hook that won't be in v5.

## Examples (web only so far)

Expand Down Expand Up @@ -95,13 +98,80 @@ function ReportNavigationEvents() {

The event payload will be the same as provided by `addListener`, as documented here: https://reactnavigation.org/docs/en/navigation-prop.html#addlistener-subscribe-to-updates-to-navigation-lifecycle

### useIsFocused()

Convenient way to know if the screen currently has focus.

```js
function MyScreen() {
const isFocused = useIsFocused();
return <Text>{isFocused ? 'Focused' : 'Not Focused'}</Text>;
}
```

### useFocusEffect(callback)

Permit to execute an effect when the screen takes focus, and cleanup the effect when the screen loses focus.

```js
function MyScreen() {
useFocusEffect(useCallback(() => {
console.debug("screen takes focus");
return () => console.debug("screen loses focus");
}, []));
return <View>...</View>;
}
```

**NOTE**: To avoid the running the effect too often, it's important to wrap the callback in useCallback before passing it to `useFocusEffect` as shown in the example. The effect will re-execute everytime the callback changes if the screen is focused.

`useFocusEffect` can be helpful to refetch some screen data on params changes:

```js
function Profile({ userId }) {
const [user, setUser] = React.useState(null);

const fetchUser = React.useCallback(() => {
const request = API.fetchUser(userId).then(
data => setUser(data),
error => alert(error.message)
);

return () => request.abort();
}, [userId]);

useFocusEffect(fetchUser);

return <ProfileContent user={user} />;
}
```


`useFocusEffect` can be helpful to handle hardware back behavior on currently focused screen:

```js
const useBackHandler = (backHandler: () => boolean) => {
useFocusEffect(() => {
const subscription = BackHandler.addEventListener('hardwareBackPress', backHandler);
return () => subscription.remove();
});
};
```




### useFocusState()

**deprecated**: this hook does not exist in v5, you should rather use `useIsFocused`


Convenient way of subscribing to events and observing focus state of the current screen.

```js
function MyFocusTag() {
return <p>{useFocusState().isFocused ? 'Focused' : 'Not Focused'}</p>;
function MyScreen() {
const focusState = useFocusState();
return <Text>{focusState.isFocused ? 'Focused' : 'Not Focused'}</Text>;
}
```

Expand All @@ -111,3 +181,4 @@ One (always, and only one) of the following values will be true in the focus sta
- isBlurring
- isBlurred
- isFocusing

65 changes: 65 additions & 0 deletions src/Hooks.ts
@@ -1,6 +1,7 @@
import {
useState,
useContext,
useEffect,
useLayoutEffect,
useRef,
useCallback,
Expand Down Expand Up @@ -143,3 +144,67 @@ export function useFocusState() {

return focusState;
}

type EffectCallback = (() => void) | (() => () => void);

// Inspired by same hook from react-navigation v5
// See https://github.com/react-navigation/hooks/issues/39#issuecomment-534694135
export const useFocusEffect = (callback: EffectCallback) => {
const navigation = useNavigation();

useEffect(() => {
let isFocused = false;
let cleanup: (() => void) | void;

if (navigation.isFocused()) {
cleanup = callback();
isFocused = true;
}

const focusSubscription = navigation.addListener('willFocus', () => {
// If callback was already called for focus, avoid calling it again
// The focus event may also fire on intial render, so we guard against runing the effect twice
if (isFocused) {
return;
}

cleanup && cleanup();
cleanup = callback();
isFocused = true;
});

const blurSubscription = navigation.addListener('willBlur', () => {
slorber marked this conversation as resolved.
Show resolved Hide resolved
cleanup && cleanup();
cleanup = undefined;
isFocused = false;
});

return () => {
cleanup && cleanup();
focusSubscription.remove();
blurSubscription.remove();
};
}, [callback, navigation]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@slorber If I run setParams in callback, navigation will change and invoke callback again, it will raise a infinite loop

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for reporting this.

@satya164 how do you handle those loops in v5? I guess we should fix it the same way in v5 and here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v5 doesn't update navigation prop on params update since route is a separate prop. regarding how to handle it in v4, user needs to add a check manually that params are out of date before trying to update them. they should do it anyways to prevent unnecessary updates to the screen regardless of the loop anyways since we don't do any equality checks like setState does.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@satya164 I understand your point about preventing useless re-renders/infinite loops.

But if param value has to change, we still don't necessarily want to re-execute useFocusEffect again (at least user should be in control imho)

I mean:

This one should re-execute on userId param change

useFocusEffect(useCallback(() => { ... },[params.userId]));

But this one should rather not re-execute even if any param changes:

useFocusEffect(useCallback(() => { ... },[]));

So I think it's not correct to make this hook depend on navigation here, as the object is unstable it will tend to produce unwanted effect re-execution. I guess it will even re-execute if the parent re-renders for example.

I've opened another issue: #48

};

export const useIsFocused = () => {
const navigation = useNavigation();
const getNavigation = useGetter(navigation);
const [focused, setFocused] = useState(navigation.isFocused);

useEffect(() => {
const nav = getNavigation();
const focusSubscription = nav.addListener('willFocus', () =>
setFocused(true)
);
const blurSubscription = nav.addListener('willBlur', () =>
setFocused(false)
);
return () => {
focusSubscription.remove();
blurSubscription.remove();
};
}, [getNavigation]);

return focused;
};