use async function to get draft inside reducer of useImmerReducer

Viewed 431

I have this reducer function that I use for state management of my app.

const initialState = {roles: null};

const reducer = (draft, action) => {
    switch (action.type) {
      case 'initialize':
        //what should i do here????
        return;
      case 'add':
        draft.roles = {...draft.roles, action.role};
        return;
      case 'remove':
        draft.roles = Object.filter(draft.roles, role => role.name != action.role.name);
    }
  };

 const [state, dispatch] = useImmerReducer(reducer, initialState);

to initialize my state I must use an async function that reads something from asyncStorage if it exists, must set draft.roles to it, if not it should be set to a default value.

  const initialize = async () => {
    try {
      let temp = await cache.get();
      if (temp == null) {
        return defaultRoles;
      } else {
        return temp;
      }
    } catch (error) {
      console.log('initialization Error: ', error);
      return defaultRoles;
    }
  };

how can I get initilize function returned value inside 'initialize' case? if I use initilize().then(value=>draft.roles=value) I get this error:

TypeError: Proxy has already been revoked. No more operations are allowed to be performed on it

1 Answers

You cannot use asynchronous code inside of a reducer. You need to move that logic outside of the reducer itself. I am using a useEffect hook to trigger the initialize and then dispatching the results to the state.

There are quite a few syntax errors here -- should state.roles be an array or an object?

Here's my attempt to demonstrate how you can do this. Probably you want this as a Context Provider component rather than a hook but the logic is the same.

Javascript:

import { useEffect } from "react";
import { useImmerReducer } from "use-immer";

export const usePersistedReducer = () => {
  const initialState = { roles: [], didInitialize: false };

  const reducer = (draft, action) => {
    switch (action.type) {
      case "initialize":
        // store all roles & flag as initialized
        draft.roles = action.roles;
        draft.didInitialize = true;
        return;
      case "add":
        // add one role to the array
        draft.roles.push(action.role);
        return;
      case "remove":
        // remove role from the array based on name
        draft.roles = draft.roles.filter(
          (role) => role.name !== action.role.name
        );
        return;
    }
  };

  const [state, dispatch] = useImmerReducer(reducer, initialState);

  useEffect(() => {
    const defaultRoles = []; // ?? where does this come from?

   // always returns an array of roles
    const retrieveRoles = async () => {
      try {
        // does this need to be deserialized?
        let temp = await cache.get();
        // do you want to throw an error if null?
        return temp === null ? defaultRoles : temp;
      } catch (error) {
        console.log("initialization Error: ", error);
        return defaultRoles;
      }
    };

    // define the function
    const initialize = async() => {
      // wait for the roles
      const roles = await retrieveRoles();
      // then dispatch
      dispatch({type: 'initialize', roles});
    }

    // execute the function
    initialize();
  }, [dispatch]); // run once on mount - dispatch should not change

  // should use another useEffect to push changes
  useEffect(() => {
    cache.set(state.roles);
  }, [state.roles]); // run whenever roles changes

  // maybe this should be a context provider instead of a hook
  // but this is just an example
  return [state, dispatch];
};

Typescript:

import { Draft } from "immer";
import { useEffect } from "react";
import { useImmerReducer } from "use-immer";

interface Role {
  name: string;
}

interface State {
  roles: Role[];
  didInitialize: boolean;
}

type Action =
  | {
      type: "initialize";
      roles: Role[];
    }
  | {
      type: "add" | "remove";
      role: Role;
    };

// placeholder for the actual
declare const cache: { get(): Role[] | null; set(v: Role[]): void };

export const usePersistedReducer = () => {
  const initialState: State = { roles: [], didInitialize: false };

  const reducer = (draft: Draft<State>, action: Action) => {
    switch (action.type) {
      case "initialize":
        // store all roles & flag as initialized
        draft.roles = action.roles;
        draft.didInitialize = true;
        return;
      case "add":
        // add one role to the array
        draft.roles.push(action.role);
        return;
      case "remove":
        // remove role from the array based on name
        draft.roles = draft.roles.filter(
          (role) => role.name !== action.role.name
        );
        return;
    }
  };

  const [state, dispatch] = useImmerReducer(reducer, initialState);

  useEffect(() => {
    const defaultRoles: Role[] = []; // ?? where does this come from?

   // always returns an array of roles
    const retrieveRoles = async () => {
      try {
        // does this need to be deserialized?
        let temp = await cache.get();
        // do you want to throw an error if null?
        return temp === null ? defaultRoles : temp;
      } catch (error) {
        console.log("initialization Error: ", error);
        return defaultRoles;
      }
    };

    // define the function
    const initialize = async() => {
      // wait for the roles
      const roles = await retrieveRoles();
      // then dispatch
      dispatch({type: 'initialize', roles});
    }

    // execute the function
    initialize();
  }, [dispatch]); // run once on mount - dispatch should not change

  // should use another useEffect to push changes
  useEffect(() => {
    cache.set(state.roles);
  }, [state.roles]); // run whenever roles changes

  // maybe this should be a context provider instead of a hook
  // but this is just an example
  return [state, dispatch];
};
Related