Better support for react-native
outaTiME opened this issue Β· 27 comments
Hi guys,
it could be great to use AppState (https://reactnative.dev/docs/appstate) for revalidateOnFocus
and NetInfo (https://github.com/react-native-community/react-native-netinfo) for revalidateOnReconnect
, errorRetryInterval
, loadingTimeout
when running in react-native context.
Any advice with that? im handling revalidations manually for this cases.
Thanks !!!
propose a solution for it
Situation
now we have
web "online"
event for detecting network connect state;
web "visibilitychange"
and "focus"
event for detecting focus state;
Implementation
we can encapsulate them as default configs in swr config. i.e.
const defaultIsVisible = () => isdocumentvisible() && isonline()
const defaultFocusDetector = (revalidate) => {
// this function is overridable by configuration
if (window.addEventListener) {
window.addEventListener('...', revalidate, ...)
// ...
}
}
const defaultNetworkConnectionDetector = (onConnect) => {
.... // this function is overridable by configuration
}
config.networkConnectionDetector = defaultNetworkConnectionDetector;
config.focusDetector = defaultFocusDetector;
config.isVisible = defaultIsVisible;
default handlers are for web only, but you can override them through swr configuration setting
and we pass the revalidate or reconnect handlers to them
in use-swr.ts we can do
if (!IS_SERVER && config.networkConnectionDetector && config.revalidateOnReconnect) {
config.networkConnectionDetector(reconnect = softRevalidate)
}
in config.ts we could also setup focus detector
// focus revalidate
let eventsbinded = false
if (typeof window !== 'undefined' && defaultConfig.focusDetector && !eventsbinded) {
const revalidate = () => {
if (!config.isFocusing()) return
...
}
defaultConfig.focusDetector(revalidate)
// only bind the events once
eventsbinded = true
}
Usage
import React, { useEffect, useState } from "react";
import { AppState } from "react-native";
import useSWR from 'swr'
const AppStateExample = () => {
const [appState, setAppState] = useState(AppState.currentState);
const { data } = useSWR(key, fetcher, {
focusDetector(revalidate) {
handleStateChange = (...) => { if (...) revalidate }
AppState.addEventListener("change", handleStateChange);
return () => {
AppState.removeEventListener("change", handleStateChange);
}
}
})
// ...
Not sure if it's ok to do so. wanna get more feedbacks from community and maintainers. if it's doable I can try to submit a PR for that. cc @shuding
@huozhi great explanation it looks wired but it could work,
enhancement on usage (reavalidation must ocurr only when foreground imo):
import React, { useEffect, useState } from "react";
import { AppState } from "react-native";
import useSWR from 'swr'
const AppStateExample = () => {
const [appState, setAppState] = useState(AppState.currentState);
const { data } = useSWR(key, fetcher, {
focusDetector(revalidate) {
const _handleAppStateChange = nextAppState => {
if (appState.match(/inactive|background/) && nextAppState === "active") {
console.log("App has come to the foreground!");
revalidate();
}
setAppState(nextAppState);
};
AppState.addEventListener("change", _handleAppStateChange);
return () => {
AppState.removeEventListener("change", _handleAppStateChange);
}
}
})
}
note: focusDetector need to be recreated in each render to works as expected how useSWR options works in this case? and what about of errorRetryInterval
, loadingTimeout
(they are related to connection too).
@outaTiME thanks for the clear example for app state detection. π
for errorRetryInterval
and loadingTime
, they're numbers but related to the network state on slow or fast network. that also could rely on some platform layer API to detect if it's slow. probably we need on more callback for that for react native or other runtime, such as using NetInfoCellularGeneration
value from react-native-netinfo.
the idea is to decouple the platform runtime specific part out of swr, like the listeners on window
in browser runtime.
It would great if we could use our customised rule to determine when the data needs to be refreshed
there's a RFC to support plugin/middleware pattern in swr to make it more cross-platform adaptive. the APIs are not settled yet and discussions are welcomed. once we had a good form of APIs and certain bahaviors of plugin/middleware, we'll surly start the support for RN ASAP
So I assume with 0.3.7 this could also be used in global config?
seems it's more complicated, the latest changes are not enough : (. we need to give swr ability to customize the way to listen on focus/visible/connect events. need more changes on that. will bump here later once we have more support on it.
Update published my solution to npm here: https://github.com/nandorojo/swr-react-native
FWIW, I made useSWRReactNavigation
. I just wrote it and haven't tested it in my app yet. Once I test it out I can publish it to NPM.
Usage
const { data, revalidate } = useSWR(key, fetcher)
useSWRReactNavigation({
revalidate
})
You can also pass options to it:
const { data, revalidate } = useSWR(key, fetcher)
useSWRReactNavigation({
revalidate
// optional, defaults copied from SWR
revalidateOnFocus: true,
revalidateOnReconnect: true,
focusThrottleInterval: 5000,
})
Code
import { responseInterface, ConfigInterface } from 'swr'
import { useRef, useEffect } from 'react'
import { AppState } from 'react-native'
import { useNavigation } from '@react-navigation/native'
import NetInfo, { NetInfoState } from '@react-native-community/netinfo'
type Props<Data, Error> = {
/**
* Required: pass the `revalidate` function returned to you by SWR.
*/
revalidate: responseInterface<Data, Error>['revalidate']
} & Pick<
ConfigInterface,
'revalidateOnFocus' | 'revalidateOnReconnect' | 'focusThrottleInterval'
>
import { Platform } from 'react-native'
/**
* swr-react-native
*
* This helps you revalidate your SWR calls, based on navigation actions in `react-navigation`.
*/
export default function useSWRReactNavigation<Data = any, Error = any>(
props: Props<Data, Error>
) {
const {
revalidate,
// copy defaults from SWR
revalidateOnFocus = true,
revalidateOnReconnect = true,
focusThrottleInterval = 5000,
} = props
const { addListener } = useNavigation()
const lastFocusedAt = useRef<number | null>(null)
const fetchRef = useRef(revalidate)
useEffect(() => {
fetchRef.current = revalidate
})
const focusCount = useRef(0)
const previousAppState = useRef(AppState.currentState)
const previousNetworkState = useRef<NetInfoState | null>(null)
useEffect(() => {
// SWR does all of this on web.
if (Platform.OS === 'web') return
let unsubscribeReconnect: ReturnType<
typeof NetInfo.addEventListener
> | null = null
if (revalidateOnReconnect && typeof window !== 'undefined') {
unsubscribeReconnect = NetInfo.addEventListener((state) => {
if (
previousNetworkState.current?.isInternetReachable === false &&
state.isConnected &&
state.isInternetReachable
) {
fetchRef.current()
}
previousNetworkState.current = state
})
}
const onFocus = () => {
if (focusCount.current < 1) {
focusCount.current++
return
}
const isThrottled =
focusThrottleInterval &&
lastFocusedAt.current &&
Date.now() - lastFocusedAt.current <= focusThrottleInterval
if (!isThrottled) {
lastFocusedAt.current = Date.now()
fetchRef.current()
}
}
const onAppStateChange = (nextAppState: AppState['currentState']) => {
if (
previousAppState.current.match(/inactive|background/) &&
nextAppState === 'active'
) {
onFocus()
}
previousAppState.current = nextAppState
}
let unsubscribeFocus: ReturnType<typeof addListener> | null = null
if (revalidateOnFocus) {
unsubscribeFocus = addListener('focus', onFocus)
AppState.addEventListener('change', onAppStateChange)
}
return () => {
if (revalidateOnFocus) {
unsubscribeFocus?.()
AppState.removeEventListener('change', onAppStateChange)
}
if (revalidateOnReconnect) {
unsubscribeReconnect?.()
}
}
}, [
addListener,
focusThrottleInterval,
revalidateOnFocus,
revalidateOnReconnect,
])
}
I'd appreciate help testing it. It listens for 1) network reconnections, 2) app state focus, 3) react navigation focus.
You could also take out the react navigation portion of the code if you don't need that.
@nandorojo Thanks a lot for this useful custom hook! No npm package, yet? π
I had to make two changes to make the basics work:
- The argument passed to
AppState.addEventListener
(andAppState.removeEventListener
) needs to bechange
instead offocus
, seems like a typo in your version.
...
if (revalidateOnFocus) {
unsubscribeFocus = addListener('focus', onFocus);
AppState.addEventListener('change', onAppStateChange);
}
return () => {
if (revalidateOnFocus) {
unsubscribeFocus?.();
AppState.removeEventListener('change', onAppStateChange);
}
if (revalidateOnReconnect) {
unsubscribeReconnect?.();
}
};
...
new Date().getTime()
didn't work as expected for me. It returned seconds. This might be related to anti-fingerprinting settings or simply some other issue. Instead I usedDate.now()
, but had to change the operator inonFocus
. Instead of>
use<=
.
...
const onFocus = () => {
const isThrottled =
focusThrottleInterval &&
lastFocusedAt.current &&
Date.now() - lastFocusedAt.current <= focusThrottleInterval;
if (!isThrottled) {
lastFocusedAt.current = Date.now();
fetchRef.current();
}
};
...
One major issue I'm still facing is a duplicate load on first focus, because react-navigation
fires an indistinguishable focus
event on first and subsequent focuses and swr
is already triggering a fetch on first load of the app.
@te-online thanks for finding those issues! I should be able to get it on npm soon.
One major issue I'm still facing is a duplicate load on first focus, because react-navigation fires an indistinguishable focus event on first and subsequent focuses and swr is already triggering a fetch on first load of the app.
If this is always the case, it seems like we could keep track of a focusCount
and only trigger it if it's > 1
.
@nandorojo I found an added focusCount to be working fine. Here's a gist https://gist.github.com/te-online/4eaa564aa02546ea25247d2f079f935e
I finally got around to publishing this to NPM (@nandorojo/swr-react-native
)!
The package is a drop-in replacement for SWR, with added support for focus events, reconnection, and react navigation.
Link: https://github.com/nandorojo/swr-react-native
Drop-in usage is as simple as:
- import useSWR from 'swr'
+ import useSWRNative from '@nandorojo/swr-react-native'
Or, you can use it like this:
import { useSWRNativeRevalidate } from '@nandorojo/swr-react-native'
const { data, revalidate } = useSWR(key, fetcher)
useSWRNativeRevalidate({
// required: pass your revalidate function returned by SWR
revalidate
// optional, defaults copied from SWR
revalidateOnFocus: true,
revalidateOnReconnect: true,
focusThrottleInterval: 5000,
})
You might want to use this method if you're using useSWRInfinite
, for example.
@te-online thanks for your help testing and fixing this!
With the release of latest swr beta 1.0.0-beta.9, now I'm able to build the RN example with expo.
Please checkout huozhi/swr-rn-example. It provides a simple customization of "focus revalidation" and custom cache usage. I think it could resolve basic problems of RN usage.
Update: Looking at the new middleware documentation (https://swr.vercel.app/docs/middleware#keep-previous-result), it seems you can call hooks in a middleware. So I believe it's possible to build RN react-navigation integration with middleware system.
For RN I think it'd be more useful if we could register on focus event per useSWR / useSWRInfinite hook to allow focus revalidation per screen, as done in @nandorojo/swr-react-native
.
For example, when a user navigates to another screen, it's often useful to revalidate all swr calls in that screen instead of AppState change, which I guess is only possible by calling useFocusEffect per hook in react-navigation.
π SWR v1 is released. We updated the documentation for using SWR with React Native. Free feel to open new issues for giving feedback of the integration on React Native.
Thanks @huozhi!
One issue for React Native remains: knowing if a particular screen is focused.
That can be solved with my library linked above #417 (comment)
Perhaps I can release it as a middleware too (although I think its original implementation would work as a middleware anyway!)
In order for a screen to know if itβs focused, you have a hook like this:
const focused = useIsFocused()
The issue is, you have to call this function within the screen. So the SWRConfig
βs global focus checker doesnβt work, since itβs at the root of the app.
Maybe the middleware is the just the best use case here.
@nandorojo Interesting, thanks for pointing out the limitation of the example! I'll take a look at your library to see how to make the example more resilient. Also feel free to update the swr doc if you have any idea to make the example fit realy world react native app better! π
Here's how the middleware could work (simplified a bit).
function withScreenFocus(useSWRNext) {
return function useSWRFocus(key, fn, opts) {
const { addListener } = useNavigation()
const { revalidateOnFocus = opts.revalidateOnFocus ?? true } = useSWRConfig()
const swr = useSWRNext(key, fn, opts)
const { mutate } = swr
useEffect(() => {
const remove = addListener('focus', () => mutate())
return () => remove()
}, [mutate, addListener])
return swr
}
}
Then in your app:
useSWR(key, fn, { use: [withScreenFocus] })
Or, globally:
<SWRConfig value={{ ..., use: [withScreenFocus] }} />
The remaining use-cases seem like they're dealt with in the docs.
The one issue with that middleware is, you're manually calling mutate
, rather than using the initFocus
API. @huozhi do you see any downsides to this?
An alternative could be to expose a hook-specific initFocus
:
function withScreenFocus(useSWRNext) {
return function useSWRFocus(key, fn, opts) {
const { addListener } = useNavigation()
return useSWRNext(key, fn, {
...opts,
initFocus(trigger) {
const unsubscribe = addListener('focus', () => trigger())
return () => unsubscribe()
}
})
}
}
The only issue with that is, it seemingly overrides the globally-set initFocus
, rather than extending it.
I think instead of creating it as a middleware, you can create it as a wrapper component (which has SWRConfig
inside):
<SWRRNFocus>
<App/>
</SWRRNFocus>
Which has the initFocus
logic inside.
The issue is, you'd need to add this to every single screen. React Navigation tells you if a screen is focused, only within that screen.
That's why a middleware would be necessary; you have to check for screen focus in the same component scope as the SWR hook.
I see. The problem of middleware for this case is, every component will try to register an event listener :(
Is there a way to put this on a higher level, e.g. the component entry, and detect if the current focused screen has changed?
Unfortunately not, since React Navigation allows for many nested navigators (stacks inside of tabs, etc) itβs somewhat impossible to know what your navigation scope is from a hook.
Iβm not sure what the overhead is of adding a lot of navigation listeners to each component.
My current implementation is to add it sparingly to certain screens using my library linked to here. Maybe Iβll just stick with that, since it doesnβt need to literally update every hook on screen focus.
This is like the similar problem that why we changed from createCache
API to option provider
+ useSWRConfig
. For basic single screen view app, global setup is easy like the current example did. And sometimes we find out that it's hard to work with hook based APIs, in this case is useNavigation
.
Since hook now is a widely adopted concept in react community, we might need to rethink of the current API to customize focus
and reconnect
events detection. @nandorojo In short term, I think the 1st one (using mutate
API) is a good choice π. Long term I'd prefer to polish the API to support hooks more elegantly.