0

I am trying to figure out how to solve the following problem in the best way possible:

I have multiple components all requiring a global state (I am using recoil for this, since I have many different "atom" states). Only if a component gets loaded that requires that state, it will perform an expensive computation that fetches the data. This should happen only once upon initialisation. Other components that require the same piece of data should not re-trigger the data fetching, unless they explicitly call an updateState function.

Ideally, my implementation would look something like this:

const initialState = {
  uri: '',
  balance: '0',
};

const fetchExpensiveState = () => {
  uri: await fetchURI(),
  balance: await fetchBalance(),
});

const MyExpensiveData = atom({
  key: 'expensiveData',
  default: initialState,
  updateState: fetchExpensiveState,
});

function Component1() {
  const data = useRecoilMemo(MyExpensiveData); // triggers `fetchExpensiveState` upon first call
  return ...
}

function Component2() {
  const data = useRecoilMemo(MyExpensiveData); // doesn't trigger `fetchExpensiveState` a second time
  return ...
}

I could solve this by using useRecoilState and additional variables in the context that tell me whether this has been initialised already, like so:

export function useExpensiveState() {
  const [context, setContext] = useRecoilState(MyExpensiveData);

  const updateState = useCallback(async () => {
    setContext({...fetchExpensiveState(), loaded: true});
  }, []);

  useEffect(() => {
    if (!context.loaded) {
      setContext({...context, loaded: true});
      updateState();
    }
  }, []);

  return { ...context, updateState };
}

It would be possible to make this solution more elegant (not mixing loaded with the state for example). Although, because this should be imo essential and basic, it seems as though I'm missing some solution that I haven't come across yet.

phaze
  • 152
  • 7

1 Answers1

0

I solved it first by using a loaded and loading state using more useRecoilStates. However, when mounting components, that had other components as children, that all used the same state, I realized that using recoil's state would not work, since the update is only performed on the next tick. Thus, I chose to use globally scoped dictionaries instead (which might not look pretty, but works perfectly fine for this use case).

Full code, in case anyone stumbles upon a problem like this.

useContractState.js

import { useWeb3React } from '@web3-react/core';
import { useEffect, useState } from 'react';
import { atomFamily, useRecoilState } from 'recoil';

const contractState = atomFamily({
  key: 'ContractState',
  default: {},
});

var contractStateInitialized = {};
var contractStateLoading = {};

export function useContractState(key, fetchState, initialState, initializer) {
  const [state, setState] = useRecoilState(contractState(key));
  const [componentDidMount, setComponentMounting] = useState(false);

  const { library } = useWeb3React();

  const provider = library?.provider;

  const updateState = () => {
    fetchState()
      .then(setState)
      .then(() => {
        contractStateInitialized[key] = true;
        contractStateLoading[key] = false;
      });
  };

  useEffect(() => {
    // ensures that this will only be called once per init or per provider update
    // doesn't re-trigger when a component re-mounts
    if (provider != undefined && !contractStateLoading[key] && (componentDidMount || !contractStateInitialized[key])) {
      console.log('running expensive fetch:', key);
      contractStateLoading[key] = true;
      if (initializer != undefined) initializer();
      updateState();
      setComponentMounting(true);
    }
  }, [provider]);

  if (!contractStateInitialized[key] && initialState != undefined) return [initialState, updateState];

  return [state, updateState];
}

useSerumContext.js

import { useSerumContract } from '../lib/ContractConnector';
import { useContractState } from './useContractState';

export function useSerumContext() {
  const { contract } = useSerumContract();

  const fetchState = async () => ({
    owner: await contract.owner(),
    claimActive: await contract.claimActive(),
  });

  return useContractState('serumContext', fetchState);
}

The reason why I have so many extra checks is that I don't want to re-fetch the state when the component re-mounts, but the state has already been initialised. The state should however subscribe to updates on provider changes and re-fetch if it has changed.

phaze
  • 152
  • 7