Can I change other pieces of state in a Recoil Atom Effect

The Situation

I am using Recoil to manage state in a React application. I am using Atom Effects to back up recoil values with an API.

When my Atom detects something wrong with the authentication token I would like it to somehow convey that issue to the rest of the application so that the application can update state.

The Details

I have several Atoms:

  1. An atom which stores an authentication token, for use when communicating with the storage API.
  2. Atoms for various pieces of the application which I would like to save on that storage API.

In order to perform the API synchronization I have an apiStorageEffect Atom Effect. The effect uses a recoil-stored authentication token using getPromise and forwards changes using an onSet handler. That handler uses the token when calling the save method. The save method can detect when authentication has failed.

When authentication fails I need to trigger a log out event (setting the token to null, and resetting a series of atoms to their defaults).

Here is a minimal version of the storage effect:

const apiStorageEffect = (key) => async ({
    onSet,
    getPromise,
}) => {
    const authToken = await getPromise(authTokenAtom)
    if (!authToken) { return }

    onSet(async (newValue) => {
        const result = await saveToApiStorage(key, newValue, authToken)
        if (result.failed) {
            // INSERT CODE TO TRIGGER LOGOUT HERE
        }
    })
}

The Question

I can think of a few possible paths for triggering logout from within an effect, but all of them involve somehow sending information outside of the affected atom to the main application.

  1. Is it possible to modify atoms OTHER than the atom / node in question from within an atom effect?
  2. Is it possible for an atom effect to emit an event that can be immediately handled elsewhere in the React application?
  3. Is it possible for an atom effect to invoke a method that can modify recoil state more broadly?

Atom effects are still pretty new (currently unstable).

The behavior that you desire would naturally be accomplished by subscribing to all of the related atoms via a selector, and processing the effect there... however, selector effects don't yet exist.

The most straightforward way to do this now is to convert your effect into a component which performs the operation using the effect hook (explained in the recoil docs). That component would then be placed underneath the RecoilRoot providing the recoil state. Here's an example based on the information in your question:

TS Playground

import {useCallback, useEffect} from 'react';
import {atom, RecoilRoot, useRecoilValue, useResetRecoilState} from 'recoil';

declare function saveToApiStorage (
  key: string,
  data: { value1: number; value2: number; },
  authToken: string,
): Promise<{failed: boolean}>;

export const authTokenState = atom<string | null>({
  key: 'authToken',
  default: null,
});

// the key used in your effect
export const keyState = atom({key: 'key', default: ''});
// some bit of state for the API
export const value1State = atom({key: 'value1', default: 0});
// another one
export const value2State = atom({key: 'value2', default: 0});

// this is the converted effect component
export function ApiStorageEffect (): null {
  const key = useRecoilValue(keyState);
  const authToken = useRecoilValue(authTokenState);
  const value1 = useRecoilValue(value1State);
  const value2 = useRecoilValue(value2State);
  const resetAuthToken = useResetRecoilState(authTokenState);
  const resetValue1 = useResetRecoilState(value1State);
  const resetValue2 = useResetRecoilState(value2State);

  const updateStorage = useCallback(async () => {
    if (!authToken) return;

    const result = await saveToApiStorage(key, {value1, value2}, authToken);

    if (result.failed) {
      resetAuthToken();
      resetValue1();
      resetValue2();
    }
  }, [
    authToken,
    key,
    resetAuthToken,
    resetValue1,
    resetValue2,
    value1,
    value2,
  ]);

  // use void operator for correct return type
  useEffect(() => void updateStorage(), [updateStorage]);
  return null;
}

// include underneath the recoil root
function App () {
  return (
    <RecoilRoot>
      <ApiStorageEffect />
      {/* rest of app... */}
    </RecoilRoot>
  );
}