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

Migrate mobx to zustand #583

Open
wants to merge 42 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
ff637a0
Install zustand
enigma0Z Dec 10, 2024
aeca337
Convert root store to typescript & zustand
enigma0Z Dec 10, 2024
f9e09bd
Reference new zustand root store
enigma0Z Dec 10, 2024
b362a19
Fix test to work with hooks
enigma0Z Dec 10, 2024
90850f2
Add react testing library & jest-environment-jsdom
enigma0Z Dec 10, 2024
6c1891f
Make root store test more concise
enigma0Z Dec 10, 2024
5da37f1
Port DownloadStore/DownloadModel to zustand
enigma0Z Dec 10, 2024
89c8e64
Remove commented-out code
enigma0Z Dec 10, 2024
da8f22c
Remove commented-out code
enigma0Z Dec 10, 2024
6ab000e
Remove commented-out code
enigma0Z Dec 10, 2024
f9b41d1
Convert ServerModel and ServerStore to zustand
enigma0Z Dec 10, 2024
8f2ff18
Remove commented-out code
enigma0Z Dec 10, 2024
17e18fb
Fix broken tests/refs with refactor of mediastore
enigma0Z Dec 10, 2024
a4bc5ed
Convert setting store to TS
enigma0Z Dec 10, 2024
73c4790
Convert settingstore to use zustand
enigma0Z Dec 10, 2024
889dd5e
Remove usage of mobx observer
enigma0Z Dec 10, 2024
9a1c05a
Remove mobx action implementations
enigma0Z Dec 11, 2024
0e18e16
Remove mobx action implementations
enigma0Z Dec 11, 2024
bc387f4
Remove mobx action implementations
enigma0Z Dec 11, 2024
da56ce7
Add generic setter function for the stores that need it, convert all …
enigma0Z Dec 11, 2024
5cb35f0
Conver setting store to use setters
enigma0Z Dec 11, 2024
0ccf45c
Update rootStore setters
enigma0Z Dec 11, 2024
1e5bbea
Update mediastore to use setters
enigma0Z Dec 11, 2024
e8bd175
Reset all stores correctly since rootStore.reset() was refactored
enigma0Z Dec 11, 2024
b2a88e4
Fix render loop with using a set state on app initialization
enigma0Z Dec 11, 2024
163010e
Persist zustand storage, migrate mobx stores
enigma0Z Dec 11, 2024
23722c2
Fix refresh web view (which I broke trying to get tests working)
enigma0Z Dec 11, 2024
2efecc2
Remove usage of async trunk
enigma0Z Dec 11, 2024
00a4abf
Restore docstrings on root store
enigma0Z Dec 11, 2024
bf86687
Fix serializer tests
enigma0Z Dec 11, 2024
7ec4391
Fix warnings
enigma0Z Dec 11, 2024
d7825a6
Update tests to use setters
enigma0Z Dec 11, 2024
c0b1533
Merge remote-tracking branch 'upstream/master' into feature/migrate-m…
enigma0Z Dec 11, 2024
c8184ce
Remove mobx packages
enigma0Z Dec 11, 2024
32fad17
Linter fixups
enigma0Z Dec 11, 2024
8d8ed65
Clear eslint whitespace errors
enigma0Z Dec 11, 2024
789b378
Ignore eslint import error (I suspect an outdated version of react na…
enigma0Z Dec 11, 2024
209a2ac
Tag the silly eslint import error with the issue it's tracked under.
enigma0Z Dec 12, 2024
2bc2bba
Merge branch 'master' into feature/migrate-mobx-to-zustand
enigma0Z Dec 13, 2024
3741378
Merge branch 'master' into feature/migrate-mobx-to-zustand
enigma0Z Dec 19, 2024
9d7f219
Merge branch 'master' into feature/migrate-mobx-to-zustand
enigma0Z Jan 1, 2025
386c35c
Merge branch 'master' into feature/migrate-mobx-to-zustand
enigma0Z Jan 3, 2025
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
25 changes: 12 additions & 13 deletions App.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import * as Font from 'expo-font';
import * as ScreenOrientation from 'expo-screen-orientation';
import * as SplashScreen from 'expo-splash-screen';
import { StatusBar } from 'expo-status-bar';
import { observer } from 'mobx-react-lite';
import { AsyncTrunk } from 'mobx-sync-lite';
enigma0Z marked this conversation as resolved.
Show resolved Hide resolved
import PropTypes from 'prop-types';
import React, { useContext, useEffect, useState } from 'react';
Expand All @@ -34,12 +33,12 @@ import StaticScriptLoader from './utils/StaticScriptLoader';
// Import i18n configuration
import './i18n';

const App = observer(({ skipLoadingScreen }) => {
const App = ({ skipLoadingScreen }) => {
const [ isSplashReady, setIsSplashReady ] = useState(false);
const { rootStore } = useStores();
const { rootStore, downloadStore, settingStore } = useStores();
const { theme } = useContext(ThemeContext);

rootStore.settingStore.systemThemeId = useColorScheme();
settingStore.systemThemeId = useColorScheme();

SplashScreen.preventAutoHideAsync();

Expand Down Expand Up @@ -87,16 +86,16 @@ const App = observer(({ skipLoadingScreen }) => {
}, []);

useEffect(() => {
console.info('rotation lock setting changed!', rootStore.settingStore.isRotationLockEnabled);
if (rootStore.settingStore.isRotationLockEnabled) {
console.info('rotation lock setting changed!', settingStore.isRotationLockEnabled);
if (settingStore.isRotationLockEnabled) {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
} else {
ScreenOrientation.unlockAsync();
}
}, [ rootStore.settingStore.isRotationLockEnabled ]);
}, [ settingStore.isRotationLockEnabled ]);

const updateScreenOrientation = async () => {
if (rootStore.settingStore.isRotationLockEnabled) {
if (settingStore.isRotationLockEnabled) {
if (rootStore.isFullscreen) {
// Lock to landscape orientation
// For some reason video apps on iPhone use LANDSCAPE_RIGHT ¯\_(ツ)_/¯
Expand Down Expand Up @@ -163,34 +162,34 @@ const App = observer(({ skipLoadingScreen }) => {
});
};

rootStore.downloadStore.downloads
downloadStore.downloads
.forEach(download => {
if (!download.isComplete && !download.isDownloading) {
downloadFile(download);
}
});
}, [ rootStore.deviceId, rootStore.downloadStore.downloads.size ]);
}, [ rootStore.deviceId, downloadStore.downloads.size ]);

if (!(isSplashReady && rootStore.storeLoaded) && !skipLoadingScreen) {
return null;
}

return (
<SafeAreaProvider>
<ThemeProvider theme={rootStore.settingStore.theme.Elements}>
<ThemeProvider theme={settingStore.getTheme().Elements}>
<ThemeSwitcher />
<StatusBar
style='light'
backgroundColor={theme.colors.grey0}
hidden={rootStore.isFullscreen}
/>
<NavigationContainer theme={rootStore.settingStore.theme.Navigation}>
<NavigationContainer theme={settingStore.getTheme().Navigation}>
<RootNavigator />
</NavigationContainer>
</ThemeProvider>
</SafeAreaProvider>
);
});
};

