LegendApp / legend-state

Legend-State is a super fast and powerful state library that enables fine-grained reactivity and easy automatic persistence
https://legendapp.com/open-source/state/
MIT License
2.87k stars 82 forks source link

TypeError: Cannot read property 'length' of undefined #249

Closed Dank-del closed 6 months ago

Dank-del commented 6 months ago

store.tsx

import { observable } from "@legendapp/state"
import { persistObservable } from "@legendapp/state/persist"
import { enableReactTracking } from "@legendapp/state/config/enableReactTracking"
import { configureObservablePersistence } from '@legendapp/state/persist'
import { ObservablePersistAsyncStorage } from '@legendapp/state/persist-plugins/async-storage'
import AsyncStorage from "@react-native-async-storage/async-storage"
import { enableReactComponents } from "@legendapp/state/config/enableReactComponents"
export interface Client {
   id: string;
   name: string;
   ratePerHour: number;
}

export interface Project {
   id: string;
   name: string;
   description: string;
   clientId: string;
}

export interface Task {
   id: string;
   name: string;
   description: string;
   projectId: string;
}

export interface TimeTracking {
   id: string;
   startTime: string;
   endTime?: string;
   taskId: string;
}

enableReactComponents();

// Global configuration
configureObservablePersistence({
   // Use AsyncStorage in React Native
   pluginLocal: ObservablePersistAsyncStorage,
   localOptions: {
      asyncStorage: {
         // The AsyncStorage plugin needs to be given the implementation of AsyncStorage
         AsyncStorage
      }
   }
})

export const clientState$ = observable<Client[]>()
export const projectState$ = observable<Project[]>()
export const taskState$ = observable<Task[]>()
export const timeTrackingState$ = observable<TimeTracking[]>()

enableReactTracking({ auto: true })

persistObservable(clientState$, { local: 'clientState', pluginLocal: ObservablePersistAsyncStorage })
persistObservable(projectState$, { local: 'projectState', pluginLocal: ObservablePersistAsyncStorage })
persistObservable(taskState$, { local: 'taskState', pluginLocal: ObservablePersistAsyncStorage })
persistObservable(timeTrackingState$, { local: 'timeTrackingState', pluginLocal: ObservablePersistAsyncStorage })

// export const useClients = () => {
//    const clients = clientState$.get()
//    const createClient = (client: Omit<Client, 'id'>) => {
//       clientState$.set([...clients, { ...client, id: String(Date.now()) }])
//    }
//    const updateClient = (client: Client) => {
//       clientState$.set(clients.map((c) => (c.id === client.id ? client : c)))
//    }
//    const deleteClient = (id: string) => {
//       clientState$.set(clients.filter((c) => c.id !== id))
//    }
//    return { clients, createClient, updateClient, deleteClient }
// }

// export const useProjects = () => {
//    const projects = projectState$.get()
//    const createProject = (project: Omit<Project, 'id'>) => {
//       projectState$.set([...projects, { ...project, id: String(Date.now()) }])
//    }
//    const updateProject = (project: Project) => {
//       projectState$.set(projects.map((p) => (p.id === project.id ? project : p)))
//    }
//    const deleteProject = (id: string) => {
//       projectState$.set(projects.filter((p) => p.id !== id))
//    }
//    return { projects, createProject, updateProject, deleteProject }
// }

// export const useTasks = () => {
//    const tasks = taskState$.get()
//    const createTask = (task: Omit<Task, 'id'>) => {
//       taskState$.set([...tasks, { ...task, id: String(Date.now()) }])
//    }
//    const updateTask = (task: Task) => {
//       taskState$.set(tasks.map((t) => (t.id === task.id ? task : t)))
//    }
//    const deleteTask = (id: string) => {
//       taskState$.set(tasks.filter((t) => t.id !== id))
//    }
//    return { tasks, createTask, updateTask, deleteTask }
// }

// export const useTimeTracking = () => {
//    const timeTrackings = timeTrackingState$.get()
//    const createTimeTracking = (timeTracking: Omit<TimeTracking, 'id'>) => {
//       timeTrackingState$.set([...timeTrackings, { ...timeTracking, id: String(Date.now()) }])
//    }
//    const updateTimeTracking = (timeTracking: TimeTracking) => {
//       timeTrackingState$.set(timeTrackings.map((t) => (t.id === timeTracking.id ? timeTracking : t)))
//    }
//    const deleteTimeTracking = (id: string) => {
//       timeTrackingState$.set(timeTrackings.filter((t) => t.id !== id))
//    }
//    return { timeTrackings, createTimeTracking, updateTimeTracking, deleteTimeTracking }
// }

usage

import { StyleSheet, Pressable, useColorScheme, Alert } from 'react-native';

import { Text, View } from '@/components/Themed';
import { useTheme } from '@react-navigation/native';
import { clientState$ } from '@/lib/store';
import { FontAwesome } from '@expo/vector-icons';
import Colors from '@/constants/Colors';

