-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Playground screen UI states #33
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { types } from "mobx-state-tree"; | ||
import { delay } from "../utils/delay"; | ||
|
||
const Content = types.model("Content", { number: 0 }) | ||
const Loading = types.model("Loading") | ||
const Error = types.model("Error", { message: "" }) | ||
const PlaygroundUiState = types | ||
.model('PlaygroundUiState', { | ||
loading: types.maybe(Loading), | ||
error: types.maybe(Error), | ||
content: types.maybe(Content) | ||
}) | ||
.actions((self) => { | ||
function clear() { | ||
self.content = undefined | ||
self.error = undefined | ||
self.loading = undefined | ||
} | ||
function setLoading() { | ||
clear() | ||
self.loading = Loading.create() | ||
} | ||
function setError(error: string) { | ||
clear() | ||
self.error = Error.create({ message: error }) | ||
} | ||
function setContent(number: number) { | ||
clear() | ||
self.content = Content.create({ number: number }) | ||
} | ||
return { | ||
setLoading, setError, setContent | ||
} | ||
}) | ||
|
||
const initialLoading = () => PlaygroundUiState.create({ loading: Loading.create() }) | ||
|
||
export const PlaygroundStoreModel = types | ||
.model("PlaygroundStore") | ||
.props({ | ||
uiState: types.optional(PlaygroundUiState, initialLoading) | ||
}) | ||
.actions((store) => { | ||
let interval = null | ||
|
||
async function startCounter() { | ||
store.uiState.setLoading() | ||
await delay(1000) | ||
console.log("START") | ||
|
||
interval = setInterval(() => { | ||
if (store.uiState.content) { | ||
const value = store.uiState.content.number | ||
if (value < 30) { | ||
store.uiState.setContent(value + 1) | ||
} else { | ||
store.uiState.setError("Encountered a fake error") | ||
stopCounter() | ||
} | ||
} else { | ||
store.uiState.setContent(0) | ||
} | ||
}, 500) | ||
} | ||
|
||
function stopCounter() { | ||
console.log("STOP") | ||
clearInterval(interval) | ||
interval = null | ||
} | ||
async function resetCounter() { | ||
stopCounter() | ||
startCounter() | ||
} | ||
return { | ||
startCounter, stopCounter, resetCounter | ||
} | ||
}) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,76 +1,108 @@ | ||
import React, { FC } from "react" | ||
import * as React from "react" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is fine but since we have I actually found that this wildcard import is best practise (they exist in RN?) 😱 and shared another interesting guidelines below There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a difference in behaviour (at least on my VSCode so it might be my config) with autocomplete. When I start writing a function from react such as In case of The |
||
import { observer } from "mobx-react-lite" | ||
import { StackScreenProps } from "@react-navigation/stack" | ||
import { AppStackScreenProps } from "../navigators" | ||
import { Image, ImageStyle, TextStyle, View, ViewStyle } from "react-native" | ||
import { | ||
Text, Screen | ||
} from "../components" | ||
import { isRTL } from "../i18n" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is fine to go as its not really used, but incase it comes up again - this returns if the current device language direction is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's weird that this would need to be in the feature code though |
||
import { BaseScreenProps } from "../navigators" | ||
import { TextStyle, View, ViewStyle } from "react-native" | ||
import { Text, Button } from "../components" | ||
import { colors, spacing } from "../theme" | ||
import { useSafeAreaInsetsStyle } from "../utils/useSafeAreaInsetsStyle" | ||
import { useFocusEffect } from "@react-navigation/core" | ||
import { useStores } from "../models" | ||
|
||
interface PlaygroundScreenProps extends BaseScreenProps<"Playground"> { | ||
} | ||
|
||
export const PlaygroundScreen: FC<StackScreenProps<AppStackScreenProps, "Playground">> = observer(function PlaygroundScreen() { | ||
const $bottomContainerInsets = useSafeAreaInsetsStyle(["bottom"]) | ||
export const PlaygroundScreen: React.FC<PlaygroundScreenProps> = observer((props) => { | ||
const $bottomContainerInsets = useSafeAreaInsetsStyle(["bottom"]) | ||
const { playgroundStore } = useStores() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎉 Nice work Leaving some extra info for others: https://mobx.js.org/defining-data-stores.html |
||
|
||
return ( | ||
<View style={$container}> | ||
<View style={$topContainer}> | ||
<Text | ||
testID="welcome-heading" | ||
style={$welcomeHeading} | ||
tx="playgroundScreen.readyForLaunch" | ||
preset="heading" | ||
/> | ||
<Text tx="playgroundScreen.postscript" preset="subheading" /> | ||
</View> | ||
useFocusEffect( | ||
React.useCallback(() => { | ||
playgroundStore.startCounter() | ||
return () => playgroundStore.stopCounter() | ||
}, [playgroundStore]) | ||
) | ||
|
||
<View style={[$bottomContainer, $bottomContainerInsets]}> | ||
<Text tx="playgroundScreen.exciting" size="md" /> | ||
</View> | ||
</View> | ||
) | ||
}) | ||
return <View style={$container}> | ||
{ | ||
StateComponent( | ||
$bottomContainerInsets, | ||
playgroundStore.uiState, | ||
() => playgroundStore.resetCounter() | ||
) | ||
} | ||
</View> | ||
}) | ||
|
||
const $container: ViewStyle = { | ||
flex: 1, | ||
backgroundColor: colors.background, | ||
} | ||
function StateComponent(insets, uiState, onResetClicked) { | ||
const { loading, error, content } = uiState | ||
if (loading) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we make this a Switch Case operator? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure how to write a switch like that, these are separate properties of the |
||
return LoadingComponent(insets) | ||
} else if (error) { | ||
return ErrorComponent(insets, error, onResetClicked) | ||
} else if (content) { | ||
return ContentComponent(insets, content, onResetClicked) | ||
} | ||
throw new Error(`UiState type is not handled ${JSON.stringify(uiState)}`) | ||
} | ||
|
||
const $topContainer: ViewStyle = { | ||
flexShrink: 1, | ||
flexGrow: 1, | ||
flexBasis: "57%", | ||
justifyContent: "center", | ||
paddingHorizontal: spacing.large, | ||
} | ||
function ContentComponent(insets, content, onResetClicked) { | ||
return <View> | ||
<View style={$topContainer}> | ||
<Text | ||
testID="welcome-heading" | ||
style={$welcomeHeading} | ||
tx="playgroundScreen.readyForLaunch" | ||
preset="heading" | ||
/> | ||
<Text tx="playgroundScreen.postscript" preset="subheading" /> | ||
</View> | ||
|
||
const $bottomContainer: ViewStyle = { | ||
flexShrink: 1, | ||
flexGrow: 0, | ||
flexBasis: "43%", | ||
backgroundColor: colors.palette.neutral100, | ||
borderTopLeftRadius: 16, | ||
borderTopRightRadius: 16, | ||
paddingHorizontal: spacing.large, | ||
justifyContent: "space-around", | ||
} | ||
const $welcomeLogo: ImageStyle = { | ||
height: 88, | ||
width: "100%", | ||
marginBottom: spacing.huge, | ||
} | ||
<View style={[$bottomContainer, insets]}> | ||
<Text tx="playgroundScreen.exciting" size="md" /> | ||
<Text text={`Value is: ${content.number}`} size="md" /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @juankysoriano had a question in the nav PR I didn't get to - but this shows off the difference between |
||
<Button text="Reset" onPress={onResetClicked} /> | ||
</View> | ||
</View> | ||
} | ||
|
||
const $welcomeFace: ImageStyle = { | ||
height: 169, | ||
width: 269, | ||
position: "absolute", | ||
bottom: -47, | ||
right: -80, | ||
transform: [{ scaleX: isRTL ? -1 : 1 }], | ||
} | ||
function ErrorComponent(insets, error, onResetClicked) { | ||
return <View style={[insets]}> | ||
<Text text={`ERROR: ${error.message}`} size="md" /> | ||
<Button text="Refresh" onPress={onResetClicked} /> | ||
</View> | ||
} | ||
|
||
function LoadingComponent(insets) { | ||
return <View style={[insets, { justifyContent: "space-around", flexGrow: 1 }]}> | ||
<Text text={`LOADING`} size="md" style={{ textAlign: "center" }} /> | ||
</View> | ||
} | ||
|
||
const $container: ViewStyle = { | ||
flex: 1, | ||
backgroundColor: colors.background, | ||
} | ||
|
||
const $topContainer: ViewStyle = { | ||
flexShrink: 1, | ||
flexGrow: 1, | ||
flexBasis: "57%", | ||
justifyContent: "center", | ||
paddingHorizontal: spacing.large, | ||
} | ||
|
||
const $bottomContainer: ViewStyle = { | ||
flexShrink: 1, | ||
flexGrow: 0, | ||
flexBasis: "43%", | ||
backgroundColor: colors.palette.neutral100, | ||
borderTopLeftRadius: 16, | ||
borderTopRightRadius: 16, | ||
paddingHorizontal: spacing.large, | ||
justifyContent: "space-around", | ||
} | ||
|
||
const $welcomeHeading: TextStyle = { | ||
marginBottom: spacing.medium, | ||
} | ||
|
||
const $welcomeHeading: TextStyle = { | ||
marginBottom: spacing.medium, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When would we use
types.maybe()
vs.types.optional()
? (line 41
)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand correctly, optional means that if the value is not there (i.e. persisted from last session) then it would set the default value provided as the second argument. Maybe seems like the actual nullable type (with two variants, one uses
null
and the otherundefined
to express missing value)