Apeva
Apeva

Reputation: 46

A non-serializable value was detected in an action, in the path: payload

I am a novice developer, please tell me why an error occurs when calling initPayment: "A non-serializable value was detected in an action, in the path: payload. Value: [Error: Invalid hook call. Hooks can only be called inside of the body of a function component]"

import { useSQLiteContext } from "expo-sqlite";
const paymentSlice = createSliceWithThunks({
  name: "payments",
  initialState,
  reducers: (create) => ({
    initPayment: create.asyncThunk(
      async function (_, { rejectWithValue }) {
        try {
          const db = useSQLiteContext();
          const payments = await db.getAllAsync<Payments>("SELECT * FROM payments");
          return payments;
        } catch (err) {
          return rejectWithValue(err);
        }
      },
      {
        fulfilled: (state, action: PayloadAction<Payments[]>) => {
          state.list = action.payload;
        },
        rejected: (state) => {
          console.error("Error");
        },
      }
    ),

  }),
});
export default function Main() {
  const { theme } = useContext(ThemeContext);

  return (
    <SQLiteProvider
      databaseName="payments.db"
      assetSource={{ assetId: require("../assets/payments.db") }}
    >
      <Provider store={store}>
            <NavigationContainer theme={theme}>
              <PaperProvider>
                <StatusBar style="auto" />
                <Stack.Navigator
                  screenOptions={{
                    contentStyle: {
                      backgroundColor: theme.colors.background,
                      padding: 20,
                    },
                    headerShadowVisible: false,
                    headerStyle: {
                      backgroundColor: theme.colors.background,
                    },
                  }}
                >
                  <Stack.Screen name="Home" component={HomeScreen} />
                  <Stack.Screen name="Payment" component={Payment} />
                </Stack.Navigator>
              </PaperProvider>
            </NavigationContainer>
      </Provider>
    </SQLiteProvider>
  );
}

Upvotes: 1

Views: 111

Answers (1)

Drew Reese
Drew Reese

Reputation: 202836

You cannot call React hooks from anything other than React functions and custom React hooks, so calling the useSQLiteContext in the initPayment action creator is invalid.

You can however call useSQLiteContext in the React component that is dispatching initPayment and pass the DB instance to the Thunk action.

Example:

const paymentSlice = createSliceWithThunks({
  name: "payments",
  initialState,
  reducers: (create) => ({
    initPayment: create.asyncThunk(
      async function ({ db }, { rejectWithValue }) {
        try {
          return db.getAllAsync<Payments>("SELECT * FROM payments");
        } catch (err) {
          return rejectWithValue(err);
        }
      },
      {
        fulfilled: (state, action: PayloadAction<Payments[]>) => {
          state.list = action.payload;
        },
        rejected: (state) => {
          console.error("Error");
        },
      }
    ),
  }),
});
import { useSQLiteContext } from "expo-sqlite";

...

const SomeComponent = () => {
  const dispatch = useDispatch();
  const db = useSQLiteContext();

  ...

  const someHandler = async () => {
    ...
    const payments = await dispatch(initPayment({ db })).unwrap();
    ...
  };

  ...
};

Alternative to passing db to specific Thunk actions that use it

Create an intermediate wrapper component that can call useSQLiteContext hook and pass db in the Thunk middlware's extraArgument.

See getDefaultMiddleware API reference for details.

Example:

import { useSQLiteContext } from "expo-sqlite";
import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "../path/to/rootReducer";

const Wrapper = ({ children }) => {
  const db = useSQLiteContext();

  // Create stable/memoized Redux store instance reference
  const store = useMemo(() => {
    return configureStore({
      reducer: rootReducer,
      middleware: getDefaultMiddleware =>
        getDefaultMiddleware({
          thunk: {
            extraArgument: {
              db, // <-- pass db to Thunk middleware
            }
          }
        }),
    });
  }, [db]);

  return (
    <Provider store={store}>
      {children}
    </Provider>
  );
};

Render Wrapper in Redux Provider component's place in Main.

export default function Main() {
  const { theme } = useContext(ThemeContext);

  return (
    <SQLiteProvider
      databaseName="payments.db"
      assetSource={{ assetId: require("../assets/payments.db") }}
    >
      <Wrapper>
        <NavigationContainer theme={theme}>
          <PaperProvider>
            <StatusBar style="auto" />
            <Stack.Navigator screenOptions={{ ... }}>
              <Stack.Screen name="Home" component={HomeScreen} />
              <Stack.Screen name="Payment" component={Payment} />
            </Stack.Navigator>
          </PaperProvider>
        </NavigationContainer>
      </Wrapper>
    </SQLiteProvider>
  );
}

Access the extra ThunkApi property to get the passed db instance.

const paymentSlice = createSliceWithThunks({
  name: "payments",
  initialState,
  reducers: (create) => ({
    initPayment: create.asyncThunk(
      async function (_, { extra, rejectWithValue }) {
        try {
          const { db } = extra; // <-- access db from extraArgument
          return db.getAllAsync<Payments>("SELECT * FROM payments");
        } catch (err) {
          return rejectWithValue(err);
        }
      },
      {
        fulfilled: (state, action: PayloadAction<Payments[]>) => {
          state.list = action.payload;
        },
        rejected: (state) => {
          console.error("Error");
        },
      }
    ),
  }),
});

Upvotes: 1

Related Questions