export default function TabOneScreen() {
  const theme = useTheme();
  const colorScheme = useColorScheme();
  const clients = clientState$.get();
  return (
    <View style={styles.container}>
      {clients?.map((client) => (
        <View key={client.id} style={{
          backgroundColor: theme.colors.card,
          padding: 10,
          borderRadius: 10
        }}>
          <View style={{
            display: 'flex',
            flexDirection: 'row',
            justifyContent: 'space-between',
            backgroundColor: theme.colors.card,
          }}>
            <Text style={styles.title}>{client.name}</Text>
            <Pressable onPress={() => {
              Alert.prompt('Are you sure?', 'Tap "yes" to confirm', [
                {
                  text: 'Yes',
                  style: 'destructive',
                  onPress: () => {
                    clientState$.set(clientState$.get().filter((v) => v.id === client.id))
                  }
                },
                {
                  text: 'Cancel',
                  onPress: () => console.log('Cancel Pressed'),
                  style: 'cancel',
                },
              ])
            }}>
              {({ pressed }) => (
                <FontAwesome
                  name="trash-o"
                  size={25}
                  color={Colors[colorScheme ?? 'light'].text}
                  style={{ marginRight: 15, opacity: pressed ? 0.5 : 1 }}
                />
              )}
            </Pressable>
          </View>
          <View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />

          <Text style={styles.footer}>Gives ${client.ratePerHour} per hour</Text>
        </View>
      ))}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    margin: 9,
    rowGap: 9
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
  },
  separator: {
    marginVertical: 10,
    height: 1,
    width: '100%',
  },
  footer: {
    textAlign: 'center',
    fontSize: 16,
  }
});
Dank-del commented 6 months ago

fixed by rewriting to this

import { observable } from "@legendapp/state"
import { persistObservable } from "@legendapp/state/persist"
import { enableReactTracking } from "@legendapp/state/config/enableReactTracking"
import { configureObservablePersistence } from '@legendapp/state/persist'
import { ObservablePersistAsyncStorage } from '@legendapp/state/persist-plugins/async-storage'
import AsyncStorage from "@react-native-async-storage/async-storage"
import { enableReactComponents } from "@legendapp/state/config/enableReactComponents"
import * as Crypto from 'expo-crypto';

export interface Client {
   id: string;
   name: string;
   ratePerHour: number;
}

export interface Project {
   id: string;
   name: string;
   description: string;
   clientId: string;
}

export interface Task {
   id: string;
   name: string;
   description: string;
   projectId: string;
}

export interface TimeTracking {
   id: string;
   startTime: string;
   endTime?: string;
   taskId: string;
}

enableReactComponents();

// Global configuration
configureObservablePersistence({
   // Use AsyncStorage in React Native
   pluginLocal: ObservablePersistAsyncStorage,
   localOptions: {
      asyncStorage: {
         // The AsyncStorage plugin needs to be given the implementation of AsyncStorage
         AsyncStorage
      }
   }
})

export const state$ = observable({
   clients: [] as Client[],
   addClient: (client: Omit<Client, 'id'>) => {
      state$.assign({ clients: [...state$.clients.get(), { ...client, id: Crypto.randomUUID() }] })
   },
   updateClient: (client: Client) => {
      state$.assign({ clients: state$.clients.get().map((c) => (c.id === client.id ? client : c)) })
   },
   deleteClient: (id: string) => {
      state$.assign({ clients: state$.clients.get().filter((c) => c.id !== id) })
   },
   projects: [] as Project[],
   addProject: (project: Omit<Project, 'id'>) => {
      state$.assign({ projects: [...state$.projects.get(), { ...project, id: Crypto.randomUUID() }] })
   },
   updateProject: (project: Project) => {
      state$.assign({ projects: state$.projects.get().map((p) => (p.id === project.id ? project : p)) })
   },
   deleteProject: (id: string) => {
      state$.assign({ projects: state$.projects.get().filter((p) => p.id !== id) })
   },
   tasks: [] as Task[],
   addTask: (task: Omit<Task, 'id'>) => {
      state$.assign({ tasks: [...state$.tasks.get(), { ...task, id: Crypto.randomUUID() }] })
   },
   updateTask: (task: Task) => {
      state$.assign({ tasks: state$.tasks.get().map((t) => (t.id === task.id ? task : t)) })
   },
   deleteTask: (id: string) => {
      state$.assign({ tasks: state$.tasks.get().filter((t) => t.id !== id) })
   },
   timeTracking: [] as TimeTracking[],
   addTimeTracking: (timeTracking: Omit<TimeTracking, 'id'>) => {
      state$.assign({ timeTracking: [...state$.timeTracking.get(), { ...timeTracking, id: Crypto.randomUUID() }] })
   },
   updateTimeTracking: (timeTracking: TimeTracking) => {
      state$.assign({ timeTracking: state$.timeTracking.get().map((t) => (t.id === timeTracking.id ? timeTracking : t)) })
   },
   deleteTimeTracking: (id: string) => {
      state$.assign({ timeTracking: state$.timeTracking.get().filter((t) => t.id !== id) })
   },
});

enableReactTracking({ auto: true })
persistObservable(state$, { local: 'state', pluginLocal: ObservablePersistAsyncStorage })
jmeistrich commented 6 months ago

I think the original problem was that although you typed the observables as arrays they weren't initialized as arrays so they were treated as objects. So just initializing as empty arrays should fix it:

export const clientState$ = observable<Client[]>([])

And one suggestion: although what you're doing should work fine, recreating arrays every time is not great for performance. It's more efficient to use array methods like push/splice. Instead of:

 addClient: (client: Omit<Client, 'id'>) => {
      state$.assign({ clients: [...state$.clients.get(), { ...client, id: Crypto.randomUUID() }] })
   }

I'd suggest:

addClient: (client: Omit<Client, 'id'>) => {
      state$.clients.push({ ...client, id: Crypto.randomUUID() })
   }