Skip to main content

Overview

This guide will teach you how to add two additional Custom Commands for use within Reactotron when using Ignite alongside the Apollo Client 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

npx ignite-cli@latest new 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:

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:

app/app.tsx
import { ApolloProvider } from "@apollo/client";
import { client as apolloClient } from "app/stores/apollo";

// ...

return (
<ApolloProvider client={apolloClient}>
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<ErrorBoundary catchErrors={Config.catchErrors}>
<GestureHandlerRootView style={$container}>
<AppNavigator
linking={linking}
initialState={initialNavigationState}
onStateChange={onNavigationStateChange}
/>
</GestureHandlerRootView>
</ErrorBoundary>
</SafeAreaProvider>
</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

app/devtools/ReactotronConfig.ts
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: apolloClient.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

Cache Snapshot Custom Command

Cache Snapshot Timeline Collapsed

Cache Snapshot Timeline Expanded

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:

{
"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.

app/devtools/ReactogronConfig.ts
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:

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.");
}
},
});

Cache By Key Custom Command

Cache By Key Timeline Collapsed

Cache By Key Timeline Expanded

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.

Is this page still up to date? Did it work for you?