switch to jotai

This commit is contained in:
austinried 2021-06-28 11:12:19 +09:00
parent 17f5639aef
commit 4606586102
13 changed files with 110 additions and 151 deletions

View File

@ -1,17 +1,17 @@
import React from 'react'; import React from 'react';
import { NavigationContainer } from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import { RecoilRoot } from 'recoil';
import SplashPage from './src/components/SplashPage'; import SplashPage from './src/components/SplashPage';
import RootNavigator from './src/components/navigation/RootNavigator'; import RootNavigator from './src/components/navigation/RootNavigator';
import { Provider } from 'jotai';
const App = () => ( const App = () => (
<RecoilRoot> <Provider>
<SplashPage> <SplashPage>
<NavigationContainer> <NavigationContainer>
<RootNavigator /> <RootNavigator />
</NavigationContainer> </NavigationContainer>
</SplashPage> </SplashPage>
</RecoilRoot> </Provider>
); );
export default App; export default App;

View File

@ -1,11 +1,6 @@
import 'react-native-gesture-handler'; import 'react-native-gesture-handler';
import 'react-native-get-random-values'; import 'react-native-get-random-values';
import { AppRegistry, LogBox } from 'react-native';
// ignore recoil's timer warning on android:
// https://github.com/facebookexperimental/Recoil/issues/1030
LogBox.ignoreLogs(["timer"]);
import { enableScreens } from 'react-native-screens'; import { enableScreens } from 'react-native-screens';
enableScreens(); enableScreens();

86
package-lock.json generated
View File

