DEV Community

Ankita Kulkarni
Ankita Kulkarni

Posted on • Originally published at Medium on

Oh Hello Apollo Client , Goodbye Redux!

I know I got excited with the title there but it is kinda true ๐Ÿ˜…. In this blog post, I will cover why your switch to GQL and Apollo Client 3 should make you walk away from Redux. I will also talk about my journey from Redux -> Apollo Client.

I have had my share of doubts so in the past couple of projects, I was really skeptical of using Apollo Client as a state management solution. I love โค Apollo and specifically the changes made in Apollo client 3 that changed my mind completely ๐Ÿ˜ป

Why I like Redux and what it is good at ๐Ÿ’™

  • Global state management solution where you have a good visual of your entire state
  • You use actions to trigger state updates and asynchronous requests (love ๐Ÿ’Œ my boo: Redux saga ๐Ÿ”—)
  • The entire ecosystem is amazing, you get Redux time travel too for debugging! โฒ
  • You can use libraries like Redux selectors (another awesome library ๐Ÿ”—) to select data from the state and transform

which brings me to my next pointโ€ฆ ๐Ÿ‘‡

What is considered a good state management solution? โœ…

  1. My data is normalized (no dupes please ๐Ÿ™)
  2. Specific actions i.e. user logging in / routing should be able to trigger asynchronous requests ๐Ÿ’ฏ
  3. We want to transform the data so that our component is not huge and we can write tests!! ๐Ÿต
  4. Lastly, visualize the store i.e. we can view our global state and debug easily ๐ŸŒŽ

and Iโ€™m sure there are more but the above were the top ones in my list! ๐Ÿฅ‡

After I started using GQL โœจ

  • I didnโ€™t use Redux in the GQL project because we were using React Hooks and React Context and it felt repetitive because you can use useReducer and useContext where you can dispatch actions and update state
  • Apollo Client exposes custom hooks โš“๏ธ like useQuery, useMutation which automatically exposed loading, success and error states so I didnโ€™t need to trigger 3 different actions in my redux store i.e. CART_REQUEST, CART_SUCCESS and CART_ERROR. For example, here is a comparison โšก๏ธ

A lot of boilerplate code has reduced ๐Ÿ˜ˆ You get the loading, success, and error states right from the useQuery and useMutation hook.

So what was missing? ๐Ÿ˜…

Going back to the definition of a good state management library ๐Ÿ‘†

  • I really loved useQuery and useMutation custom hooks although I wasnโ€™t fully convinced to switch for state management completely as I really liked using Redux selectors that select data & we have the ability to transform it ๐Ÿ˜ฅ
  • In the meanwhile, I was using React Context instead of Redux
  • I also didnโ€™t want to read the Apollo cache all the time
  • At the time, there was no way to store values outside the cache
  • I also wanted actions to trigger asynchronous requests like Redux sagaโ€™s do ๐Ÿšถโ€โ™€
  • On top of this, I found Apollo client cache really hard to read ๐Ÿ˜ซ

But with Apollo Client 3, they introduced Reactive Variables and local only fields that changed everything ๐Ÿ’–

Apollo Client 3 gives us 2 really cool things ๐Ÿ˜Ž

  1. Local only fields
  2. Reactive Variables

They are fields that resolve on the client side itself by reading data from the cache if you want thus replacing the transformers in Redux. Letโ€™s take a look how that would work.

My data is normalized (no dupes please ๐Ÿ™)

Apollo Client takes care of the heavy lifting for you ๐Ÿ’ช. You donโ€™t need to constantly dispatch actions to change state. With redux, we were really used to that and the benefit there is you have full control although do we really need full control? ๐Ÿ˜ถ

You are already using GQL โค๏ธ so everything is a graph ๐Ÿ“ˆ and is stored in the graph i.e. you already have all your data in your cache then why add Redux on top to duplicate it? ๐Ÿคทโ€โ™€ You are going to add more overhead ๐Ÿ™ˆ

Apollo Client automatically caches your data and normalizes new data in query responses and after mutation. Similar to what you would do in Redux where you would need to make sure that your data is normalized. If you are onboarding a new developer, itโ€™s hard because they also need to consider and learn how to do this on an architecture level which adds more overhead.

Visualization of the Apollo client cache with cart data in it
How the cache looks like

Apollo client stores data using references so it can be smart by looking it up easily using that reference as a key. Here is an awesome blog post ๐Ÿ”— written by Khalil Stemmler on Demystifying Apollo Cache which you should read before switching to AC3 for state management. ๐Ÿ’ฏ

Data transformations ๐Ÿ˜„

We want data transformations in an application so there is a clear separation of side-effect to transform layer. This way we can make sure that our component file is not huge and we can write tests for those transformers

https://medium.com/media/bcb60b1b989a751e19eb3c6117889e25/href

We will use local only fields mainly for transforming data.

1. Local only fields ๐ŸŒผ

Local only fields is a way we can define client side fields on the GQL type that doesnโ€™t need to come from the server. You can resolve them locally on your frontend.

