Last active
March 23, 2023 21:20
-
-
Save Grubba27/23cf4be926c4fcea17850b2d866b56e1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Meteor } from 'meteor/meteor'; | |
| import { Mongo } from 'meteor/mongo'; | |
| import { | |
| useReducer, | |
| useMemo, | |
| useEffect, | |
| Reducer, | |
| DependencyList, | |
| useRef, | |
| } from 'react'; | |
| import { Tracker } from 'meteor/tracker'; | |
| type useFindActions<T> = | |
| | { type: 'refresh'; data: T[] } | |
| | { type: 'addedAt'; document: T; atIndex: number } | |
| | { type: 'changedAt'; document: T; atIndex: number } | |
| | { type: 'removedAt'; atIndex: number } | |
| | { type: 'movedTo'; fromIndex: number; toIndex: number }; | |
| const useFindReducer = <T>(data: T[], action: useFindActions<T>): T[] => { | |
| switch (action.type) { | |
| case 'refresh': | |
| return action.data; | |
| case 'addedAt': | |
| return [ | |
| ...data.slice(0, action.atIndex), | |
| action.document, | |
| ...data.slice(action.atIndex), | |
| ]; | |
| case 'changedAt': | |
| return [ | |
| ...data.slice(0, action.atIndex), | |
| action.document, | |
| ...data.slice(action.atIndex + 1), | |
| ]; | |
| case 'removedAt': | |
| return [ | |
| ...data.slice(0, action.atIndex), | |
| ...data.slice(action.atIndex + 1), | |
| ]; | |
| case 'movedTo': | |
| const doc = data[action.fromIndex]; | |
| const copy = [ | |
| ...data.slice(0, action.fromIndex), | |
| ...data.slice(action.fromIndex + 1), | |
| ]; | |
| copy.splice(action.toIndex, 0, doc); | |
| return copy; | |
| } | |
| }; | |
| // Check for valid Cursor or null. | |
| // On client, we should have a Mongo.Cursor (defined in | |
| // https://github.com/meteor/meteor/blob/devel/packages/minimongo/cursor.js and | |
| // https://github.com/meteor/meteor/blob/devel/packages/mongo/collection.js). | |
| // On server, however, we instead get a private Cursor type from | |
| // https://github.com/meteor/meteor/blob/devel/packages/mongo/mongo_driver.js | |
| // which has fields _mongo and _cursorDescription. | |
| const checkCursor = <T>( | |
| cursor: | |
| | Mongo.Cursor<T> | |
| | Partial<{ _mongo: any; _cursorDescription: any }> | |
| | undefined | |
| | null | |
| ) => { | |
| if ( | |
| cursor !== null && | |
| cursor !== undefined && | |
| !(cursor instanceof Mongo.Cursor) && | |
| !(cursor._mongo && cursor._cursorDescription) | |
| ) { | |
| console.warn( | |
| 'Warning: useFind requires an instance of Mongo.Cursor. ' + | |
| 'Make sure you do NOT call .fetch() on your cursor.' | |
| ); | |
| } | |
| }; | |
| // Synchronous data fetch. It uses cursor observing instead of cursor.fetch() because synchronous fetch will be deprecated. | |
| const fetchData = <T>(cursor: Mongo.Cursor<T>) => { | |
| const data: T[] = []; | |
| // in Meteor 3 | |
| const observer = cursor.observe({ | |
| addedAt(document, atIndex, before) { | |
| data.splice(atIndex, 0, document); | |
| console.log(data); | |
| }, | |
| }); | |
| if (observer.isReady) observer.stop(); | |
| else | |
| setTimeout(async () => { | |
| await observer.isReadyPromise; | |
| observer.stop(); | |
| }, 0); | |
| // in Meteor 2.x | |
| // new Promise( resolve => { | |
| // const observe = cursor.observe({ | |
| // addedAt(document, atIndex, before) { | |
| // data.splice(atIndex, 0, document); | |
| // }, | |
| // }); | |
| // resolve(observe) | |
| // }).then(async o => { | |
| // o.stop(); | |
| // }); | |
| return data; | |
| }; | |
| const useFindClient = <T = any>( | |
| factory: () => Mongo.Cursor<T> | undefined | null, | |
| deps: DependencyList = [] | |
| ) => { | |
| const cursor = useMemo(() => { | |
| // To avoid creating side effects in render, opt out | |
| // of Tracker integration altogether. | |
| const cursor = Tracker.nonreactive(factory); | |
| if (Meteor.isDevelopment) { | |
| checkCursor(cursor); | |
| } | |
| return cursor; | |
| }, deps); | |
| const [data, dispatch] = useReducer<Reducer<T[], useFindActions<T>>, null>( | |
| useFindReducer, | |
| null, | |
| () => { | |
| if (!(cursor instanceof Mongo.Cursor)) { | |
| return []; | |
| } | |
| return fetchData(cursor); | |
| } | |
| ); | |
| // Store information about mounting the component. | |
| // It will be used to run code only if the component is updated. | |
| const didMount = useRef(false); | |
| useEffect(() => { | |
| // Fetch intitial data if cursor was changed. | |
| if (didMount.current) { | |
| if (!(cursor instanceof Mongo.Cursor)) { | |
| return; | |
| } | |
| const data = fetchData(cursor); | |
| dispatch({ type: 'refresh', data }); | |
| } else { | |
| didMount.current = true; | |
| } | |
| if (!(cursor instanceof Mongo.Cursor)) { | |
| return; | |
| } | |
| const observer = cursor.observe({ | |
| addedAt(document, atIndex, before) { | |
| dispatch({ type: 'addedAt', document, atIndex }); | |
| }, | |
| changedAt(newDocument, oldDocument, atIndex) { | |
| dispatch({ type: 'changedAt', document: newDocument, atIndex }); | |
| }, | |
| removedAt(oldDocument, atIndex) { | |
| dispatch({ type: 'removedAt', atIndex }); | |
| }, | |
| movedTo(document, fromIndex, toIndex, before) { | |
| dispatch({ type: 'movedTo', fromIndex, toIndex }); | |
| }, | |
| // @ts-ignore | |
| _suppress_initial: true, | |
| }); | |
| return () => { | |
| observer.stop(); | |
| }; | |
| }, [cursor]); | |
| return cursor ? data : cursor; | |
| }; | |
| const useFindServer = <T = any>( | |
| factory: () => Mongo.Cursor<T> | undefined | null, | |
| deps: DependencyList | |
| ) => | |
| Tracker.nonreactive(() => { | |
| const cursor = factory(); | |
| if (Meteor.isDevelopment) checkCursor(cursor); | |
| return cursor?.fetch?.() ?? null; | |
| }); | |
| export const useFind = Meteor.isServer ? useFindServer : useFindClient; | |
| function useFindDev<T = any>( | |
| factory: () => Mongo.Cursor<T> | undefined | null, | |
| deps: DependencyList = [] | |
| ) { | |
| function warn(expects: string, pos: string, arg: string, type: string) { | |
| console.warn( | |
| `Warning: useFind expected a ${expects} in it\'s ${pos} argument ` + | |
| `(${arg}), but got type of \`${type}\`.` | |
| ); | |
| } | |
| if (typeof factory !== 'function') { | |
| warn('function', '1st', 'reactiveFn', factory); | |
| } | |
| if (!deps || !Array.isArray(deps)) { | |
| warn('array', '2nd', 'deps', typeof deps); | |
| } | |
| return useFind(factory, deps); | |
| } | |
| export default Meteor.isDevelopment ? useFindDev : useFind; | |
| /// server.js | |
| export const TasksCollection = new Mongo.Collection('tasks'); | |
| Meteor.methods({ | |
| async addTask({ description }) { | |
| return await TasksCollection.insertAsync({ | |
| description, | |
| done: true, | |
| userId: Meteor.userId(), | |
| createdAt: new Date(), | |
| }); | |
| }, | |
| }); | |
| /// client.tsx | |
| export const TaskItems = () => { | |
| const [hideDone, setHideDone] = useState(false); | |
| useSubscribe(tasksPublication.config.name); | |
| const userId = useTracker('user', () => Meteor.userId()); | |
| const filter = hideDone ? { done: { $ne: true }, userId } : { userId }; | |
| const tasks = useFind( | |
| () => | |
| TasksCollection.find(filter, { | |
| sort: { createdAt: -1 }, | |
| }), | |
| [hideDone] | |
| ); // here where it throws an error | |
| const pendingCount = useFind( | |
| () => TasksCollection.find({ done: { $ne: true }, userId }), | |
| [hideDone] | |
| ).length; | |
| return ( | |
| <Box | |
| mt={8} | |
| py={{ base: 2 }} | |
| px={{ base: 4 }} | |
| pb={{ base: 4 }} | |
| border={1} | |
| borderStyle="solid" | |
| borderRadius="md" | |
| borderColor={useColorModeValue('gray.400', 'gray.700')} | |
| > | |
| <HStack mt={2}> | |
| <Box w="70%"> | |
| <Text | |
| as="span" | |
| color={useColorModeValue('gray.600', 'gray.400')} | |
| fontSize="xs" | |
| > | |
| You have {tasks.length} {tasks.length === 1 ? 'task ' : 'tasks '} | |
| and {pendingCount || 0} pending. | |
| </Text> | |
| </Box> | |
| <Stack w="30%" justify="flex-end" direction="row"> | |
| <Button | |
| bg="teal.600" | |
| color="white" | |
| colorScheme="teal" | |
| size="xs" | |
| onClick={() => setHideDone(!hideDone)} | |
| > | |
| {hideDone ? 'Show All Tasks' : 'Show Pending'} | |
| </Button> | |
| </Stack> | |
| </HStack> | |
| {tasks.map(task => ( | |
| <TaskItem | |
| key={task._id} | |
| task={task} | |
| onMarkAsDone={taskId => toggleTaskDone({ taskId })} | |
| onDelete={taskId => removeTask({ taskId })} | |
| /> | |
| ))} | |
| </Box> | |
| ); | |
| }; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment