Refactor MassiveSnack

Instead of using a context for the whole app
use DeviceEventEmitter with root state.
This will probably improve performance,
since I think the react context was
re-rendering the entire DOM tree.
This commit is contained in:
Brandon Presley 2022-11-01 15:55:37 +13:00
parent ace327ecad
commit 49b5eb48c6
10 changed files with 109 additions and 136 deletions

34
App.tsx
View File

@ -4,19 +4,20 @@ import {
NavigationContainer,
} from '@react-navigation/native'
import {useEffect, useMemo, useState} from 'react'
import {useColorScheme} from 'react-native'
import {DeviceEventEmitter, useColorScheme} from 'react-native'
import {
DarkTheme as PaperDarkTheme,
DefaultTheme as PaperDefaultTheme,
Provider as PaperProvider,
Snackbar,
} from 'react-native-paper'
import MaterialIcon from 'react-native-vector-icons/MaterialIcons'
import {lightColors} from './colors'
import {AppDataSource} from './data-source'
import {settingsRepo} from './db'
import MassiveSnack from './MassiveSnack'
import Routes from './Routes'
import Settings from './settings'
import {TOAST} from './toast'
import {defaultSettings, SettingsContext} from './use-settings'
export const CombinedDefaultTheme = {
@ -48,6 +49,7 @@ const App = () => {
? CombinedDarkTheme.colors.primary
: CombinedDefaultTheme.colors.primary,
})
const [snackbar, setSnackbar] = useState('')
useEffect(() => {
AppDataSource.initialize().then(async () => {
@ -56,6 +58,10 @@ const App = () => {
setSettings(gotSettings)
setInitialized(true)
})
DeviceEventEmitter.addListener(TOAST, ({value}: {value: string}) => {
console.log(`${Routes.name}.toast:`, {value})
setSnackbar(value)
})
}, [])
const theme = useMemo(() => {
@ -87,14 +93,24 @@ const App = () => {
theme={theme}
settings={{icon: props => <MaterialIcon {...props} />}}>
<NavigationContainer theme={theme}>
<MassiveSnack>
{initialized && (
<SettingsContext.Provider value={settingsContext}>
<Routes />
</SettingsContext.Provider>
)}
</MassiveSnack>
{initialized && (
<SettingsContext.Provider value={settingsContext}>
<Routes />
</SettingsContext.Provider>
)}
</NavigationContainer>
<Snackbar
duration={3000}
onDismiss={() => setSnackbar('')}
visible={!!snackbar}
action={{
label: 'Close',
onPress: () => setSnackbar(''),
color: theme.colors.primary,
}}>
{snackbar}
</Snackbar>
</PaperProvider>
)
}

View File

@ -8,8 +8,8 @@ import {AppDataSource} from './data-source'
import {planRepo} from './db'
import {DrawerParamList} from './drawer-param-list'
import GymSet from './gym-set'
import {useSnackbar} from './MassiveSnack'
import {Plan} from './plan'
import {toast} from './toast'
import useDark from './use-dark'
import {write} from './write'
@ -20,7 +20,6 @@ const setRepo = AppDataSource.manager.getRepository(GymSet)
export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
const [showMenu, setShowMenu] = useState(false)
const [showRemove, setShowRemove] = useState(false)
const {toast} = useSnackbar()
const {reset} = useNavigation<NavigationProp<DrawerParamList>>()
const dark = useDark()
@ -65,7 +64,7 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
console.log(`${DrawerMenu.name}.uploadSets:`, file.length)
const lines = file.split('\n')
console.log(lines[0])
if (!setFields.includes(lines[0])) return toast('Invalid csv.', 3000)
if (!setFields.includes(lines[0])) return toast('Invalid csv.')
const values = lines
.slice(1)
.filter(line => line)
@ -92,21 +91,22 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
sets: +sets,
minutes: +minutes,
seconds: +seconds,
image: '',
}
return set
})
console.log(`${DrawerMenu.name}.uploadSets:`, {values})
await setRepo.insert(values)
toast('Data imported.', 3000)
toast('Data imported.')
reset({index: 0, routes: [{name}]})
}, [reset, name, toast])
}, [reset, name])
const uploadPlans = useCallback(async () => {
const result = await DocumentPicker.pickSingle()
const file = await FileSystem.readFile(result.uri)
console.log(`${DrawerMenu.name}.uploadPlans:`, file.length)
const lines = file.split('\n')
if (lines[0] != planFields) return toast('Invalid csv.', 3000)
if (lines[0] !== planFields) return toast('Invalid csv.')
const values = file
.split('\n')
.slice(1)
@ -122,8 +122,8 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
return plan
})
await planRepo.insert(values)
toast('Data imported.', 3000)
}, [toast])
toast('Data imported.')
}, [])
const upload = useCallback(async () => {
setShowMenu(false)
@ -137,9 +137,9 @@ export default function DrawerMenu({name}: {name: keyof DrawerParamList}) {
setShowRemove(false)
if (name === 'Home') await setRepo.delete({})
else if (name === 'Plans') await planRepo.delete({})
toast('All data has been deleted.', 4000)
toast('All data has been deleted.')
reset({index: 0, routes: [{name}]})
}, [reset, name, toast])
}, [reset, name])
if (name === 'Home' || name === 'Plans')
return (

View File

@ -2,19 +2,18 @@ import {RouteProp, useNavigation, useRoute} from '@react-navigation/native'
import {useCallback} from 'react'
import {NativeModules, View} from 'react-native'
import {PADDING} from './constants'
import {getNow, setRepo} from './db'
import {setRepo} from './db'
import GymSet from './gym-set'
import {HomePageParams} from './home-page-params'
import {useSnackbar} from './MassiveSnack'
import SetForm from './SetForm'
import StackHeader from './StackHeader'
import {toast} from './toast'
import {useSettings} from './use-settings'
export default function EditSet() {
const {params} = useRoute<RouteProp<HomePageParams, 'EditSet'>>()
const {set} = params
const navigation = useNavigation()
const {toast} = useSnackbar()
const {settings} = useSettings()
const startTimer = useCallback(
@ -35,9 +34,6 @@ export default function EditSet() {
const add = useCallback(
async (value: GymSet) => {
startTimer(value.name)
const [{now}] = await getNow()
value.created = now
value.hidden = false
console.log(`${EditSet.name}.add`, {set: value})
const result = await setRepo.save(value)
console.log({result})
@ -46,9 +42,9 @@ export default function EditSet() {
value.weight > set.weight ||
(value.reps > set.reps && value.weight === set.weight)
)
toast("Great work King! That's a new record.", 3000)
toast("Great work King! That's a new record.")
},
[startTimer, set, toast, settings],
[startTimer, set, settings],
)
const save = useCallback(

View File

@ -7,8 +7,8 @@ import ConfirmDialog from './ConfirmDialog'
import {MARGIN, PADDING} from './constants'
import {getNow, planRepo, setRepo} from './db'
import MassiveInput from './MassiveInput'
import {useSnackbar} from './MassiveSnack'
import StackHeader from './StackHeader'
import {toast} from './toast'
import {useSettings} from './use-settings'
import {WorkoutsPageParams} from './WorkoutsPage'
@ -26,7 +26,6 @@ export default function EditWorkout() {
params.value.seconds?.toString() ?? '30',
)
const [sets, setSets] = useState(params.value.sets?.toString() ?? '3')
const {toast} = useSnackbar()
const navigation = useNavigation()
const setsRef = useRef<TextInput>(null)
const stepsRef = useRef<TextInput>(null)
@ -94,13 +93,13 @@ export default function EditWorkout() {
const handleName = (value: string) => {
setName(value.replace(/,|'/g, ''))
if (value.match(/,|'/))
toast('Commas and single quotes would break CSV exports', 6000)
toast('Commas and single quotes would break CSV exports')
}
const handleSteps = (value: string) => {
setSteps(value.replace(/,|'/g, ''))
if (value.match(/,|'/))
toast('Commas and single quotes would break CSV exports', 6000)
toast('Commas and single quotes would break CSV exports')
}
const submitName = () => {

View File

@ -1,49 +0,0 @@
import {createContext, useContext, useState} from 'react'
import {Snackbar} from 'react-native-paper'
import {CombinedDarkTheme, CombinedDefaultTheme} from './App'
import useDark from './use-dark'
export const SnackbarContext = createContext<{
toast: (value: string, timeout: number) => void
}>({toast: () => null})
export const useSnackbar = () => {
return useContext(SnackbarContext)
}
export default function MassiveSnack({
children,
}: {
children?: JSX.Element[] | JSX.Element
}) {
const [snackbar, setSnackbar] = useState('')
const [timeoutId, setTimeoutId] = useState(0)
const dark = useDark()
const toast = (value: string, timeout: number) => {
setSnackbar(value)
clearTimeout(timeoutId)
const id = setTimeout(() => setSnackbar(''), timeout)
setTimeoutId(id)
}
return (
<>
<SnackbarContext.Provider value={{toast}}>
{children}
</SnackbarContext.Provider>
<Snackbar
onDismiss={() => setSnackbar('')}
visible={!!snackbar}
action={{
label: 'Close',
onPress: () => setSnackbar(''),
color: dark
? CombinedDarkTheme.colors.background
: CombinedDefaultTheme.colors.background,
}}>
{snackbar}
</Snackbar>
</>
)
}

View File

@ -1,6 +1,7 @@
import {createDrawerNavigator} from '@react-navigation/drawer'
import {useMemo} from 'react'
import {IconButton} from 'react-native-paper'
import {useEffect, useMemo, useState} from 'react'
import {DeviceEventEmitter} from 'react-native'
import {IconButton, Snackbar, useTheme} from 'react-native-paper'
import BestPage from './BestPage'
import {DrawerParamList} from './drawer-param-list'
import HomePage from './HomePage'

View File

@ -4,10 +4,10 @@ import DocumentPicker from 'react-native-document-picker'
import {Button, Card, TouchableRipple} from 'react-native-paper'
import ConfirmDialog from './ConfirmDialog'
import {MARGIN} from './constants'
import {setRepo} from './db'
import {getNow, setRepo} from './db'
import GymSet from './gym-set'
import MassiveInput from './MassiveInput'
import {useSnackbar} from './MassiveSnack'
import {toast} from './toast'
import {useSettings} from './use-settings'
export default function SetForm({
@ -28,7 +28,6 @@ export default function SetForm({
end: set.reps.toString().length,
})
const [removeImage, setRemoveImage] = useState(false)
const {toast} = useSnackbar()
const {settings} = useSettings()
const weightRef = useRef<TextInput>(null)
const repsRef = useRef<TextInput>(null)
@ -42,8 +41,10 @@ export default function SetForm({
image = await setRepo.findOne({where: {name}}).then(s => s?.image)
console.log(`${SetForm.name}.handleSubmit:`, {image})
const [{now}] = await getNow()
save({
name,
created: now,
reps: Number(reps),
weight: Number(weight),
id: set.id,
@ -59,13 +60,13 @@ export default function SetForm({
const handleName = (value: string) => {
setName(value.replace(/,|'/g, ''))
if (value.match(/,|'/))
toast('Commas and single quotes would break CSV exports', 6000)
toast('Commas and single quotes would break CSV exports')
}
const handleUnit = (value: string) => {
setUnit(value.replace(/,|'/g, ''))
if (value.match(/,|'/))
toast('Commas and single quotes would break CSV exports', 6000)
toast('Commas and single quotes would break CSV exports')
}
const changeImage = useCallback(async () => {

View File

@ -1,7 +1,7 @@
import {Picker} from '@react-native-picker/picker'
import {useFocusEffect} from '@react-navigation/native'
import {useCallback, useEffect, useMemo, useState} from 'react'
import {NativeModules, ScrollView} from 'react-native'
import {DeviceEventEmitter, NativeModules, ScrollView} from 'react-native'
import DocumentPicker from 'react-native-document-picker'
import {Button} from 'react-native-paper'
import {darkColors, lightColors} from './colors'
@ -10,10 +10,10 @@ import {MARGIN} from './constants'
import {settingsRepo} from './db'
import DrawerHeader from './DrawerHeader'
import Input from './input'
import {useSnackbar} from './MassiveSnack'
import Page from './Page'
import Settings from './settings'
import Switch from './Switch'
import {toast} from './toast'
import {useSettings} from './use-settings'
export default function SettingsPage() {
@ -21,7 +21,6 @@ export default function SettingsPage() {
const [ignoring, setIgnoring] = useState(false)
const [term, setTerm] = useState('')
const {settings, setSettings} = useSettings()
const {toast} = useSnackbar()
useEffect(() => {
console.log(`${SettingsPage.name}.useEffect:`, {settings})
@ -43,21 +42,25 @@ export default function SettingsPage() {
const changeAlarmEnabled = useCallback(
(enabled: boolean) => {
if (enabled) toast('Timers will now run after each set.', 4000)
else toast('Stopped timers running after each set.', 4000)
if (enabled)
DeviceEventEmitter.emit('toast', {
value: 'Timers will now run after each set',
timeout: 4000,
})
else toast('Stopped timers running after each set.')
if (enabled && !ignoring) setBattery(true)
update(enabled, 'alarm')
},
[setBattery, ignoring, toast, update],
[setBattery, ignoring, update],
)
const changeVibrate = useCallback(
(enabled: boolean) => {
if (enabled) toast('When a timer completes, vibrate your phone.', 4000)
else toast('Stop vibrating at the end of timers.', 4000)
if (enabled) toast('When a timer completes, vibrate your phone.')
else toast('Stop vibrating at the end of timers.')
update(enabled, 'vibrate')
},
[toast, update],
[update],
)
const changeSound = useCallback(async () => {
@ -68,70 +71,70 @@ export default function SettingsPage() {
if (!fileCopyUri) return
settingsRepo.update({}, {sound: fileCopyUri})
setSettings({...settings, sound: fileCopyUri})
toast('This song will now play after rest timers complete.', 4000)
}, [toast, setSettings, settings])
toast('This song will now play after rest timers complete.')
}, [setSettings, settings])
const changeNotify = useCallback(
(enabled: boolean) => {
update(enabled, 'notify')
if (enabled) toast('Show when a set is a new record.', 4000)
else toast('Stopped showing notifications for new records.', 4000)
if (enabled) toast('Show when a set is a new record.')
else toast('Stopped showing notifications for new records.')
},
[toast, update],
[update],
)
const changeImages = useCallback(
(enabled: boolean) => {
update(enabled, 'images')
if (enabled) toast('Show images for sets.', 4000)
else toast('Stopped showing images for sets.', 4000)
if (enabled) toast('Show images for sets.')
else toast('Stopped showing images for sets.')
},
[toast, update],
[update],
)
const changeUnit = useCallback(
(enabled: boolean) => {
update(enabled, 'showUnit')
if (enabled) toast('Show option to select unit for sets.', 4000)
else toast('Hid unit option for sets.', 4000)
if (enabled) toast('Show option to select unit for sets.')
else toast('Hid unit option for sets.')
},
[toast, update],
[update],
)
const changeSteps = useCallback(
(enabled: boolean) => {
update(enabled, 'steps')
if (enabled) toast('Show steps for a workout.', 4000)
else toast('Stopped showing steps for workouts.', 4000)
if (enabled) toast('Show steps for a workout.')
else toast('Stopped showing steps for workouts.')
},
[toast, update],
[update],
)
const changeShowDate = useCallback(
(enabled: boolean) => {
update(enabled, 'showDate')
if (enabled) toast('Show date for sets by default.', 4000)
else toast('Stopped showing date for sets by default.', 4000)
if (enabled) toast('Show date for sets by default.')
else toast('Stopped showing date for sets by default.')
},
[toast, update],
[update],
)
const changeShowSets = useCallback(
(enabled: boolean) => {
update(enabled, 'showSets')
if (enabled) toast('Show target sets for workouts.', 4000)
else toast('Stopped showing target sets for workouts.', 4000)
if (enabled) toast('Show target sets for workouts.')
else toast('Stopped showing target sets for workouts.')
},
[toast, update],
[update],
)
const changeNoSound = useCallback(
(enabled: boolean) => {
update(enabled, 'noSound')
if (enabled) toast('Disable sound on rest timer alarms.', 4000)
else toast('Enabled sound for rest timer alarms.', 4000)
if (enabled) toast('Disable sound on rest timer alarms.')
else toast('Enabled sound for rest timer alarms.')
},
[toast, update],
[update],
)
const switches: Input<boolean>[] = [
@ -183,10 +186,13 @@ export default function SettingsPage() {
)
}, [term, settings.color, changeTheme, settings.theme])
const changeColor = useCallback((value: string) => {
setSettings({...settings, color: value})
settingsRepo.update({}, {color: value})
}, [])
const changeColor = useCallback(
(value: string) => {
setSettings({...settings, color: value})
settingsRepo.update({}, {color: value})
},
[setSettings, settings],
)
return (
<>

View File

@ -10,11 +10,11 @@ import {AppDataSource} from './data-source'
import {getNow, setRepo} from './db'
import GymSet from './gym-set'
import MassiveInput from './MassiveInput'
import {useSnackbar} from './MassiveSnack'
import {PlanPageParams} from './plan-page-params'
import SetForm from './SetForm'
import StackHeader from './StackHeader'
import StartPlanItem from './StartPlanItem'
import {toast} from './toast'
import {useSettings} from './use-settings'
export default function StartPlan() {
@ -23,7 +23,6 @@ export default function StartPlan() {
const [reps, setReps] = useState('')
const [weight, setWeight] = useState('')
const [unit, setUnit] = useState<string>('kg')
const {toast} = useSnackbar()
const [minutes, setMinutes] = useState(3)
const [seconds, setSeconds] = useState(30)
const [best, setBest] = useState<GymSet>()
@ -109,9 +108,9 @@ export default function StartPlan() {
settings.notify &&
(+weight > best.weight || (+reps > best.reps && +weight === best.weight))
)
toast("Great work King! That's a new record.", 5000)
else if (settings.alarm) toast('Resting...', 3000)
else toast('Added set', 3000)
toast("Great work King! That's a new record.")
else if (settings.alarm) toast('Resting...')
else toast('Added set')
if (!settings.alarm) return
const milliseconds = Number(minutes) * 60 * 1000 + Number(seconds) * 1000
const {vibrate, sound, noSound} = settings
@ -119,14 +118,11 @@ export default function StartPlan() {
NativeModules.AlarmModule.timer(...args)
}
const handleUnit = useCallback(
(value: string) => {
setUnit(value.replace(/,|'/g, ''))
if (value.match(/,|'/))
toast('Commas and single quotes would break CSV exports', 6000)
},
[toast],
)
const handleUnit = useCallback((value: string) => {
setUnit(value.replace(/,|'/g, ''))
if (value.match(/,|'/))
toast('Commas and single quotes would break CSV exports')
}, [])
return (
<>

7
toast.ts Normal file
View File

@ -0,0 +1,7 @@
import {DeviceEventEmitter} from 'react-native'
export const TOAST = 'toast'
export function toast(value: string) {
DeviceEventEmitter.emit(TOAST, {value})
}