Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement reactive variables for tracking local state. #5799

Merged
merged 2 commits into from
Jan 17, 2020

Conversation

benjamn
Copy link
Member

@benjamn benjamn commented Jan 16, 2020

Reactive local variables are functions that allow you to store local state in a well-known, private location, outside of the cache, consume that state while reading queries from the cache, and update the state whenever you like.

Basic usage:

const lv = cache.makeLocalVar(123)
console.log(lv()) // 123
console.log(lv(lv() + 1)) // 124
console.log(lv()) // 124
lv("asdf") // TS type error

TypeScript infers the value type of the variable based on the initial value, which is why switching from a number to a string is a type error here.

Any parts of any queries that relied upon the value of the variable will be invalidated when/if the variable's value is updated:

const cache: InMemoryCache = new InMemoryCache({
  typePolicies: {
    Person: {
      fields: {
        name() {
          return nameVar();
        },
      },
    },
  },
});

const nameVar = cache.makeLocalVar("Ben");

const query = gql`
  query {
    onCall {
      name @client
    }
  }
`;

cache.writeQuery({
  query,
  data: {
    onCall: {
      __typename: "Person",
    },
  },
});

expect(cache.readQuery({ query })).toEqual({
  onCall: {
    __typename: "Person",
    name: "Ben",
  },
});

nameVar("Hugh");

expect(cache.readQuery({ query })).toEqual({
  onCall: {
    __typename: "Person",
    name: "Hugh",
  },
});

With this new primitive available, it should not be necessary to construct awkward queries just to read and write local state, since you can read the variable directly with nameVar() and update its value by calling nameVar(newValue).

Local variables are functions allow you to store local state in a
well-known, private location, an update it whenever you like:

  const lv = cache.makeLocalVar(123)
  console.log(lv()) // 123
  console.log(lv(lv() + 1)) // 124
  console.log(lv()) // 124
  lv("asdf") // type error

Any parts of any queries that relied upon the value of the variable will
be invalidated when/if the variable's value is updated.
Comment on lines +302 to +314
public makeLocalVar<T>(value: T): LocalVar<T> {
return function LocalVar(newValue) {
if (arguments.length > 0) {
if (value !== newValue) {
value = newValue;
localVarDep.dirty(LocalVar);
}
} else {
localVarDep(LocalVar);
}
return value;
};
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty proud of how short this implementation can be, thanks to our existing dependency tracking machinery.

Copy link
Member

@hwillson hwillson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yay optimism!! Thanks for working on this @benjamn - looks great!

@benjamn benjamn merged commit a8288e0 into master Jan 17, 2020
@benjamn benjamn deleted the reactive-local-state-variables branch January 17, 2020 15:26
@d-asensio
Copy link

Thanks @benjamn! this really makes the difference!

I made a simple choice list example in a code sandbox to test that feature and it looks amazing:

Before reactive variables (using two different approaches to achieve the desired behavior):
https://codesandbox.io/s/apolloclient-3-local-state-management-before-reactive-variables-lngfo

After reactive variables:
https://codesandbox.io/s/apolloclient-3-local-state-management-with-reactive-variables-w2uxk

I will seriously considerate to switch from redux to apollo ;)

benjamn added a commit that referenced this pull request Feb 21, 2020
Shorter, and not as suggestive of Apollo Client 2.x local state. Returns a
function of type ReactiveVar<T> (f.k.a. LocalVar<T>).

Follow-up to #5799.
@rektide
Copy link

rektide commented Jun 12, 2020

As an old-hand users & fan of the Flyd "minimalistic but powerful, modular, functional reactive programming library", I feel very at home & used to this! Flyd has a similar api, where myVar() returns the variable and myVar(42) updates the value of it. It was neat seeing this api pattern again! Thought I'd drop by & mention the similarity. Cheers, thanks for the hard work all.

benjamn added a commit that referenced this pull request Jun 30, 2020
The makeVar method was originally attached to InMemoryCache so that we
could call cache.broadcastWatches() whenever the variable was updated.
See #5799 and #5976 for background.

However, as a number of developers have reported, requiring access to an
InMemoryCache to create a ReactiveVar can be awkward, since the code that
calls makeVar may not be colocated with the code that creates the cache,
and it is often desirable to create and initialize reactive variables
before the cache has been created.

As this commit shows, the ReactiveVar function can infer the current
InMemoryCache from a contextual Slot, when called without arguments (that
is, when reading the variable). When the variable is updated (by passing a
new value to the ReactiveVar function), any caches that previously read
the variable will be notified of the update. Since this logic happens at
variable access time rather than variable creation time, makeVar can be a
free-floating global function, importable directly from @apollo/client.

This new system allows the variable to become associated with any number
of InMemoryCache instances, whereas previously a given variable was only
ever associated with one InMemoryCache. Note: when I say "any number" I
very much mean to include zero, since a ReactiveVar that has not been
associated with any caches yet can still be used as a container, and will
not trigger any broadcasts when updated.

The Slot class that makes this all work may seem like magic, but we have
been using it ever since Apollo Client 2.5 (#3394, via the optimism
library), so it has been amply battle-tested. This magic works.
@benjamn
Copy link
Member Author

benjamn commented Jun 30, 2020

Just a quick note that we've made this API even easier to use (makeVar is just a free-floating function now): #6512

benjamn added a commit that referenced this pull request Jun 30, 2020
…6512)

The makeVar method was originally attached to InMemoryCache so that we
could call cache.broadcastWatches() whenever the variable was updated.
See #5799 and #5976 for background.

However, as a number of developers have reported, requiring access to an
InMemoryCache to create a ReactiveVar can be awkward, since the code that
calls makeVar may not be colocated with the code that creates the cache,
and it is often desirable to create and initialize reactive variables
before the cache has been created.

As this commit shows, the ReactiveVar function can infer the current
InMemoryCache from a contextual Slot, when called without arguments (that
is, when reading the variable). When the variable is updated (by passing a
new value to the ReactiveVar function), any caches that previously read
the variable will be notified of the update. Since this logic happens at
variable access time rather than variable creation time, makeVar can be a
free-floating global function, importable directly from @apollo/client.

This new system allows the variable to become associated with any number
of InMemoryCache instances, whereas previously a given variable was only
ever associated with one InMemoryCache. Note: when I say "any number" I
very much mean to include zero, since a ReactiveVar that has not been
associated with any caches yet can still be used as a container, and will
not trigger any broadcasts when updated.

The Slot class that makes this all work may seem like magic, but we have
been using it ever since Apollo Client 2.5 (#3394, via the optimism
library), so it has been amply battle-tested. This magic works.
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 16, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants