Search code examples
javascripttypescriptreact-nativereduxredux-thunk

Async does wait for data to be returned in a redux-thunk function


I've being trying populate my redux store with data that comes from my mongo-db realm database. Whenever I run the function below it will execute fine but the problem is data will be delayed and ends up not reaching my redux store.

My thunk function:

export const addItemsTest = createAsyncThunk(
  "addItems",
  async (config: any) => {
    try {
      return await Realm.open(config).then(async (projectRealm) => {
        let syncItems = await projectRealm.objects("Item");
        await syncItems.addListener((x, changes) => {
          x.map(async (b) => {
            console.log(b);
            return b;
          });
        });
      });
    } catch (error) {
      console.log(error);
      throw error;
    }
  }
);

and my redux reducer:

  extraReducers: (builder) => {
      builder.addCase(addItemsTest.fulfilled, (state, { payload }: any) => {
        try {
          console.log("from add Items");
          console.log(payload);
          state.push(payload);
        } catch (error) {
            console.log(error)
         }
      });
  }

Expected Results: My redux store should have these data once addItemsTest return something:

[{
itemCode: 1,
itemDescription: 'Soccer Ball',
itemPrice: '35',
partition: 'partitionValue',
},
{
itemCode: 2,
itemDescription: 'Base Ball',
itemPrice: '60',
partition: 'partitionValue',
}
]

Actual Results: Console Results


Solution

  • Mixed Syntaxes

    You are combining await/async and Promise.then() syntax in a very confusing way. It is not an error to mix the two syntaxes, but I do not recommend it. Stick to just await/async

    Void Return

    Your action actually does not return any value right now because your inner then function doesn't return anything. The only return is inside of the then is in the x.map callback. await syncItems is the returned value for the mapper, not for your function.

    Right now, here's what your thunk does:

    • open a connection
    • get items from realm
    • add a listener to those items which logs the changes
    • returns a Promise which resolves to void

    Solution

    I believe what you want is this:

    export const addItemsTest = createAsyncThunk(
      "addItems",
      async (config: any) => {
        try {
          const projectRealm = await Realm.open(config);
          const syncItems = await projectRealm.objects("Item");
          console.log(syncItems);
          return syncItems;
        } catch (error) {
          console.log(error);
          throw error;
        }
      }
    );
    

    Without the logging, it can be simplified to:

    export const addItemsTest = createAsyncThunk(
      "addItems",
      async (config: any) => {
        const projectRealm = await Realm.open(config);
        return await projectRealm.objects("Item");
      }
    );
    

    You don't need to catch errors because the createAsyncThunk will handle errors by dispatching an error action.

    Edit: Listening To Changes

    It seems that your intention is to sync your redux store with changes in your Realm collection. So you want to add a listener to the collection that calls dispatch with some action to process the changes.

    Here I am assuming that this action takes an array with all of the items in your collection. Something like this:

    const processItems = createAction("processItems", (items: Item[]) => ({
      payload: items
    }));
    

    Replacing the entire array in your state is the easiest approach. It will lead to some unnecessary re-renders when you replace item objects with identical versions, but that's not a big deal.

    Alternatively, you could pass specific properties of the changes such as insertions and handle them in your reducer on a case-by-case basis.

    In order to add a listener that dispatches processItems, we need access to two variables: the realm config and the redux dispatch. You can do this in your component or by calling an "init" action. I don't think there's really much difference. You could do something in your reducer in response to the "init" action if you wanted.

    Here's a function to add the listener. The Realm.Results object is "array-like" but not exactly an array so we use [...x] to cast it to an array.

    FYI this function may throw errors. This is good if using in createAsyncThunk, but in a component we would want to catch those errors.

    const loadCollection = async (config: Realm.Configuration, dispatch: Dispatch): Promise<void> => {
      const projectRealm = await Realm.open(config);
      const collection = await projectRealm.objects<Item>("Item");
      collection.addListener((x, changes) => {
        dispatch(processItems([...x]));
      });
    }
    

    Adding the listener through an intermediate addListener action creator:

    export const addListener = createAsyncThunk(
      "init",
      async (config: Realm.Configuration, { dispatch }) => {
        return await loadCollection(config, dispatch);
      }
    );
    
    // is config a prop or an imported global variable?
    const InitComponent = ({config}: {config: Realm.Configuration}) => {
      const dispatch = useDispatch();
    
      useEffect( () => {
        dispatch(addListener(config));
      }, [config, dispatch]);
    
      /* ... */
    }
    

    Adding the listener directly:

    const EffectComponent = ({config}: {config: Realm.Configuration}) => {
      const dispatch = useDispatch();
    
      useEffect( () => {
        // async action in a useEffect need to be defined and then called
        const addListener = async () => {
          try {
            loadCollection(config, dispatch);
          } catch (e) {
            console.error(e);
          }
        }
    
        addListener();
        
      }, [config, dispatch]);
    
      /* ... */
    }