@ -14,6 +14,7 @@
"@react-navigation/material-top-tabs": "^5.3.15", "@react-navigation/material-top-tabs": "^5.3.15",
"@react-navigation/native": "^5.9.4", "@react-navigation/native": "^5.9.4",
"@react-navigation/stack": "^5.14.5", "@react-navigation/stack": "^5.14.5",
"jotai": "^1.1.0",
"md5": "^2.3.0", "md5": "^2.3.0",
"react": "17.0.1", "react": "17.0.1",
"react-native": "0.64.1", "react-native": "0.64.1",
@ -27,7 +28,6 @@
"react-native-screens": "^3.4.0", "react-native-screens": "^3.4.0",
"react-native-tab-view": "^2.16.0", "react-native-tab-view": "^2.16.0",
"react-native-track-player": "^1.2.7", "react-native-track-player": "^1.2.7",
"recoil": "^0.3.1",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"xmldom": "^0.5.0" "xmldom": "^0.5.0"
}, },
@ -6116,11 +6116,6 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"node_modules/hamt_plus": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
"integrity": "sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE="
},
"node_modules/has": { "node_modules/has": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@ -8461,6 +8456,47 @@
"@sideway/pinpoint": "^2.0.0" "@sideway/pinpoint": "^2.0.0"
} }
}, },
"node_modules/jotai": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jotai/-/jotai-1.1.0.tgz",
"integrity": "sha512-+c/bMxS/c1LgsOYVfevOYLAu+WEgn85TJhmBoNSNHjMV+mTKZY0ACSyCrX1yz8KialK4+Ggm6d7Ek3sTXy8UgA==",
"engines": {
"node": ">=12"
},
"peerDependencies": {
"@urql/core": "*",
"immer": "*",
"optics-ts": "*",
"react": ">=16.8",
"react-query": "*",
"valtio": "*",
"wonka": "*",
"xstate": "*"
},
"peerDependenciesMeta": {
"@urql/core": {
"optional": true
},
"immer": {
"optional": true
},
"optics-ts": {
"optional": true
},
"react-query": {
"optional": true
},
"valtio": {
"optional": true
},
"wonka": {
"optional": true
},
"xstate": {
"optional": true
}
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -10772,25 +10808,6 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/recoil": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/recoil/-/recoil-0.3.1.tgz",
"integrity": "sha512-KNA3DRqgxX4rRC8E7fc6uIw7BACmMPuraIYy+ejhE8tsw7w32CetMm8w7AMZa34wzanKKkev3vl3H7Z4s0QSiA==",
"dependencies": {
"hamt_plus": "1.0.2"
},
"peerDependencies": {
"react": ">=16.13.1"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/regenerate": { "node_modules/regenerate": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@ -17936,11 +17953,6 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"hamt_plus": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
"integrity": "sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE="
},
"has": { "has": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@ -19673,6 +19685,12 @@
"@sideway/pinpoint": "^2.0.0" "@sideway/pinpoint": "^2.0.0"
} }
}, },
"jotai": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jotai/-/jotai-1.1.0.tgz",
"integrity": "sha512-+c/bMxS/c1LgsOYVfevOYLAu+WEgn85TJhmBoNSNHjMV+mTKZY0ACSyCrX1yz8KialK4+Ggm6d7Ek3sTXy8UgA==",
"requires": {}
},
"js-tokens": { "js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -21513,14 +21531,6 @@
"resolve": "^1.1.6" "resolve": "^1.1.6"
} }
}, },
"recoil": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/recoil/-/recoil-0.3.1.tgz",
"integrity": "sha512-KNA3DRqgxX4rRC8E7fc6uIw7BACmMPuraIYy+ejhE8tsw7w32CetMm8w7AMZa34wzanKKkev3vl3H7Z4s0QSiA==",
"requires": {
"hamt_plus": "1.0.2"
}
},
"regenerate": { "regenerate": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",

View File

@ -16,6 +16,7 @@
"@react-navigation/material-top-tabs": "^5.3.15", "@react-navigation/material-top-tabs": "^5.3.15",
"@react-navigation/native": "^5.9.4", "@react-navigation/native": "^5.9.4",
"@react-navigation/stack": "^5.14.5", "@react-navigation/stack": "^5.14.5",
"jotai": "^1.1.0",
"md5": "^2.3.0", "md5": "^2.3.0",
"react": "17.0.1", "react": "17.0.1",
"react-native": "0.64.1", "react-native": "0.64.1",
@ -29,7 +30,6 @@
"react-native-screens": "^3.4.0", "react-native-screens": "^3.4.0",
"react-native-tab-view": "^2.16.0", "react-native-tab-view": "^2.16.0",
"react-native-track-player": "^1.2.7", "react-native-track-player": "^1.2.7",
"recoil": "^0.3.1",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"xmldom": "^0.5.0" "xmldom": "^0.5.0"
}, },

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { Button, FlatList, Text, View } from 'react-native'; import { FlatList, Text, View } from 'react-native';
import { useRecoilValue, useResetRecoilState } from 'recoil'; import { useAtomValue } from 'jotai/utils';
import { Artist } from '../models/music'; import { Artist } from '../models/music';
import { artistsState } from '../state/music'; import { artistsAtom } from '../state/music';
const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => ( const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
<View> <View>
@ -15,7 +15,7 @@ const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
); );
const List = () => { const List = () => {
const artists = useRecoilValue(artistsState); const artists = useAtomValue(artistsAtom);
const renderItem: React.FC<{ item: Artist }> = ({ item }) => ( const renderItem: React.FC<{ item: Artist }> = ({ item }) => (
<ArtistItem item={item} /> <ArtistItem item={item} />
@ -30,24 +30,10 @@ const List = () => {
); );
} }
const ListPlusControls = () => {
const resetArtists = useResetRecoilState(artistsState);
return (
<View>
<Button
title='Reset to default'
onPress={resetArtists}
/>
<List />
</View>
);
}
const ArtistsList = () => ( const ArtistsList = () => (
<View> <View>
<React.Suspense fallback={<Text>Loading...</Text>}> <React.Suspense fallback={<Text>Loading...</Text>}>
<ListPlusControls /> <List />
</React.Suspense> </React.Suspense>
</View> </View>
) )

View File

@ -1,10 +1,10 @@
import { useNavigation } from '@react-navigation/core'; import { useNavigation } from '@react-navigation/core';
import { useAtom } from 'jotai';
import md5 from 'md5'; import md5 from 'md5';
import React from 'react'; import React from 'react';
import { Button, Text, View } from 'react-native'; import { Button, Text, View } from 'react-native';
import { useRecoilState } from 'recoil';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { appSettingsState } from '../state/settings'; import { appSettingsAtom } from '../state/settings';
const TestControls = () => { const TestControls = () => {
const navigation = useNavigation(); const navigation = useNavigation();
@ -19,7 +19,7 @@ const TestControls = () => {
} }
const ServerSettingsView = () => { const ServerSettingsView = () => {
const [appSettings, setAppSettings] = useRecoilState(appSettingsState); const [appSettings, setAppSettings] = useAtom(appSettingsAtom);
const bootstrapServer = () => { const bootstrapServer = () => {
if (appSettings.servers.length !== 0) { if (appSettings.servers.length !== 0) {

View File

@ -1,10 +1,10 @@
import { useAtomValue } from 'jotai/utils';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { FlatList, Text, View } from 'react-native'; import { FlatList, Text, View } from 'react-native';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import LinearGradient from 'react-native-linear-gradient'; import LinearGradient from 'react-native-linear-gradient';
import { useRecoilValue } from 'recoil';
import { Album } from '../../models/music'; import { Album } from '../../models/music';
import { albumsState, albumsUpdatingState, useUpdateAlbums } from '../../state/music'; import { albumsAtom, albumsUpdatingAtom, useUpdateAlbums } from '../../state/music';
import colors from '../../styles/colors'; import colors from '../../styles/colors';
import textStyles from '../../styles/text'; import textStyles from '../../styles/text';
import TopTabContainer from '../common/TopTabContainer'; import TopTabContainer from '../common/TopTabContainer';
@ -89,8 +89,8 @@ const AlbumListRenderItem: React.FC<{ item: Album }> = ({ item }) => (
); );
const AlbumsList = () => { const AlbumsList = () => {
const albums = useRecoilValue(albumsState); const albums = useAtomValue(albumsAtom);
const updating = useRecoilValue(albumsUpdatingState); const updating = useAtomValue(albumsUpdatingAtom);
const updateAlbums = useUpdateAlbums(); const updateAlbums = useUpdateAlbums();
useEffect(() => { useEffect(() => {

View File

@ -1,8 +1,8 @@
import { useAtomValue } from 'jotai/utils';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { FlatList, Image, Text, View } from 'react-native'; import { FlatList, Image, Text, View } from 'react-native';
import { useRecoilValue } from 'recoil';
import { Artist } from '../../models/music'; import { Artist } from '../../models/music';
import { artistsState, artistsUpdatingState, useUpdateArtists } from '../../state/music'; import { artistsAtom, artistsUpdatingAtom, useUpdateArtists } from '../../state/music';
import textStyles from '../../styles/text'; import textStyles from '../../styles/text';
import TopTabContainer from '../common/TopTabContainer'; import TopTabContainer from '../common/TopTabContainer';
@ -28,8 +28,8 @@ const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
); );
const ArtistsList = () => { const ArtistsList = () => {
const artists = useRecoilValue(artistsState); const artists = useAtomValue(artistsAtom);
const updating = useRecoilValue(artistsUpdatingState); const updating = useAtomValue(artistsUpdatingAtom);
const updateArtists = useUpdateArtists(); const updateArtists = useUpdateArtists();
useEffect(() => { useEffect(() => {

View File

@ -1,27 +1,21 @@
import { atom, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { atom, useAtom } from 'jotai';
import { useAtomValue, useUpdateAtom } from 'jotai/utils';
import { Album, Artist } from '../models/music'; import { Album, Artist } from '../models/music';
import { SubsonicApiClient } from '../subsonic/api'; import { SubsonicApiClient } from '../subsonic/api';
import { activeServer } from './settings'; import { activeServerAtom } from './settings';
export const artistsState = atom<Artist[]>({ export const artistsAtom = atom<Artist[]>([]);
key: 'artistsState', export const artistsUpdatingAtom = atom(false);
default: [],
});
export const artistsUpdatingState = atom<boolean>({
key: 'artistsUpdatingState',
default: false,
});
export const useUpdateArtists = () => { export const useUpdateArtists = () => {
const server = useRecoilValue(activeServer); const server = useAtomValue(activeServerAtom);
const [updating, setUpdating] = useAtom(artistsUpdatingAtom);
const setArtists = useUpdateAtom(artistsAtom);
if (!server) { if (!server) {
return () => Promise.resolve(); return () => Promise.resolve();
} }
const [updating, setUpdating] = useRecoilState(artistsUpdatingState);
const setArtists = useSetRecoilState(artistsState);
return async () => { return async () => {
if (updating) { if (updating) {
return; return;
@ -42,25 +36,18 @@ export const useUpdateArtists = () => {
} }
} }
export const albumsState = atom<Album[]>({ export const albumsAtom = atom<Album[]>([]);
key: 'albumsState', export const albumsUpdatingAtom = atom(false);
default: [],
});
export const albumsUpdatingState = atom<boolean>({
key: 'albumsUpdatingState',
default: false,
});
export const useUpdateAlbums = () => { export const useUpdateAlbums = () => {
const server = useRecoilValue(activeServer); const server = useAtomValue(activeServerAtom);
const [updating, setUpdating] = useAtom(albumsUpdatingAtom);
const setAlbums = useUpdateAtom(albumsAtom);
if (!server) { if (!server) {
return () => Promise.resolve(); return () => Promise.resolve();
} }
const [updating, setUpdating] = useRecoilState(albumsUpdatingState);
const setAlbums = useSetRecoilState(albumsState);
return async () => { return async () => {
if (updating) { if (updating) {
return; return;

View File

@ -1,28 +1,12 @@
import { atom, DefaultValue, selector } from 'recoil'; import { atom } from 'jotai';
import { AppSettings, Server } from '../models/settings'; import { AppSettings } from '../models/settings';
import { getAppSettings, setAppSettings } from '../storage/settings'; import atomWithAsyncStorage from '../storage/atomWithAsyncStorage';
export const appSettingsState = atom<AppSettings>({ export const appSettingsAtom = atomWithAsyncStorage<AppSettings>('@appSettings', {
key: 'appSettingsState', servers: [],
default: selector({
key: 'appSettingsState/default',
get: () => getAppSettings(),
}),
effects_UNSTABLE: [
({ onSet }) => {
onSet((newValue) => {
if (!(newValue instanceof DefaultValue)) {
setAppSettings(newValue);
}
});
}
],
}); });
export const activeServer = selector<Server | undefined>({ export const activeServerAtom = atom((get) => {
key: 'activeServer', const appSettings = get(appSettingsAtom);
get: ({get}) => { return appSettings.servers.find(x => x.id == appSettings.activeServer);
const appSettings = get(appSettingsState);
return appSettings.servers.find(x => x.id == appSettings.activeServer);
}
}); });

View File

@ -1,26 +1,28 @@
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
export async function getItem(key: string): Promise<string | null> { export async function getItem(key: string): Promise<any | null> {
try { try {
return await AsyncStorage.getItem(key); const item = await AsyncStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (e) { } catch (e) {
console.error(`getItem error (key: ${key})`, e); console.error(`getItem error (key: ${key})`, e);
return null; return null;
} }
} }
export async function multiGet(keys: string[]): Promise<[string, string | null][]> { export async function multiGet(keys: string[]): Promise<[string, any | null][]> {
try { try {
return await AsyncStorage.multiGet(keys); const items = await AsyncStorage.multiGet(keys);
return items.map(x => [x[0], x[1] ? JSON.parse(x[1]) : null]);
} catch (e) { } catch (e) {
console.error(`multiGet error`, e); console.error(`multiGet error`, e);
return []; return [];
} }
} }
export async function setItem(key: string, item: string): Promise<void> { export async function setItem(key: string, item: any): Promise<void> {
try { try {
await AsyncStorage.setItem(key, item); await AsyncStorage.setItem(key, JSON.stringify(item));
} catch (e) { } catch (e) {
console.error(`setItem error (key: ${key})`, e); console.error(`setItem error (key: ${key})`, e);
} }
@ -28,7 +30,7 @@ export async function setItem(key: string, item: string): Promise<void> {
export async function multiSet(items: string[][]): Promise<void> { export async function multiSet(items: string[][]): Promise<void> {
try { try {
await AsyncStorage.multiSet(items); await AsyncStorage.multiSet(items.map(x => [x[0], JSON.stringify(x[1])]));
} catch (e) { } catch (e) {
console.error(`multiSet error`, e); console.error(`multiSet error`, e);
} }

View File

@ -0,0 +1,10 @@
import { atomWithStorage } from 'jotai/utils';
import { getItem, setItem } from './asyncstorage';
export default <T>(key: string, defaultValue: T) => {
return atomWithStorage<T>(key, defaultValue, {
getItem: async () => await getItem(key) || defaultValue,
setItem: setItem,
delayInit: true,
});
}

View File

@ -1,15 +0,0 @@
import { AppSettings } from '../models/settings';
import { getItem, setItem } from './asyncstorage';
const appSettingsKey = '@appSettings';
export async function getAppSettings(): Promise<AppSettings> {
const item = await getItem(appSettingsKey);
return item ? JSON.parse(item) : {
servers: [],
};
}
export async function setAppSettings(appSettings: AppSettings): Promise<void> {
await setItem(appSettingsKey, JSON.stringify(appSettings));
}