Skip to content
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

Merged
merged 3 commits into from
Mar 20, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions reactnative/RNPlayground/app/models/PlaygroundStore.ts
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)
Copy link
Contributor

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)

Copy link
Contributor Author

@JozefCeluch JozefCeluch Mar 20, 2023

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 other undefined to express missing value)

})
.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
}
})

2 changes: 2 additions & 0 deletions reactnative/RNPlayground/app/models/RootStore.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Instance, SnapshotOut, types } from "mobx-state-tree"
import { PlaygroundStoreModel } from "./PlaygroundStore"

/**
* A RootStore model.
*/
export const RootStoreModel = types.model("RootStore").props({
playgroundStore: types.optional(PlaygroundStoreModel, {})
})

/**
Expand Down
4 changes: 3 additions & 1 deletion reactnative/RNPlayground/app/navigators/AppNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
DefaultTheme,
NavigationContainer,
} from "@react-navigation/native"
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs"
import { createMaterialBottomTabNavigator, MaterialBottomTabScreenProps } from "@react-navigation/material-bottom-tabs"
import { observer } from "mobx-react-lite"
import React from "react"
import { useColorScheme } from "react-native"
Expand Down Expand Up @@ -42,6 +42,8 @@ export type NavigatorParamList = {

const Tab = createMaterialBottomTabNavigator<NavigatorParamList>()

export type BaseScreenProps<T extends keyof NavigatorParamList> = MaterialBottomTabScreenProps<NavigatorParamList, T>

function AppTabs() {
const iconSize = 24
return (
Expand Down
36 changes: 18 additions & 18 deletions reactnative/RNPlayground/app/screens/AboutScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { FC } from "react"
import { observer } from "mobx-react-lite"
import { StackScreenProps } from "@react-navigation/stack"
import { AppStackScreenProps } from "../navigators"
import { BaseScreenProps } from "../navigators"
import { Image, ImageStyle, TextStyle, View, ViewStyle } from "react-native"
import {
Text, Screen
Expand All @@ -10,26 +10,26 @@ import { isRTL } from "../i18n"
import { colors, spacing } from "../theme"
import { useSafeAreaInsetsStyle } from "../utils/useSafeAreaInsetsStyle"

export const AboutScreen: FC<StackScreenProps<AppStackScreenProps, "About">> = observer(function AboutScreen() {
const $bottomContainerInsets = useSafeAreaInsetsStyle(["bottom"])
export const AboutScreen: FC<BaseScreenProps<"About">> = observer(function AboutScreen() {
const $bottomContainerInsets = useSafeAreaInsetsStyle(["bottom"])

return (
<View style={$container}>
<View style={$topContainer}>
<Text
testID="welcome-heading"
style={$welcomeHeading}
tx="aboutScreen.readyForLaunch"
preset="heading"
/>
<Text tx="aboutScreen.postscript" preset="subheading" />
</View>
return (
<View style={$container}>
<View style={$topContainer}>
<Text
testID="welcome-heading"
style={$welcomeHeading}
tx="aboutScreen.readyForLaunch"
preset="heading"
/>
<Text tx="aboutScreen.postscript" preset="subheading" />
</View>

<View style={[$bottomContainer, $bottomContainerInsets]}>
<Text tx="aboutScreen.exciting" size="md" />
</View>
<View style={[$bottomContainer, $bottomContainerInsets]}>
<Text tx="aboutScreen.exciting" size="md" />
</View>
)
</View>
)
})

const $container: ViewStyle = {
Expand Down
160 changes: 96 additions & 64 deletions reactnative/RNPlayground/app/screens/PlaygroundScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,76 +1,108 @@
import React, { FC } from "react"
import * as React from "react"
Copy link
Contributor

Choose a reason for hiding this comment

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

This is fine but since we have allowSyntheticDefaultImports: true in tsconfig.json we don't need this change.

I actually found that this wildcard import is best practise (they exist in RN?) 😱 and shared another interesting guidelines below
facebook/react#18102
https://github.com/facebook/react-native-website/blob/main/STYLEGUIDE.md

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 useEffect then autocomplete behaves differently depending on the import type:

In case of import * as React ... autocomplete will update the function call to React.useEffect
In case of import React ... the autocomplete will add another import {useEffect} and not change the function call

The import * is definitely more verbose but at least for now it makes it easier for me to understand where the code comes from even if it is probably not the correct JS approach

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"
Copy link
Contributor

Choose a reason for hiding this comment

The 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 Right-To-Left. This is part of the I18nManager localization lib, which supports multiple languages including Arabic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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()
Copy link
Contributor

Choose a reason for hiding this comment

The 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
The main responsibility of stores is to move logic and state out of your components into a standalone testable unit that can be used in both frontend and backend JavaScript.


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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we make this a Switch Case operator?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 uiState object where each can be either there or undefined

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" />
Copy link
Contributor

Choose a reason for hiding this comment

The 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 tx=screen.stringName and text={String Literal}. Hope this helps

<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,
}
3 changes: 2 additions & 1 deletion reactnative/RNPlayground/app/screens/WelcomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import {
Text,
} from "../components"
import { isRTL } from "../i18n"
import { BaseScreenProps } from "../navigators"
import { colors, spacing } from "../theme"
import { useSafeAreaInsetsStyle } from "../utils/useSafeAreaInsetsStyle"

const welcomeLogo = require("../../assets/images/logo.png")
const welcomeFace = require("../../assets/images/welcome-face.png")


export const WelcomeScreen: FC<WelcomeScreenProps> = observer(function WelcomeScreen(
export const WelcomeScreen: FC<BaseScreenProps<"Welcome">> = observer(function WelcomeScreen(
) {

const $bottomContainerInsets = useSafeAreaInsetsStyle(["bottom"])
Expand Down