App.propTypes = {
skipLoadingScreen: PropTypes.bool
Expand Down
65 changes: 65 additions & 0 deletions __mocks__/zustand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// __mocks__/zustand.ts
import { act } from '@testing-library/react'
import type * as ZustandExportedTypes from 'zustand'
export * from 'zustand'

const { create: actualCreate, createStore: actualCreateStore } =
jest.requireActual<typeof ZustandExportedTypes>('zustand')

// a variable to hold reset functions for all stores declared in the app
export const storeResetFns = new Set<() => void>()

const createUncurried = <T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
const store = actualCreate(stateCreator)
const initialState = store.getInitialState()
storeResetFns.add(() => {
store.setState(initialState, true)
})
return store
}

// when creating a store, we get its initial state, create a reset function and add it in the set
export const create = (<T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
console.log('zustand create mock')

// to support curried version of create
return typeof stateCreator === 'function'
? createUncurried(stateCreator)
: createUncurried
}) as typeof ZustandExportedTypes.create

const createStoreUncurried = <T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
const store = actualCreateStore(stateCreator)
const initialState = store.getInitialState()
storeResetFns.add(() => {
store.setState(initialState, true)
})
return store
}

// when creating a store, we get its initial state, create a reset function and add it in the set
export const createStore = (<T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
console.log('zustand createStore mock')

// to support curried version of createStore
return typeof stateCreator === 'function'
? createStoreUncurried(stateCreator)
: createStoreUncurried
}) as typeof ZustandExportedTypes.createStore

