Skip to content

Commit

Permalink
feat(recipes): apollo client cache
Browse files Browse the repository at this point in the history
  • Loading branch information
frankcalise committed Mar 26, 2024
1 parent 5459c32 commit eb9275b
Showing 1 changed file with 202 additions and 0 deletions.
202 changes: 202 additions & 0 deletions docs/recipes/ApolloClientCache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
---
title: Extracting Apollo Client's Cache in Reactotron
description: How to enhance your Ignite debugging experience when using the Apollo client with Custom Commands in Reactotron
tags:
- Apollo Client
- Cache
- Reactotron
- Custom Commands
last_update:
author: Frank Calise
publish_date: 2024-03-26
---

# Overview

This guide will teach you how to add two additional [Custom Commands](https://docs.infinite.red/reactotron/custom-commands/) for use within [Reactotron](https://docs.infinite.red/reactotron/) when using Ignite alongside the [Apollo Client](https://www.apollographql.com/docs/react/) library.

# Prerequisites

You'll need the following to get going with this recipe:

- An Ignite project with Reactotron configured (this is done for you)
- Configured with an Apollo Client pointed at a GraphQL backend

## Install Commands

```bash
npx ignite-cli@latest ignite-apollo-cmds --yes
cd ignite-apollo-cmds
npx expo install @apollo/client graphql
mkdir app/stores/apollo
touch app/stores/apollo/index.tsx
```

## Quick Apollo Client Setup

Open up `app/stores/apollo/index.tsx` and initialize your Apollo Client, feel free to customize this to your liking:

```tsx
// app/stores/apollo/index.tsx
import { ApolloClient, InMemoryCache } from "@apollo/client";

const cache = new InMemoryCache();

export const client = new ApolloClient({
uri: "https://api.graphql.guide/graphql",
cache,
defaultOptions: {
watchQuery: { fetchPolicy: "cache-and-network" },
},
});
```

Now we need to pass this client into the provider at the root app level, so open `app/app.tsx` and wrap the return value that is already there:

```tsx
// success-line-start
import { ApolloProvider } from "@apollo/client";
import { client as apolloClient } from "app/stores/apollo";
// success-line-end

// ...

return (
// success-line
<ApolloProvider client={apolloClient}>
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<ErrorBoundary catchErrors={Config.catchErrors}>
<GestureHandlerRootView style={$container}>
<AppNavigator
linking={linking}
initialState={initialNavigationState}
onStateChange={onNavigationStateChange}
/>
</GestureHandlerRootView>
</ErrorBoundary>
</SafeAreaProvider>
// success-line
</ApolloProvider>
);
```

# Reactotron Config

We'll be adding two additional commands to our Reactotron setup.

1. Extract an entire snapshot of the current cache and display it to the timeline
2. Extract a specific key of the current cache and display it to the timeline

Both of these modifications will be added to `app/devtools/ReactotronConfig.ts`. Before we add these, we'll need access to our client - so add the following import in that file: `import { client as apolloClient } from "../stores/apollo"`

## Cache Snapshot

Somewhere after the `Reactotron.configure` statement (below or above the existing custom commands will work fine), add the following code

```tsx
reactotron.onCustomCommand({
title: "Extract Apollo Client Cache",
description: "Gets the updated InMemory cache from Apollo Client",
command: "extractApolloCache",
handler: () => {
Reactotron.display({
name: "Apollo Cache",
preview: "Cache Snapshot",
value: client.cache.extract(),
});
},
});
```

Now if we look at our Reactotron window, you'll see under the Custom Commands we have a new button! Press that and flip back to the timeline. You'll see we have a new list item that we can tap on and see the value of the in-memory cache from the Apollo Client

TODO insert screenshot here

## Cache by Key

Quite often, though, you're in-memory cache could be quite large and maybe you're not that interested in all the data. We can create another command that will do a look up specifically by the key we pass into the Reactotron UI.

To do this, we'll utilize the `args` property to allow our command to take in a string. We'll then get access to that value in the `handler` callback, which we can use however we wish.

Let's plan this out:

1. Upon press, make sure the user filled out a key, if not we'll log an error to the Timeline
2. Extract the cache and look for the requested key
a. if it doesn't exist, log an error to the Timeline
b. if it does exist, return the value

To make this easier, we'll first create a helper function to extract a specific key path from the cache. This will allow us to request a key in some nested object, for example if we had the following:

```json
{
"parent": {
"child": {
"someProp": 5
}
}
}
```

We could directly request the value `parent.child.someProp` to be logged out via this Custom Command. Here's a helper function that'll get you started, customize it how you like! This one will be able to access array value via their index in addition to a key directly.

```tsx
function getNestedCacheValue(keyPath: string): any {
// Extract the entire cache
const cache: NormalizedCacheObject = client.cache.extract();

// Define a regular expression to match keys and array accessors
const pathSegmentRegex = /[^.[\]]+|\[\d+\]/g;

// Extract path segments, including array indices
const pathSegments = keyPath.match(pathSegmentRegex) || [];

// Navigate through the path segments to get to the desired value
const value = pathSegments.reduce((acc, segment) => {
// Check if the segment is an array accessor, e.g., [1]
if (segment.startsWith("[") && segment.endsWith("]")) {
// Extract the index from the segment and convert it to a number
const index = parseInt(segment.slice(1, -1), 10);
return acc ? acc[index] : undefined;
}
// Handle normal object property access
return acc ? acc[segment] : undefined;
}, cache);

return value ?? null; // Return null if the value is undefined at any point
}
```

With that in place, we can set up our new Custom Command:

```tsx
reactotron.onCustomCommand({
title: "Extract Apollo Client Cache by Key",
description: "Retrieves a specific key from the Apollo Client cache",
command: "extractApolloCacheByKey",
args: [{ name: "key", type: ArgType.String }],
handler: (args) => {
const { key } = args ?? {};
if (key) {
const findValue = getNestedCacheValue(key);
if (findValue) {
Reactotron.display({
name: "Apollo Cache",
preview: `Cache Value for Key: ${key}`,
value: findValue,
});
} else {
Reactotron.display({
name: "Apollo Cache",
preview: `Value not available for key: ${key}`,
});
}
} else {
Reactotron.log("Could not extract cache value. No key provided.");
}
},
});
```

TODO insert screenshot here

Head back to your Reactotron UI and you'll see the Custom Command at the bottom (or top, depending on how you register in your config) available for use. You can now extract values from more complex key paths such as `ROOT_QUERY.chapter({"id":1}).sections[2]` rather than having to traverse the entire object.

0 comments on commit eb9275b

Please sign in to comment.