<![CDATA[ClassDojo Engineering Blog]]>https://engineering.classdojo.com/classdojo.jpgClassDojo Engineering Bloghttps://engineering.classdojo.comGatsbyJSMon, 01 Dec 2025 14:52:51 GMTMon, 01 Dec 2025 14:50:18 GMT<![CDATA[Automating Weekly Business Reviews with DBT Macros]]>https://engineering.classdojo.com/2025/03/19/automating-weekly-business-reviews-with-dbthttps://engineering.classdojo.com/2025/03/19/automating-weekly-business-reviews-with-dbtWed, 19 Mar 2025 00:00:00 GMT<![CDATA[XKCD's "Is It Worth the Time?" Considered Harmful]]>https://engineering.classdojo.com/2025/01/08/its-not-worth-the-time-yethttps://engineering.classdojo.com/2025/01/08/its-not-worth-the-time-yetWed, 08 Jan 2025 00:00:00 GMT<![CDATA[18,957 tests in under 6 minutes: ClassDojo's approach to backend testing]]>https://engineering.classdojo.com/2024/12/13/backend-testing-at-classdojo-2024https://engineering.classdojo.com/2024/12/13/backend-testing-at-classdojo-2024Fri, 13 Dec 2024 00:00:00 GMT<![CDATA[JSDoc comments can make fixtures easier to work with]]>https://engineering.classdojo.com/2024/10/30/jsdoc-comments-for-fixtureshttps://engineering.classdojo.com/2024/10/30/jsdoc-comments-for-fixturesWed, 30 Oct 2024 00:00:00 GMT<![CDATA[Adopting React Query]]> = { start: (params: Params) => void; done: boolean; error: Error; } // More on this later... const useFetcherMutation = makeOperation({ ... }); const RootComponent = () => { const { start, done, error }: FetcherResult = useFetcherMutation(); useEffect(() => { if (error) { // handle error } else if (done) { // handle success } }, [done, error]); return ( ) } const LeafComponent = ({ start }) => { const handleClick = () => { // No way to handle success/error here, we can only call it. // There may or may not be handling somewhere else...? start({ ... }); }; return ; } ``` With React Query, the mutation function itself allows for handling the trigger & success/error cases co-located: ```typescript jsx // React Query // Partial typing to illustrate the point type ReactQueryResult = { start: (params: Params) => Promise; } // More on this later... const useReactQueryMutation = makeOperation({ ... }); const RootComponent = () => { const { start }: FetcherResult = useReactQueryMutation(); return ( ) } const LeafComponent = ({ start }) => { // Mutation trigger & success/error cases colocated const handleClick = async () => { try { const result = await start({ ... }); // handle success } catch(ex) { // handle error } } return ; } ``` Finally, Fetchers’ cache keys were string-based, which meant they couldn’t provide granular control for targeting multiple cache keys like React Query does. For example, a cache key’s pattern in Fetchers looked like this: ```javascript const cacheKey = 'fetcherName=classStoryPost/params={"classId":"123","postId":"456"}' ``` In React Query, we get array based cache keys that support objects, allowing us to target certain cache entries for invalidation using partial matches: ```javascript const cacheKey = ['classStoryPost', { classId: '123', postId: '456' }]; // Invalidate all story posts for a class queryClient.invalidateQueries({ queryKey: ['classStoryPost', { classId: '123' }] }); ``` The issues we were facing were solvable problems, but not worth the effort. Rather than continuing to invest time and energy into Fetchers, we decided to put it towards migrating our codebase to React Query. The only question left was “How?” ## The Plan At ClassDojo, we have a weekly “web guild” meeting for discussing, planning, and assigning engineering work that falls outside the scope of teams and their product work. We used these meetings to drive discussions and gain consensus around a migration plan and divvy out the work to developers. To understand the plan we agreed on, let’s review Fetchers. The API consists of three primary functions: `makeMemberFetcher`, `makeCollectionFetcher`, and `makeOperation`. Each is a factory function for producing hooks that query or mutate our API. The hooks returned by each factory function are _almost_ identical to React Query’s `useQuery`, `useInfiniteQuery`, and `useMutation` hooks. Functionally, they achieve the same things, but with different options, naming conventions, and implementations. The similarities between the hooks returned from Fetchers’ factory functions and React Query made for the perfect place to target our migration. The plan was to implement alternate versions of Fetcher’s factory functions using the same API interfaces, but instead using React Query hooks under the hood. By doing so, we could ship both implementations simultaneously and use a feature switch to toggle between the two. Additionally, we could rely on Fetchers’ unit tests to catch any differences between the two. ![](https://directus.internal.classdojo.com/assets/66bec141-fd42-4d63-a112-dc6b2dc0d77f) Our plan felt solid, but we still wanted to be careful in how we rolled out the new implementations so as to minimize risk. Given that we were rewriting each of Fetchers’ factory functions, each had the possibility of introducing their own class of bugs. On top of that, our front end had four different apps consuming the Fetchers library, layering on additional usage patterns and environmental circumstances. Spotting errors thrown inside the library code is easy, but spotting errors that cascade out to other parts of the app as a result of small changes in behavior is _much_ harder. We decided to use a phased rollout of each factory function one at a time, app by app so that any error spikes would be isolated to one implementation or app at a time, making it easy to spot which implementation had issues. Below is some pseudocode that illustrates the sequencing of each phase: ``` for each factoryFn in Fetchers: write factoryFn using React Query for each app in ClassDojo: rollout React Query factoryFn using feature switch monitor for errors if errors: turn off feature switch fix bugs repeat ``` ## What Went Well? Abstractions made the majority of this project a breeze. The factory functions provided a single point of entry to replace our custom logic with React Query hooks. Instead of having to assess all 365 usages of Fetcher hooks, their options, and how they map to a React Query hook, we just had to ensure that the hook _returned_ by each factory function behaved the same way it did before. Additionally, swapping implementations between Fetchers and React Query was just a matter of changing the exported functions from Fetchers’ index file, avoiding massive PRs with 100+ files changed in each: ```typescript // before migration export { makeMemberFetcher } from './fetchers'; // during migration import { makeMemberFetcher as makeMemberFetcherOld } from './fetchers'; import { makeMemberFetcher as makeMemberFetcherNew } from './rqFetchers'; const makeMemberFetcher = isRQFetchersOn ? makeMemberFetcherNew : makeMemberFetcherOld; export { makeMemberFetcher }; ``` Our phased approach played a big role in the success of the project. The implementation of makeCollectionFetcher worked fine in the context of one app, but surfaced some errors in the context of another. It wasn’t necessarily easy to know _what_ was causing the bug, but the surface area we had to scan for potential problems was _much_ smaller, allowing us to iterate faster. Phasing the project also naturally lent itself well to parallelizing the development process and getting many engineers involved. Getting the implementations of each factory function to behave exactly the same as before was not an easy process. We went through many iterations of fixing broken tests before the behavior matched up correctly. Doing that alone would have been a slow and painful process. ## How Can We Improve? One particular pain point with this project were Fetchers’ unit tests. Theoretically, they should have been all we needed to verify the new implementations. Unfortunately, they were written with dependencies on implementation details, making it difficult to just run them against a new implementation. I spent some time trying to rewrite them, but quickly realized the effort wasn't worth the payoff. Instead, we relied on unit & end-to-end tests throughout the application that would naturally hit these codepaths. The downside was that we spent a lot of time stepping through and debugging those other tests to understand what was broken in our new implementations. This was a painful reminder to write unit tests that only observe the inputs and outputs. Another pain point was the manual effort involved in monitoring deployments for errors. When we rolled out the first phase of the migration, we realized it’s not so easy to tell whether we were introducing new errors or not. There was a lot of existing noise in our logs that required babysitting the deployments and checking reported errors to confirm whether or not the error was new. We also realized we didn’t have a good mechanism for scoping our error logs down to the latest release only. We’ve since augmented our logs with better tags to make it easier to query for the “latest” version. We’ve also set up a weekly meeting to triage error logs to specific teams so that we don’t end up in the same situation again. ## What's Next? Migrating to React Query was a huge success. It rid us of maintaining a complex chunk of code that very few developers even understood. Now we’ve started asking ourselves, “What’s next?”. We’ve already started using lifecycle callbacks to deprecate our cache invalidation & optimistic update patterns. Those patterns were built on top of redux to subscribe to lifecycle events in Fetchers’ mutations, but now we can simply hook into onMutate, onSuccess, onError provided by React Query. Next, we’re going to look at using async mutations to simplify how we handle the UX for success & error cases. There are still a lot of patterns leftover from Fetchers and it will be a continued effort to rethink how we can simplify things using React Query. ## Conclusion Large code migrations can be really scary. There’s a lot of potential for missteps if you’re not careful. I personally believe that what made this project successful was treating it like a refactor. The end goal wasn’t to _change_ the behavior of anything, just to refactor the implementation. Trying to swap one for the other without first finding their overlap could have made this a messy project. Instead, we wrote new implementations, verified they pass our tests, and shipped them one by one. This project also couldn’t have happened without the excellent engineering culture at ClassDojo. Instead of being met with resistance, everyone was eager and excited to help out and get things moving. I’m certain there will be more projects like this to follow in the future.]]>https://engineering.classdojo.com/2023/09/11/adopting-react-queryhttps://engineering.classdojo.com/2023/09/11/adopting-react-queryMon, 11 Sep 2023 00:00:00 GMT<![CDATA[Slack is the Worst Info-Radiator]]>https://engineering.classdojo.com/2023/06/14/slack-is-the-worst-info-radiatorhttps://engineering.classdojo.com/2023/06/14/slack-is-the-worst-info-radiatorWed, 14 Jun 2023 00:00:00 GMT<![CDATA[Being On-call at ClassDojo]]>https://engineering.classdojo.com/2023/02/22/oncall-at-classdojohttps://engineering.classdojo.com/2023/02/22/oncall-at-classdojoWed, 22 Feb 2023 00:00:00 GMT<![CDATA[Culling Containers with a Leaky Bucket]]>https://engineering.classdojo.com/2022/12/05/culling-containers-with-a-leaky-buckethttps://engineering.classdojo.com/2022/12/05/culling-containers-with-a-leaky-bucketMon, 05 Dec 2022 00:00:00 GMT<![CDATA[Patterns of the Swarm]]>https://engineering.classdojo.com/2022/11/22/patterns-of-the-swarmhttps://engineering.classdojo.com/2022/11/22/patterns-of-the-swarmTue, 22 Nov 2022 00:00:00 GMT<![CDATA[Shell Patterns for Easy Code Migrations]]>https://engineering.classdojo.com/2022/10/12/shell-patterns-for-easy-code-migrationshttps://engineering.classdojo.com/2022/10/12/shell-patterns-for-easy-code-migrationsWed, 12 Oct 2022 00:00:00 GMT<![CDATA[Simulating network problems with docker network]]>https://engineering.classdojo.com/2022/10/05/simulating-network-problems-with-docker-networkhttps://engineering.classdojo.com/2022/10/05/simulating-network-problems-with-docker-networkWed, 05 Oct 2022 00:00:00 GMT<![CDATA[Post-Merge Code Reviews are Great!]]>https://engineering.classdojo.com/2022/09/14/post-merge-code-reviews-are-greathttps://engineering.classdojo.com/2022/09/14/post-merge-code-reviews-are-greatWed, 14 Sep 2022 00:00:00 GMT<![CDATA[Engineering Dojo, Episode 2: How and why we use CI/CD]]>https://engineering.classdojo.com/2022/08/24/enginering-dojo-2-ci-cdhttps://engineering.classdojo.com/2022/08/24/enginering-dojo-2-ci-cdWed, 24 Aug 2022 00:00:00 GMT<![CDATA[Playtesting: maintaining high product quality without QA]]>https://engineering.classdojo.com/2022/08/24/playtestinghttps://engineering.classdojo.com/2022/08/24/playtestingWed, 24 Aug 2022 00:00:00 GMT<![CDATA[Using NodeJS's AsyncLocalStorage to Instrument a Webserver]]>https://engineering.classdojo.com/2022/08/09/async-local-storage-in-detailhttps://engineering.classdojo.com/2022/08/09/async-local-storage-in-detailTue, 09 Aug 2022 00:00:00 GMT<![CDATA[Large Analytics SQL Queries are a Code Smell]]>https://engineering.classdojo.com/2022/08/03/large-analytics-sql-queries-are-a-code-smellhttps://engineering.classdojo.com/2022/08/03/large-analytics-sql-queries-are-a-code-smellWed, 03 Aug 2022 00:00:00 GMT<![CDATA[From Google Analytics to Matomo Part 3]]>https://engineering.classdojo.com/2022/07/27/google-analytics-to-matomo-p3https://engineering.classdojo.com/2022/07/27/google-analytics-to-matomo-p3Wed, 27 Jul 2022 00:00:00 GMT<![CDATA[From Google Analytics to Matomo Part 2]]>https://engineering.classdojo.com/2022/07/20/google-analytics-to-matomo-p2https://engineering.classdojo.com/2022/07/20/google-analytics-to-matomo-p2Wed, 20 Jul 2022 00:00:00 GMT<![CDATA[From Google Analytics to Matomo Part 1]]>https://engineering.classdojo.com/2022/07/13/google-analytics-to-matomo-p1https://engineering.classdojo.com/2022/07/13/google-analytics-to-matomo-p1Wed, 13 Jul 2022 00:00:00 GMT<![CDATA[Scalable, Replicated VPN - with Pritunl]]>https://engineering.classdojo.com/2022/06/12/scalable-replicated-vpn-with-pritunlhttps://engineering.classdojo.com/2022/06/12/scalable-replicated-vpn-with-pritunlSun, 12 Jun 2022 00:00:00 GMT<![CDATA[TypeScript String Theory]]>https://engineering.classdojo.com/2022/05/04/typescript-string-theoryhttps://engineering.classdojo.com/2022/05/04/typescript-string-theoryWed, 04 May 2022 00:00:00 GMT<![CDATA[Engineering Dojo, Episode 1: Building Payment Systems]]>https://engineering.classdojo.com/2022/04/05/engineering-dojo-1-payment-systemhttps://engineering.classdojo.com/2022/04/05/engineering-dojo-1-payment-systemTue, 05 Apr 2022 00:00:00 GMT<![CDATA[Red and Blue Function Mistakes in JavaScript]]>https://engineering.classdojo.com/2022/03/25/red-blue-function-mistakes-in-jshttps://engineering.classdojo.com/2022/03/25/red-blue-function-mistakes-in-jsFri, 25 Mar 2022 00:00:00 GMT<![CDATA[Staying Connected as a Digital Nomad]]>https://engineering.classdojo.com/2022/03/10/staying-connected-as-a-digital-nomadhttps://engineering.classdojo.com/2022/03/10/staying-connected-as-a-digital-nomadThu, 10 Mar 2022 00:00:00 GMT<![CDATA[How We Built a DataOps Platform]]>https://engineering.classdojo.com/2022/02/07/dataops-at-classdojohttps://engineering.classdojo.com/2022/02/07/dataops-at-classdojoMon, 07 Feb 2022 00:00:00 GMT<![CDATA[Editing 200 Files with Bash and Perl]]>https://engineering.classdojo.com/2022/01/13/editing-200-files-with-bash-and-perlhttps://engineering.classdojo.com/2022/01/13/editing-200-files-with-bash-and-perlThu, 13 Jan 2022 00:00:00 GMT<![CDATA[Our Approach to Mob Programming]]>https://engineering.classdojo.com/2021/12/06/mob-programming-at-classdojohttps://engineering.classdojo.com/2021/12/06/mob-programming-at-classdojoMon, 06 Dec 2021 00:00:00 GMT<![CDATA[Canary Containers at ClassDojo in Too Much Detail]]>https://engineering.classdojo.com/2021/11/02/canary-containers-in-too-much-detailhttps://engineering.classdojo.com/2021/11/02/canary-containers-in-too-much-detailTue, 02 Nov 2021 00:00:00 GMT<![CDATA[Entity Component Systems: Performance isn't the only benefit]]>https://engineering.classdojo.com/2021/10/29/entity-component-systems-lead-to-great-codehttps://engineering.classdojo.com/2021/10/29/entity-component-systems-lead-to-great-codeFri, 29 Oct 2021 00:00:00 GMT<![CDATA[P1, P2,...P5 is a broken system: here's what we do instead]]>https://engineering.classdojo.com/2021/09/30/p1-p2-is-a-broken-systemhttps://engineering.classdojo.com/2021/09/30/p1-p2-is-a-broken-systemThu, 30 Sep 2021 00:00:00 GMT<![CDATA[Even Better Rate Limiting]]>https://engineering.classdojo.com/2021/08/25/even-better-rate-limitinghttps://engineering.classdojo.com/2021/08/25/even-better-rate-limitingWed, 25 Aug 2021 00:00:00 GMT<![CDATA[AsyncLocalStorage Makes the Commons Legible]]>https://engineering.classdojo.com/2021/08/20/asynclocalstorage-makes-the-commons-legiblehttps://engineering.classdojo.com/2021/08/20/asynclocalstorage-makes-the-commons-legibleFri, 20 Aug 2021 00:00:00 GMT<![CDATA[The 3 things I didn't understand about TypeScript]]>https://engineering.classdojo.com/2021/08/10/3-confusing-things-about-typescripthttps://engineering.classdojo.com/2021/08/10/3-confusing-things-about-typescriptTue, 10 Aug 2021 00:00:00 GMT<![CDATA[Manipulating Text & Files solves problems]]>https://engineering.classdojo.com/2021/08/02/manipulating-text-and-files-solves-problemshttps://engineering.classdojo.com/2021/08/02/manipulating-text-and-files-solves-problemsMon, 02 Aug 2021 00:00:00 GMT<![CDATA[HyperLogLog-orrhea]]>https://engineering.classdojo.com/2021/07/23/hyperloglog-orrheahttps://engineering.classdojo.com/2021/07/23/hyperloglog-orrheaFri, 23 Jul 2021 00:00:00 GMT<![CDATA[Creating an Actionable Logging Taxonomy]]>https://engineering.classdojo.com/2021/07/20/actionable-logging-taxonomyhttps://engineering.classdojo.com/2021/07/20/actionable-logging-taxonomyTue, 20 Jul 2021 00:00:00 GMT<![CDATA[Graceful web-server shutdowns behind HAProxy]]>https://engineering.classdojo.com/2021/07/13/haproxy-graceful-server-shutdownshttps://engineering.classdojo.com/2021/07/13/haproxy-graceful-server-shutdownsTue, 13 Jul 2021 00:00:00 GMT<![CDATA[Service Discovery With Consul at ClassDojo]]>https://engineering.classdojo.com/2018/05/10/service-discovery-with-consul-at-classdojohttps://engineering.classdojo.com/2018/05/10/service-discovery-with-consul-at-classdojoThu, 10 May 2018 00:00:00 GMT<![CDATA[Powering Class Story With Texture]]> We initially used [UIKit](https://developer.apple.com/documentation/uikit) to power our first version of story: Class Story. The spec only allowed photo, text, or photo and text posts for teachers So we used [UITableView](https://developer.apple.com/documentation/uikit/uitableview) and [UITableViewCell](https://developer.apple.com/documentation/uikit/uitableviewcell) for the views. We use [Core Data](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreData/index.html) to store all of the data, so we can leverage convenient UIKit classes like [NSFetchedResultsController](https://developer.apple.com/documentation/coredata/nsfetchedresultscontroller) to make the data-binding easier. However, as the product grew, we started running into common issues with our UIKit-based solution. Notably: - Out of bounds exceptions on [NSFetchedResultsControllerDelegate](https://developer.apple.com/documentation/coredata/nsfetchedresultscontrollerdelegate) calls - Scroll slowdown on older devices - [NSIndexPath](https://developer.apple.com/documentation/foundation/nsindexpath) wrangling to handle cells above and below Core-Data backed cells - Height calculation issues on cells - [Auto Layout](https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/index.html) issues - Preloading older data as the user scrolled to the bottom of a feed (i.e. infinite scrolling) We needed to find a better set of solutions to tackle each issue. But it wasn't as if we were the first team ever to try and figure out how to develop a feed - there are several open-source solutions for handling this use-case. Our game plan was to examine such solutions and see if they were better than refactoring and staying with UIKit. We looked at a few of these libraries ([ComponentKit](http://componentkit.org/), [IGListKit](https://instagram.github.io/IGListKit/), and [Texture - formerly AsyncDisplayKit](http://texturegroup.org/)) and built a few test apps to see what kinds of costs/benefits each provided. We ultimately ended up settling on Texture because of its high-performance scrolling, ability to handle complex user-interactions, large contributor base and intelligent preloading for infinite scroll. Texture gave a few wins right out of the box: height calculation, autolayout constraint issues, and infinite scrolling were effectively solved for us. Texture implements a [flexbox-styled layout system](http://texturegroup.org/docs/automatic-layout-examples-2.html) that infers height from a layout specification object each view can choose to implement. Layout and render calculations are all performed on background threads, which allows the main thread to remain available for user-interactions and animations. A convenient delegate method, [`-tableView:(ASTableView *)tableView willBeginBatchFetchWithContext:(ASBatchContext *)context`](http://texturegroup.org/appledoc/Protocols/ASTableDelegate.html#//api/name/tableNode:willBeginBatchFetchWithContext:) allows us to detect when the user is going to scroll to the bottom of the screen and fire the network requests necessary to provide a smooth, infinite-scroll, experience. Handling NSIndexPath calculations required a bit of additional wrangling. Quite a few of our feeds include auxiliary cells at the top or bottom of the feed. These cells usually have some kind of call to action inviting users down some deeper user flow we are trying to encourage. Implementing these cells in UIKit and Core Data caused us to implement some rather annoying index calculations. Most of these calculations arose because of constraints around NSFetchedResultsController. The fetched results controller is a convenient class that allows handling of predicates, Core Data update handlers, and data fetching. The problem is that the fetched results controller always assumes that its indices start from row 0. Adding additional cells above or below require calculations on detecting whether or not a given index is for an auxiliary cell and, if not, requires a transformation on the index so that NSFetchedResultsController knows how to handle it. ```objectivec - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { if ([self isIndexPathForFirstHeaderCell:indexPath]) { // calculate cell here return cell; } NSIndexPath *adjustedIndexPath = [self adjustedIndexPathForFetchedResultsController:indexPath]; id data = [self.fetchedResultsController objectAtIndex:adjustedIndexPath]; // calculate cell here return cell; } ``` There is one main problem with this approach: if the order of the data models backing the auxiliary cells is modified, we have to remember to recalculate the indicies. If those calculations are incorrect, the wrong data would show up for a given cell (at best) or the app would crash (at worst). In order to fix this issue, we opted to check for the class of the backing data and having the delegate methods map it to the appropriate logic. ```objectivec - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { id backingData = [self dataForIndexPath:indexPath]; class dataClass = [backingData class]; if (dataClass == [AuxiliaryData class]) { // Configure and return AuxiliaryCell } else if (dataClass == [ManagedObjectCustomClass class]) { // Configure and return managed object table view cell } } ``` This approach gives us two advantages. First, it solves the issue of index calculation not matching up with the backing data object. Checking the class allows the data binding to be index agnostic; instead of indexes determining views, the data determines the appropriate view. This became especially important in crushing bugs regarding interactivity: because the cell indexes directly match the backing data, there isn’t a chance that tapping a cell would trigger the logic for the cell above, or the cell below. Secondly, this approach eliminated a common bug where a newly added or deleted auxiliary cell caused Core Data backed cells to be visible or not. With the index calculation, it was possible that a cell previously held by a Core Data object would now have an NSIndexPath associated with an auxiliary cell (or vice-versa). By checking the class of the data, we don’t have to worry about the timing in which the indicies are updated and whether or not it matches up in time with Core Data automatic updates. Texture also comes with a rather nifty preloading system. It provides a convenient method that notifies the system when the user is n screen-lengths away from the bottom of the screen (n is defaulted to 2). The method gives a simple context object which needs to be notified when data is being fetched and when the fetching is completed. ```objectivec - (void)tableView:(ASTableView *)tableView willBeginBatchFetchWithContext:(ASBatchContext *)context { [context beginBatchFetching]; // do fetch work [context completeBatchFetching:YES]; } ``` We decided to use the NSFetchedResultsControllerDelegate along with Texture’s [ASTableNode](http://texturegroup.org/appledoc/Classes/ASTableNode.html) batchUpdate system to have this batch fetch system automatically update the upcoming cells offscreen. In order to do this properly, the context can’t be given the `completeBatchFetching:` signal until both the data fetch and the animation update has been completed. To do this, we use a single property to keep track of the current state. ```objectivec typedef NS_OPTIONS(NSUInteger, CDBFeedItemSyncMoreState) { CDBFeedItemSyncMoreStateNone = 0, CDBFeedItemSyncMoreStateRequestCompleted = 1 << 0, CDBFeedItemSyncMoreStateViewUpdateCompleted = 1 << 1, }; - (void)setSyncMoreState:(CDBFeedItemSyncMoreState)syncMoreState { BOOL requestFinished = syncMoreState & CDBFeedItemSyncMoreStateRequestCompleted; BOOL viewsUpdated = syncMoreState & CDBFeedItemSyncMoreStateViewUpdateCompleted; if (requestFinished && viewsUpdated) { if (self.batchContext) { [self.batchContext completeBatchFetching:YES]; self.batchContext = nil; } _syncMoreState = CDBFeedItemSyncMoreStateNone; } else { _syncMoreState = syncMoreState; } } ``` When the data fetch request completes, it simply sets the syncMoreState property to ```objectivec self.syncMoreState = self.syncMoreState|CDBFeedItemSyncMoreStateRequestCompleted ``` When the view update completes, it updates the syncMoreState property as well ```objectivec - (void)updateForChanges:(CDUFetchedResultsUpdateModel *)updateModel { __weak typeof(self) weakSelf = self; [self.tableNode handleFetchedResultsUpdates:updateModel completion:^(BOOL finished) { __strong typeof(weakSelf) strongSelf = weakSelf; if (weakSelf.batchContext) { weakSelf.syncMoreState = weakSelf.syncMoreState|CDBFeedItemSyncMoreStateViewUpdateCompleted; } }]; } ``` This approach allowed us to connect the existing functionality in UIKit with the performance and syntactic-sugar that Texture provides. Here is a side-by-side comparison on an iPad Gen 3 - iOS 8: However, Texture did come without its own set of challenges. In particular, Texture’s reliance on background dispatch pools created interesting threading problems with our Core Data stack. Texture tries to call init and view loading methods on background threads where possible, but often those were the same places where Core Data objects were being referenced. Because our NSFetchedResultsControllers always run on the main thread, these background dispatch pools often caused concurrency warnings when a Texture view attempted to use an [NSManagedObject](https://developer.apple.com/documentation/coredata/nsmanagedobject). To address this, we strictly enforce read-only operations on the main queue [NSManagedObjectContext](https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext). By limiting the main queue to only read-only operations, we are able to get the latest data from the NSFetchedResultsController while preventing any update/insert/delete operations that may cause instability in other parts of the app. Overall, this is the least satisfactory aspect of Texture and is an ongoing area of exploration for us. Like any library, Texture provides a series of tradeoffs. It allows us to leverage existing UIKit technology and [GCD](https://developer.apple.com/documentation/dispatch), but exposed us to an entirely new set of potential concurrency issues. However, we’ve found Texture provides a smoother scrolling experience, better performance on older devices, and allows us to have a higher velocity in developing new functionality. Overall, if your application includes an Instagram/Pinterest style feed, we encourage you try out Texture for yourself!]]>https://engineering.classdojo.com/2017/07/13/texturehttps://engineering.classdojo.com/2017/07/13/textureThu, 13 Jul 2017 00:00:00 GMT<![CDATA[Lifecycle Emails at ClassDojo (Part I)]]>https://engineering.classdojo.com/2017/06/13/lifecycle-emails-at-classdojo-ihttps://engineering.classdojo.com/2017/06/13/lifecycle-emails-at-classdojo-iTue, 13 Jun 2017 00:00:00 GMT<![CDATA[Running 4558 Tests in 1m 55sec (or Saving 50 Hours/week)]]>https://engineering.classdojo.com/2017/05/22/running-4558-tests-in-1m-55sechttps://engineering.classdojo.com/2017/05/22/running-4558-tests-in-1m-55secMon, 22 May 2017 00:00:00 GMT<![CDATA[A/B Testing From Scratch at ClassDojo]]>https://engineering.classdojo.com/2017/04/01/ab-testing-from-scratchhttps://engineering.classdojo.com/2017/04/01/ab-testing-from-scratchSat, 01 Apr 2017 00:00:00 GMT<![CDATA[Speeding Up Android Builds: Tips & Tricks]]>https://engineering.classdojo.com/2017/01/24/speeding-up-android-builds-tips-and-trickshttps://engineering.classdojo.com/2017/01/24/speeding-up-android-builds-tips-and-tricksTue, 24 Jan 2017 00:00:00 GMT<![CDATA[Integration Testing React and Redux With Mocha and Enzyme]]>https://engineering.classdojo.com/2017/01/12/integration-testing-react-reduxhttps://engineering.classdojo.com/2017/01/12/integration-testing-react-reduxThu, 12 Jan 2017 00:00:00 GMT<![CDATA[Catching React Errors With ReactTryCatchBatchingStrategy]]>https://engineering.classdojo.com/2016/12/10/catching-react-errorshttps://engineering.classdojo.com/2016/12/10/catching-react-errorsSat, 10 Dec 2016 00:00:00 GMT<![CDATA[Creating Photo Collages With Node-canvas]]>https://engineering.classdojo.com/2016/05/26/collages-with-redshift-highland-and-canvashttps://engineering.classdojo.com/2016/05/26/collages-with-redshift-highland-and-canvasThu, 26 May 2016 00:00:00 GMT<![CDATA[Better Rate Limiting With Redis Sorted Sets]]>https://engineering.classdojo.com/2015/02/06/rolling-rate-limiterhttps://engineering.classdojo.com/2015/02/06/rolling-rate-limiterFri, 06 Feb 2015 00:00:00 GMT<![CDATA[Continuous Testing at ClassDojo]]>https://engineering.classdojo.com/2015/01/20/continuous-testing-at-classdojohttps://engineering.classdojo.com/2015/01/20/continuous-testing-at-classdojoTue, 20 Jan 2015 00:00:00 GMT<![CDATA[Fs-Tail]]>https://engineering.classdojo.com/2014/12/18/fs-tailhttps://engineering.classdojo.com/2014/12/18/fs-tailThu, 18 Dec 2014 00:00:00 GMT<![CDATA[Production-Quality Node.js (Part III): Preventing Defects]]>https://engineering.classdojo.com/2014/06/23/production-quality-node-dot-js-part-iiihttps://engineering.classdojo.com/2014/06/23/production-quality-node-dot-js-part-iiiMon, 23 Jun 2014 00:00:00 GMT<![CDATA[Production-Quality Node.js (Part II): Detecting Defects]]>https://engineering.classdojo.com/2014/06/03/production-quality-node-dot-js-part-iihttps://engineering.classdojo.com/2014/06/03/production-quality-node-dot-js-part-iiTue, 03 Jun 2014 00:00:00 GMT<![CDATA[Production-Quality Node.js (Part I): The Basics]]>https://engineering.classdojo.com/2014/06/01/production-quality-node-dot-js-part-ihttps://engineering.classdojo.com/2014/06/01/production-quality-node-dot-js-part-iSun, 01 Jun 2014 00:00:00 GMT