Letโ€™s say we have the following query for getting the userโ€™s cart โšก

Here is how your cart query data object from the above query looks like ๐Ÿ‘ˆ

Letโ€™s say we have this user story, ๐Ÿ’„

As a user, I want to see if an item is out of stock or low stock based on the items in my cart.๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ป

Here is how your React component might look like for it without using the Apollo client side variable: ๐Ÿ’„ โšก๏ธ

Typically in Redux, we would extract the logic of the function getTextForLowOrOutOfStock outside using a redux selector. ๐Ÿ†—

With AC3, you can achieve the above by reading the cache and adding the string for โ€˜out of stockโ€™ and โ€˜low stockโ€™ accordingly within your client itself.

OK But, how can we use local only fields? ๐Ÿค”

We can create local only fields on the Cart type with the @client directive. ๐ŸŽ‰ For example, โšก๏ธ here stockText is the client side field.

With the @client directive, Apollo client will look into the cache to resolve the field. It wonโ€™t make a call over the network for that field because of the directive. Now stockText can be accessed anytime we declare a Cart type because it is a field on the Cart type.

Now we can directly access stockText in our React component by doing the following โšก๏ธ

2. Reactive Variables ๐ŸŒธ

We can also create custom client side values stored outside the cache known as Reactive Variables. Sometimes we just want to create a field outside of the type structure which can still be accessed through the Apollo client globally. For that, Apollo client gives us Reactive variables.

Modifying a reactive variable triggers an update of every active query that depends on that variable, as well an update of the react state associated with any variable values returned from the useReactiveVar React hook.

Reactive variables donโ€™t update the cache but store the state information that we want to access at any point in our application. In Redux, we usually dispatch an action to store such a value in the store.

Letโ€™s say we have this user story, ๐Ÿ’„

As a user, I want to view the number of items in my cart that are on sale. ๐Ÿ’ฏ ๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ป

For this you would have to read all the items in cart, check items that are on sale and count them. With Apollo client, you can achieve this by ๐Ÿ‘‡ โšก๏ธ

You can do way more than this. You can also access existing fields (i.e. readNumberOfOOSItems) through other fields as well. ๐Ÿ™Œ

You can access the above readNumberOfOOSItems via a query as well which gives you loading, data and error states:

But wait, what is the difference between local only fields and reactive variables? ๐Ÿค”

In a local only field, you create a new field on the type itself i.e. from our example, we created stockText on the Cart type i.e. you canโ€™t access stockText anywhere else.

But for reactive variables, you can access it anywhere you like and it isnโ€™t restricted to a specific type. Unlike the Apollo Client cache, reactive variables donโ€™t enforce data normalization, meaning you can store data in any format you want. ๐Ÿคฏ

noice

Specific actions should trigger asynchronous requests โฉ

Once we do retrieve data or if the user wants to route based on certain information from the server, we might want to trigger asynchronous requests or rather specific actions that the user should take.

Letโ€™s say we have this user story, ๐Ÿ’„

As a user, I want to be taken to the sign in page if Iโ€™m not logged in else I want to see the app page

Here we want to track if the user is logged in or not and accordingly route the user. We can achieve this by creating a reactive variable for it.

Reactive variables are variables stored in the client and outside the cache but the components can also access their values directly. In the example below, isUserLoggedIn is a reactive variable that has been created using makeVar function. It invokes the function to check if there is a token in the browser Cookies๐Ÿช. (In the real world, we will obviously check for token expiry as well ๐Ÿ˜‰).

Anything under fields is a field policy. A field policy is basically a contract between client and the function which dictates how that field is going to be resolved. We have a field policy to read the number of out of stock items and check if the user is logged in or not.

Next, in order to access this value within the component, we can do the following โšก๏ธ

The above will re-render whenever the value changes for isUserLoggedInVar

If you want to trigger an API request once the user has logged in, you can achieve this by listening to isUserLoggedIn in a useEffect. ๐Ÿ‘ˆ

Therefore, we can trigger async requests based on whatโ€™s in the state.

But wait, can I update the value of the Reactive variable? ๐Ÿค”

Yes you can! We can update the value of the reactive variable anywhere in our application, for example if we wanted to update the value of isUserLoggedInVar to false or anything else, we can! We just need to invoke the function isUserLoggedInVar directly!

Visualize store / cache ๐Ÿ”ฎ

Lastly, visualize the store i.e. we can view our global state and debug easily

Just like Redux developer tools, Apollo client also have their developer tools, here is a link. ๐Ÿ”— Initially, I had some difficulty visualizing the cache as the Apollo developer tools are not as mature as Redux developer tools.

But after understanding how Apollo client stores data internally and how it optimizes it, things got a lot easier. I am able to visualize the cache. ๐Ÿ˜„

In the Queries and Mutation tab, you will see a list of Queries and Mutations executed in your application (just like Redux does). In the cache tab, you will see the entire cache i.e. your Root query along with the cache references that got updated.

