Recoil: [GraphQL] Can I sync Recoil state with GraphQL?

Created on 10 Nov 2020  路  9Comments  路  Source: facebookexperimental/Recoil

What I am working on is a collaborative spreadsheet, where table cells between different views (view is the projection of a table, table cannot exist without views, a view may contain different cells as filtering may happen) will be shared, and changes between users on the same view will be shared. So I am looking for a state management tool, that serves as a intermediate layer between my GraphQL API and my UI components, that it can push the notification to the backend and receive the subscription from it, while also used as a synchronization tool for cells in different views.

What I have tried so far is I have created an atom family for all the cells in my table, those cells will for user be used for different views of that table. This is what I have tried, by creating the useCellValue class function, I wrapped not only the atom update but also the GraphQL update inside of it. It looks fine, so by calling the cellUpdater retrieved from that function, I can update not only the atom but also the backend.

type CellValueQuery = {workspaceId: string, tableId: string, recordId: string, fieldId: string}

export const cellValueStates = atomFamily<CellValue, CellValueQuery>({
  key: 'cell-value-states',
  default: null,
});

class Table {
    cellValueStates: (param: CellValueQuery) => RecoilState<CellValue>;

    useCellValue(cellValueQuery: CellValueQuery): [CellValue, SetterOrUpdater<CellValue>] {
      const [cellValue, setLocalCell] = useRecoilState(this.cellValueStates(cellValueQuery));

      const cellUpdater = (cellValue) => {
        const [setRemoteCell, { data }] = useMutation(UPDATE_CELL_MUTATION);
        setLocalCell(cellValue);
        setRemoteCell({variables: {workspaceId: cellValueQuery.workspaceId, tableId: cellValueQuery.tableId, 
          recordId: cellValueQuery.recordId, fieldId: cellValueQuery.fieldId, cellValue}
        })
      }

      return [cellValue, cellUpdater];
    }
}

So my questions are

  1. Since I have many of those cells, I realized that I may need to use the hook useCellValue I created in a for loop or whatever way, I know this is not really possible by design. How am I able to achieve that with recoil?
  2. Where can I subscribe to the changes from the GraphQL backend? I am wishing to do it in a centralized place like Table class where all the updates on atoms happened there and those changes can later be propagated to cells in views that are using that atom's state.

Thanks a lot! As someone who is really new to frontend I am really looking for a best solution for my app and hopefully this can be elegantly done by Recoil!

Most helpful comment

Correct. But, using useRecoilState() to subscribe to local Recoil changes can be useful if you want to setup a bi-direction sync.

Atom Effects were developed to make it easier to setup and define this type of state synchronization. However, they currently don't support React Hooks, which is problematic for working with GraphQL. We're pursuing a few options, but the easiest for you now would be to use a component like this with effects to do the state synchronization.

All 9 comments

Since I have many of those cells, I realized that I may need to use the hook useCellValue I created in a for loop or whatever way, I know this is not really possible by design. How am I able to achieve that with recoil?

You can get multiple dynamic Recoil values working around the limitation of not using React hooks in a loop in a few ways. You could use the waitForAll() helper:

function Table({cellValueQueries: Array<CellValueQuery>}) {
  const cellData: Array<CellValue> = useRecoilValue(waitForAll(cellValueQueries.map(callValueStates)));
  ...
}

Or you could make a selector() or selectorFamily() which iterates over dependent cell value atoms in traditional loop.

Where can I subscribe to the changes from the GraphQL backend? I am wishing to do it in a centralized place like Table class where all the updates on atoms happened there and those changes can later be propagated to cells in views that are using that atom's state.

As you've found, you can mutate the GraphQL backed with a hook wrapper like this. We've also been experimenting with an Atom Effect that does something similar. Getting the GraphQL state for the initial value can also be done by using a selector that queries GraphQL as the default value, or initializing that atom from a query in an Atom Effect. However, subscribing to GraphQL changes is a little harder. What you could try is a hook that subscribes to GraphQL changes, then uses a useEffect() when that value changes to update the atom value. Then you'd have to mount a component which uses this state synchronization hook for each cell you want to sync.

Since I have many of those cells, I realized that I may need to use the hook useCellValue I created in a for loop or whatever way, I know this is not really possible by design. How am I able to achieve that with recoil?

You can get multiple dynamic Recoil values working around the limitation of not using React hooks in a loop in a few ways. You could use the waitForAll() helper:

function Table({cellValueQueries: Array<CellValueQuery>}) {
  const cellData: Array<CellValue> = useRecoilValue(waitForAll(cellValueQueries.map(callValueStates)));
  ...
}

Or you could make a selector() or selectorFamily() which iterates over dependent cell value atoms in traditional loop.

