Closed outaTiME closed 3 years ago
propose a solution for it
now we have
web "online"
event for detecting network connect state;
web "visibilitychange"
and "focus"
event for detecting focus state;
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
}
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
I think this issue answers my question: https://github.com/vercel/swr/discussions/614
Any of you @outaTiME @huozhi working on a PR for that?
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.
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,
})
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:
AppState.addEventListener
(and AppState.removeEventListener
) needs to be change
instead of focus
, 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 used Date.now()
, but had to change the operator in onFocus
. 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 https://github.com/vercel/swr/issues/417#issuecomment-737420642
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.
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) forrevalidateOnReconnect
,errorRetryInterval
,loadingTimeout
when running in react-native context.Any advice with that? im handling revalidations manually for this cases.
Thanks !!!