You can use GraphiQL to query anything (including Reactive variables) just like you would in the GQL playground. But if you want to query Reactive variables , make sure to check the checkbox โ€œLoad from cacheโ€.

I find that Redux dev tools are superior with time travel although once you learn how the cache looks like and how it takes care of the heavy lifting for you, it will get a lot simpler. But, I would say this is definitely a pain point of Apollo client dev tools overall ๐Ÿค•.

Lastly, keep an open mind

https://medium.com/media/7f446247325b2b814408d4727aaf4695/href

  • The difference between Redux and Apollo Client is that you either take control and do everything on your own (like Redux) or let a mature library like Apollo Client handle that for you ๐Ÿ™Œ
  • Donโ€™t get me wrong, I do love control ๐Ÿ˜‚. but Apollo client is taking care of the bulk of the work for you so you can focus on the core of your application
  • I kept comparing Apollo client to Redux 1:1 and although it was great to help me understand how my app would scale, this was also a reason I was holding back because now I have to unlearn what I have learned and trust that Apollo client will take care of it for you. ๐Ÿ‘Œ
  • When you are using Apollo client, it does feel redundant to use Redux on top of it as you are now keeping 2 copies of the same data i.e. Apollo client cache and Redux global store. ๐Ÿ™ˆ
  • The more you learn about the cache, the more you start to love it! โค๏ธ

Thank you for making it so far, hope you found this post useful ๐Ÿ’ฏ and it helps you draw comparisons between Redux and Apollo Client. ๐Ÿ™Œ

Top comments (10)

Collapse
 
aspirisen profile image
Dmitrii Pikulin

Nice article!
One note about Here is how your React component might look like for it without using the Apollo client side variable:
You can simplify the code by using useMemo and include data in deps, so you don't need effect here

Collapse
 
darrylwolfaardt profile image
DarrylW

great summary, thanks

Collapse
 
kauly profile image
Kauly

Its gonna be hard to maintain the cache object organized when the app scales. Nevertheless, I really like of 1:1 comparsion, thank you

Collapse
 
sergelerner profile image
Serge Lerner

Great piece!

I've switched from Redux to Apollo myself some time ago, and like it quite much! But recently with Apollo 3 I've stumbled upon a use case where in my local-only field I'd wish to return multiple reactive vars. In other words: keep my local-only fields minimal, and derive data from multiple reactive vars.

For example:

    something: {
      read() {
        return {
          a: aVar(),
          b: bVar()
        };
      }
    },
Enter fullscreen mode Exit fullscreen mode

The issue with this is that it creates a new reference on every read, therefore makes it hard working with React, since it always fails on shallow comparison based on reference when used with useEffect dependency array for example.

When I used to work with Redux, where you are also encouraged to keep your store state minimal, and derive data from the state as needed, you have the same issue. But there was another layer, a lib called Reselect, which memoized all your derived state. Hence passes a stable reference to React, until one of its memoization function inputs changed.

After some thought. Where I considered both various deep compare solutions and memoization at different stages. I ended up introducing Reselect to my Apollo 3 workflow, now I can derive data from state at ease and not worry about references and shallow comparison, plus optimize for performance. The downside its quiet verboseโ€ฆ but looks like its either you write more code or more code runs (with some performance impact) on your behalf ๐Ÿ™ƒ

If you have any other experience with such use case, I would love to hear!

Collapse
 
filipleonard profile image
FilipLeonard • Edited

Hi @sergelerner! Do you have an update on using Apollo Client 3 + Reselect? Does it scale well?

I am in the process of migrating state management from Redux to Apollo and noticed the following:

const store = makeVar({ age: 42, planet: "Earth"});

function someComponent () {
  const { age } = useReactiveVar(store);

  return <h1>Jane is {age} years old</h1>;
}

// this does not trigger a rerender
store({ age: 42, planet: "Earth"});

// this triggers a rerender, as expected
store({ age: 43, planet: "Earth"});

// this also triggers a rerender, not ideal since I don't use planet in the component. (Assume age has still the initial value of 42).
store({ age: 42, planet: "Mars"});
Enter fullscreen mode Exit fullscreen mode

I understand I can use Reselect somehow to solve this unnecessary rerendering but then if I use Reselct, and also if I bring in Immer to be able to mutate state directly, I'm wondering, is it really worth it to switch from Redux to AC3? Thanks!

Collapse
 
sylvainbaronnet profile image
Sylvain Baronnet

I recommend you checking redux-requests.klisiczynski.com/ It simplifies things like Apollo does while letting you take control if needed (adding more actions / reducers, using saga on different request states, etc)
One advantage is that it works well with Rest API and Graphql (I know Apollo support Rest but it's not great IMO)

Collapse
 
tiavinamika profile image
TiavinaMichael

thanks, great article :)

Collapse
 
kulkarniankita9 profile image
Ankita Kulkarni

Awesome, thank you! Glad you liked it :)

Collapse
 
ricardopaul profile image
Ricardo Paul

What a good article!! Thank you

Collapse
 
kulkarniankita9 profile image
Ankita Kulkarni

Thank you, glad you liked it! :)