Replies: 2 comments
-
I've updated my code to use the following a linked observable for the active challenge. After that the render fewer hooks error went away but the useTranslations hook causes the import {Challenge} from '@/lib/types/challenge'
import {linked, observable} from '@legendapp/state'
import {api} from '@/lib/services/api/api'
import {TxKeyPath} from '@/lib/i18n'
interface Store {
challenges: Challenge[]
initialized: boolean
initialize: () => Promise<void>
error: TxKeyPath | null
fetch: () => Promise<void>
}
export const challengesStore$ = observable<Store>({
challenges: [],
challenges_keyExtractor: (item: Challenge) => item.uuid,
initialized: false,
error: null,
initialize: async () => {
if (__DEV__) {
console.log("initializing challenges store")
}
try {
await challengesStore$.fetch()
} catch (err) {
console.error('Error initializing challenges:', err)
challengesStore$.assign({
initialized: true,
error: 'common:errorInitializing'
})
}
},
fetch: async () => {
if (__DEV__) {
console.log("fetching challenges")
}
const response = await api.getChallenges()
if (response.kind == "ok") {
challengesStore$.assign({
challenges: response.data,
initialized: true,
error: null
})
} else {
return Promise.reject(response)
}
}
})
export const activeChallenge$ = observable(linked({
get: () => {
return challengesStore$.challenges.get().find(challenge => challenge.status === 'active') || null
},
set: (updatedChallenge) => {
if (updatedChallenge.value === null) return
const challenges = challengesStore$.challenges.get()
const challengeIndex = challenges.findIndex(c => c.uuid === updatedChallenge.value.uuid)
if (challengeIndex !== -1) {
challengesStore$.challenges[challengeIndex].set(updatedChallenge.value)
}
}
})) And updated the ui code: import {$styles, ThemedStyle} from '@/lib/theme'
import {Screen} from '@/components/Screen'
import {Text} from '@/components/Text'
import {For, use$, Show, Switch, observer} from '@legendapp/state/react'
import {activeChallenge$, challengesStore$} from '@/lib/stores/challenge'
import {observable, Observable} from '@legendapp/state'
import {Alert, Button as RNButton, ScrollView, TouchableOpacity, ViewStyle, View, RefreshControl} from 'react-native'
import {useAppTheme} from '@/lib/utils/useAppTheme'
import {api} from '@/lib/services/api/api'
import {ChallengeStatus} from '@/lib/types/challenge'
import {useTranslation} from 'react-i18next'
import {useState} from 'react'
import {translate} from '@/lib/i18n'
const refreshing$ = observable<boolean>(false)
const ChallengesScreen = observer(function ChallengesScreen() {
const { themed } = useAppTheme()
// const [refreshing, setRefreshing] = useState<boolean>(false)
const refreshing = use$(refreshing$)
const onRefresh = async () => {
refreshing$.set(true)
// setRefreshing(true)
await challengesStore$.fetch()
// setRefreshing(false)
refreshing$.set(false)
}
return (
<Screen preset={'scroll'} safeAreaEdges={['top']} ScrollViewProps={{
refreshControl: <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}}>
{/*<Show if={!activeChallenge$}>*/}
{/* {() => (*/}
{/* <ScrollView*/}
{/* horizontal*/}
{/* decelerationRate={'fast'}*/}
{/* disableIntervalMomentum*/}
{/* showsHorizontalScrollIndicator={false}*/}
{/* contentContainerStyle={themed($scrollView)}*/}
{/* >*/}
{/* <For each={challengesStore$.challenges} item={ChooseChallengeItem} optimized/>*/}
{/* </ScrollView>*/}
{/* )}*/}
{/*</Show>*/}
<Show
if={activeChallenge$}
else={() => (
<ScrollView
horizontal
decelerationRate={'fast'}
disableIntervalMomentum
showsHorizontalScrollIndicator={false}
contentContainerStyle={themed($scrollView)}
>
<For each={challengesStore$.challenges} item={ChooseChallengeItem} optimized/>
</ScrollView>
)}
>
{() => (
<ActiveChallenge item$={activeChallenge$}/>
// <For each={challengesStore$.challenges} item={ActiveChallenge} optimized/>
)}
</Show>
</Screen>
)
})
export default ChallengesScreen
const $scrollView: ThemedStyle<ViewStyle> = ({ spacing }) => ({
marginTop: spacing.md,
gap: spacing.md,
})
const ChooseChallengeItem = ({item$}: { item$: Observable }) => {
const title = use$(item$.title)
const description = use$(item$.description)
const status = use$(item$.status)
const { themed } = useAppTheme()
const onUpdateStatus = async (status: ChallengeStatus) => {
const uuid = item$.uuid.peek()
const response = await api.patchChallenge(uuid, { status })
if (response.kind === 'ok') {
item$.set(response.data)
} else {
console.error('show error')
}
}
const onPress = async () => {
if (status === 'completed') return
Alert.alert(title, description, [
{
text: translate('common:cancel'),
style: 'cancel',
},
{
text: translate('challengesScreen:activate'),
style: 'default',
onPress: () => onUpdateStatus('active'),
}
])
}
return(
<TouchableOpacity style={themed($challengeItem)} onPress={onPress} activeOpacity={0.5}>
<Text text={title}/>
<Switch value={item$.status}>
{{
'completed': () => <Text tx={'challengesScreen:completed'}/>,
default: () => <RNButton title={translate('challengesScreen:showDetails')} onPress={onPress}/>
}}
</Switch>
</TouchableOpacity>
)
}
const $challengeItem: ThemedStyle<ViewStyle> = ({ colors, spacing, borderRadius }) => ({
backgroundColor: colors.palette.neutral200,
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
gap: spacing.md,
borderRadius: borderRadius.md,
})
const ActiveChallenge = ({item$}: { item$: Observable }) => {
const status = use$(item$.status)
const title = use$(item$.title)
const description = use$(item$.description)
// const { t } = useTranslation()
const { themed } = useAppTheme()
if (status !== 'active') return null
const onCancel = async () => {
const uuid = item$.uuid.peek()
const response = await api.patchChallenge(uuid, { status: 'proposed' })
if (response.kind === 'ok') {
item$.set(response.data)
} else {
console.error('show error')
}
}
const onComplete = async () => {
const uuid = item$.uuid.peek()
const response = await api.patchChallenge(uuid, { status: 'completed' })
if (response.kind === 'ok') {
item$.set(response.data)
} else {
console.error('show error')
}
}
return (
<View style={themed($activeChallenge)}>
<Text size={'lg'} weight={'semiBold'}>{title}</Text>
<Text size={'xs'}>{description}</Text>
<View style={[$styles.row, themed($buttonGroup)]}>
<RNButton title={translate('common:cancel')} onPress={onCancel}/>
<RNButton title={translate('challengesScreen:complete')} onPress={onComplete}/>
</View>
</View>
)
}
const $activeChallenge: ThemedStyle<ViewStyle> = ({ spacing }) => ({
marginTop: spacing.md,
paddingHorizontal: spacing.md,
gap: spacing.xxs,
})
const $buttonGroup: ThemedStyle<ViewStyle> = ({ spacing }) => ({
gap: spacing.xs,
alignItems: 'center',
justifyContent: 'center'
}) |
Beta Was this translation helpful? Give feedback.
0 replies
-
All the problems came from using the react compiler unfortunately |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Somehow I keep getting this error in production with my challenges component: "Rendered fewer hooks than expected. This may be caused by an accidental early return statement.". Basically what I'm trying to do is a user can have one active challenge at the time and if the user doesn't have one we show a choose challenge list. The challenge screen is rendered as an expo tab and after viewing the tab and activating/finishing some challenges and switching the tab I get the error. I'm probably missing something or not structuring the code properly.
The reason for using For for the active challenges is so that I can write directly back to the observable. Maybe there are better ways to do it(please let me know :)). Thanks in advance!
This is the screen code:
This is the observable:
Beta Was this translation helpful? Give feedback.
All reactions