// reset all stores after each test run
afterEach(() => {
act(() => {
storeResetFns.forEach((resetFn) => {
resetFn()
})
})
})
37 changes: 18 additions & 19 deletions components/AudioPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
*/

import { Audio, InterruptionModeAndroid, InterruptionModeIOS } from 'expo-av';
import { observer } from 'mobx-react-lite';
import React, { useEffect, useState } from 'react';

import MediaTypes from '../constants/MediaTypes';
import { useStores } from '../hooks/useStores';
import { msToTicks } from '../utils/Time';

const AudioPlayer = observer(() => {
const { rootStore } = useStores();
const AudioPlayer = () => {
const { mediaStore } = useStores();

const [ player, setPlayer ] = useState();

Expand Down Expand Up @@ -57,48 +56,48 @@ const AudioPlayer = observer(() => {
didJustFinish === undefined ||
isPlaying === undefined ||
positionMs === undefined ||
rootStore.mediaStore.isFinished
mediaStore.isFinished
) {
return;
}
rootStore.mediaStore.isFinished = didJustFinish;
rootStore.mediaStore.isPlaying = isPlaying;
rootStore.mediaStore.positionTicks = msToTicks(positionMs);
mediaStore.isFinished = didJustFinish;
mediaStore.isPlaying = isPlaying;
mediaStore.positionTicks = msToTicks(positionMs);
});
setPlayer(sound);
}
};

if (rootStore.mediaStore.type === MediaTypes.Audio) {
if (mediaStore.type === MediaTypes.Audio) {
createPlayer({
uri: rootStore.mediaStore.uri,
positionMillis: rootStore.mediaStore.positionMillis
uri: mediaStore.uri,
positionMillis: mediaStore.positionMillis
});
}
}, [ rootStore.mediaStore.type, rootStore.mediaStore.uri ]);
}, [ mediaStore.type, mediaStore.uri ]);

// Update the play/pause state when the store indicates it should
useEffect(() => {
if (rootStore.mediaStore.type === MediaTypes.Audio && rootStore.mediaStore.shouldPlayPause) {
if (rootStore.mediaStore.isPlaying) {
if (mediaStore.type === MediaTypes.Audio && mediaStore.shouldPlayPause) {
if (mediaStore.isPlaying) {
player?.pauseAsync();
} else {
player?.playAsync();
}
rootStore.mediaStore.shouldPlayPause = false;
mediaStore.shouldPlayPause = false;
}
}, [ rootStore.mediaStore.shouldPlayPause ]);
}, [ mediaStore.shouldPlayPause ]);

// Stop the player when the store indicates it should stop playback
useEffect(() => {
if (rootStore.mediaStore.type === MediaTypes.Audio && rootStore.mediaStore.shouldStop) {
if (mediaStore.type === MediaTypes.Audio && mediaStore.shouldStop) {
player?.stopAsync();
player?.unloadAsync();
rootStore.mediaStore.shouldStop = false;
mediaStore.shouldStop = false;
}
}, [ rootStore.mediaStore.shouldStop ]);
}, [ mediaStore.shouldStop ]);

return <></>;
});
};

export default AudioPlayer;
Loading
Loading