Where can I subscribe to the changes from the GraphQL backend? I am wishing to do it in a centralized place like Table class where all the updates on atoms happened there and those changes can later be propagated to cells in views that are using that atom's state.

As you've found, you can mutate the GraphQL backed with a hook wrapper like this. We've also been experimenting with an Atom Effect that does something similar. Getting the GraphQL state for the initial value cal also be done by using a selector that queries GraphQL as the default value or initializing it in an Atom Effect. However, subscribing to GraphQL changes is a little harder. What you could try is a hook that subscribes to GraphQL changes, then uses a useEffect() when that value changes to update the atom value. Then you'd have to mount a component which uses this state synchronization hook for each cell you want to sync.

Thanks for the thoughtful answer for my question! I am still a bit confused by

What you could try is a hook that subscribes to GraphQL changes, then uses a useEffect() when that value changes to update the atom value. Then you'd have to mount a component which uses this state synchronization hook for each cell you want to sync.

Could you help me elaborate more on this? Did you mean something like

const COMMENTS_SUBSCRIPTION = gql`
  subscription OnCommentAdded($postID: ID!) {
    commentAdded(postID: $postID) {
      id
      content
    }
  }
`;

function LatestComment({ postID }) {
  const { data: { commentAdded }, loading } = useSubscription(
    COMMENTS_SUBSCRIPTION,
    { variables: { postID } }
  );
  useEffect() {
    // update cell atom state here
    ,
    [commentAdded.content, commentAdded.id]
  }
}

Yup, something like that.

@drarmstr Hey Douglas, I have another question regarding the usage of initializing of atoms upon the refresh of a component, is it recommended to do that within useEffect say something like this

function Table({ ... }) {
 const [ records, setRecords ] = useRecoilState(recordsState)

  useEffect() {
    const { data, loading } = useMutation( ... )
    // initialize cell atom from data
    ,
    [setRecords]
  }
}

or there is any other recommended way (say atom effect or selector, looks like we have many things on the table) that we can make GraphQL (or any other API calls) and recoil work together? What is being considered as the best practices for now and in the future say atom effect is becoming a stable feature? Thanks a lot!

That exact pattern won't work as you can't use hooks (such as useMutation()) from the callback function provided to useEffect().

That exact pattern won't work as you can't use hooks (such as useMutation()) from the callback function provided to useEffect().

Oh sorry I meant something like

function Table({ ... }) {
 const [ records, setRecords ] = useRecoilState(recordsState)
 const { data, loading } = useQuery( ... )

  useEffect() {
    // initialize cell atom from data
    setRecords(...data)

    [setRecords, data, loading]
  }
}

Yeah, something like that. It's not ideal because this component would synchronously render with the new data, then async update the Recoil state. But, if you had a component just dedicated to syncing the GraphQL and Recoil state, and everything else used the Recoil state, then hopefully everything should appear consistent.

If you're only using it to sync from GraphQL -> Recoil, then you can just use the useSetRecoilState() hook to avoid subscribing the component to re-render on Recoil state changes. If you kept that, though, you could potentially setup another effect to listen to changes in the Recoil state to mutate the GraphQL for a bi-directional sync.

Yeah, something like that. It's not ideal because this component would synchronously render with the new data, then async update the Recoil state. But, if you had a component just dedicated to syncing the GraphQL and Recoil state, and everything else used the Recoil state, then hopefully everything should appear consistent.

If you're only using it to sync from GraphQL -> Recoil, then you can just use the useSetRecoilState() hook to avoid subscribing the component to re-render on Recoil state changes. If you kept that, though, you could potentially setup another effect to listen to changes in the Recoil state to mutate the GraphQL for a bi-directional sync.

Great thanks a lot for the suggestion, so basically you are suggesting using just useSetRecoilState instead of useRecoilState in my case so the component won't get rerendered thus reloads the useQuery every time the records state got updated. Am I understanding correctly?

So basically useSetRecoilState() can be used in patterns like this where I want to have a dedicated component to receive changes either from query or subscription, then dispatch those changes to whichever atoms that are subscribed to the atoms here.

Correct. But, using useRecoilState() to subscribe to local Recoil changes can be useful if you want to setup a bi-direction sync.

Atom Effects were developed to make it easier to setup and define this type of state synchronization. However, they currently don't support React Hooks, which is problematic for working with GraphQL. We're pursuing a few options, but the easiest for you now would be to use a component like this with effects to do the state synchronization.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

karevn picture karevn  路  3Comments

aappddeevv picture aappddeevv  路  3Comments

ibnumusyaffa picture ibnumusyaffa  路  4Comments

adamkleingit picture adamkleingit  路  4Comments

robsoncezario picture robsoncezario  路  3Comments