Open delyanr opened 3 years ago
We use a reactive variable as a complex state object. It would be really useful to have a way to react to changes in a particular part of this data.
Reaction to changes in some bits of the data state is also a problem for React Context value. That's why the React core team is working on selectors as well with a similar API: https://github.com/facebook/react/pull/20646 https://github.com/reactjs/rfcs/pull/119
I hope, we will see something like that in Apollo Client for React as well.
Didn't actually see this prior to creating another proposal which is related.
Issue in question: #305
I'm really interested in a feature like this, too. I think this would be the answer to the question I just posted here.
I would also find this feature useful. For my specific needs for now, I implemented a custom version shown below that is inspired by react-redux useSelector hook:
useReactiveVarWithSelector.ts
import { ReactiveVar } from '@apollo/client';
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector';
function useReactiveVarWithSelector<T, Selection>(
rv: ReactiveVar<T>,
selector: (state: T) => Selection,
isEqual?: (a: Selection, b: Selection) => boolean
): Selection {
const value = useSyncExternalStoreWithSelector(
(onStoreChange: () => void) => {
let unsubscribe: () => void;
const listener = () => {
// Notify parent listener that the variable has changed
onStoreChange();
// When variable value is changed, apollo-client notifies all the listeners
// and then clears all the listeners. Thus, we need to resubscribe to the
// next variable value change.
// See https://github.com/apollographql/apollo-client/blob/main/src/cache/inmemory/reactiveVars.ts
unsubscribe = rv.onNextChange(listener);
};
unsubscribe = rv.onNextChange(listener);
return () => unsubscribe();
},
rv,
rv,
selector,
isEqual
);
return value;
}
export default useReactiveVarWithSelector;
The hook leverages the useSyncExternalStoreWithSelector from React's own package use-sync-external-store. The subscribe parameter passed to the useSyncExternalStoreWithSelector
uses apollo-client ReactiveVar's onNextChange
subscribe method (see https://github.com/apollographql/apollo-client/blob/main/src/cache/inmemory/reactiveVars.ts#L51) that works so that when the ReactiveVar's value changes, the subscribers are notified and then the subscriptions are cleared. Hence the need to resubscribe the listener on every change to the variable.
Just a heads up that vilikets code above crashes when running on the server (ie SSR). I replaced the ùndefinedparameter with
rv´ to get around it but don't really understand the consequences of it.
@Paso Good notice, I had not tested the code with SSR and left the third parameter as undefined
. According to React's documentation on useSyncExternalStore, the third parameter getServerSnapshot
should be set as the function that returns the snapshot used during server side rendering. See the provided example there:
When server rendering, you must serialize the store value used on the server, and provide it to useSyncExternalStore. React will use this snapshot during hydration to prevent server mismatches:
const selectedField = useSyncExternalStore( store.subscribe, () => store.getSnapshot().selectedField, () => INITIAL_SERVER_SNAPSHOT.selectedField, );
In the context of apollo-client's reactive variables, it should be fine to just set this parameter as the reactive variable itself (i.e., rv
in my original comment) so that during the SSR the server snapshot would simply be the (initial) value of the reactive variable. I also updated the provided code to take this into account.
Hello!
I would like to request the addition of an argument (or two) to the
useReactiveVar
hook, that can be used to specify a subset of selected values, where if the values in the subset do not change, the component will bail out of re-rendering, even if the entire set of values does change. This is similar to many other existing APIs in redux-land, for example. Proposed API (the third argument is an optional function to assert equality):The above can currently be achieved using the
useQuery
and@client
directive combination, but usually requires multiplegql
schema definitions, which is unnecessarily verbose and cluttering, when used across many components. Alternatively, the various values can be defined using multiple reactive vars, but then we lose the benefits of co-location.This could also easily be implemented as a brand new hook as well -
useReactiveVarSelector
or similar.Below is a super simple example just to avoid confusion.
Consider when you want to co-locate some global configuration into one central reactive variable:
If a component uses
useReactiveVar
to getfield1
, the component would need to re-render even iffield2
and/orfield3
change, which is not ideal:This can be mitigated with:
However, it would be a lot more cleaner to be able to use:
Thanks.