The store/
module contains all of your application's state. Inside the store, there are multiple custom directories. Each directory can represent an automatically generated api connector that are available at Crowdbotics platform "Connector" tool, and can be added to any project. The exception goes to custom/
directory, which will be explained shortly.
Inside each connector directory, you will find the following types of files:
api.js
{model_name}.slice.js
For the connectors code, we chose to use Redux as the pattern for managing and updating the application state, and, specifically, the Redux-Toolkit library, which facilitates managing the app state and create an easier standard for writing redux logic.
Our connector tool automatically generates a service layer that serves as a helper mechanism to send requests to the multiple endpoints that you have available via the connectors installed in the project.
// store/petStoreService/api.js
import axios from "axios";
import { PETS_STORE_USERNAME, PETS_STORE_PASSWORD } from "react-native-dotenv";
const petStoreService = axios.create({
baseURL: "https://pets.crowdbotics.io",
auth: { username: PETS_STORE_USERNAME, password: PETS_STORE_PASSWORD },
headers: { Accept: "application/json", "Content-Type": "application/json" },
});
function get_pets_list() {
return petStoreService.get(`/pets`);
}
export const apiService = { get_pets_list };
For every data model existing in a connector, we create a .slice.js
file that:
- Creates a slice with the
createSlice
function. This redux-toolkit function accepts an initial state, an object full of reducer functions, and a "slice name", and automatically generates action creators and action types that correspond to the reducers and state 1- For each request defined in the
services
file, it creates a corresponding thunk usingcreateAsyncThunk
function that accepts an action type string (which is generated in thecreateSlice
) and a callback function that should return a promise. It then generates a promise lifecycle based on the provided type action and returns a thunk action creator, that runs the callback and dispatch the lifecycle actions based on the returned promise 2. This abstracts the standard recommended approach for handling async request lifecycles.
Both combined enables an abstraction for the recommended approach when handling async requests with redux. Let's take a look at the code:
//store/petStoreService/pets.slice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
import { apiService } from "./services"
// First, creating the think for getting pets data
// using the service/endpoint call as the promise callback
export const get_pets_list = createAsyncThunk(
"pets/get_pets_list",
async payload => await apiService.get_pets_list(payload)
)
// defining the initial state: pets data will be saved in the let entities' list
const initialState = { entities: [], api: { loading: "idle", error: null } }
// Creating the slice for the model Pet
const pets = createSlice({
name: "pets",
initialState: initialState,
reducers: {},
extraReducers: {
[get_pets_list.pending]: (state, action) => {
if (state.api.loading === "idle") {
state.api.loading = "pending"
}
},
[get_pets_list.fulfilled]: (state, action) => {
if (state.api.loading === "pending") {
// update Pets array with new pet list data
state.entities = [...state.entities, ...action.payload.data]
state.api.loading = "idle"
}
},
[get_pets_list.rejected]: (state, action) => {
if (state.api.loading === "pending") {
state.api.error = action.error
state.api.loading = "idle"
}
}
}
})
export default {
get_pets_list,
slice: pets
}
In the example, the action passed to createAsyncThunk
is pets/get_pets_list
, since pets
os the name of the slice and get_pets_list
is the actual action name we want to use to represent the lifecycle of the async request.
This provided action in the thunk creation will generate these action types (which are handled by our slice):
- pending: 'pets/get_pets_list/pending': request was initiated
- fulfilled: 'pets/get_pets_list/fulfilled': request when through
- rejected: 'pets/get_pets_list/rejected': On error handling
For more information, please checkout the redux-toolkit documentation: https://redux-toolkit.js.org/api/createAsyncThunk
For your custom state management needs, use the custom/
directory. As long as you follow the pattern by creating slices in the format <file_name>.slice.js
and exports all of your named thunks and a default with export default { slice: <slice_name> }
. Here is an example of an slice file, using createAsyncThunk
from Redux Toolkit:
// users.slice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
import { api } from "./api"
export const getUsers = createAsyncThunk(
"users/getUsers",
async payload => {
const response = await api.getUsers(payload)
return response.data
}
)
const initialState = { entities: [], api: { loading: "idle", error: null } }
const usersSlice = createSlice({
name: "users",
initialState: initialState,
reducers: {},
extraReducers: {
[getUsers.pending]: (state, action) => {
if (state.api.loading === "idle") {
state.api.loading = "pending"
}
},
[getUsers.fulfilled]: (state, action) => {
if (state.api.loading === "pending") {
state.entities = action.payload
state.api.loading = "idle"
}
},
[getUsers.rejected]: (state, action) => {
if (state.api.loading === "pending") {
state.api.error = action.error
state.api.loading = "idle"
}
},
}
})
export default { slice: usersSlice }
The thunks generated by createAsyncThunk
will always return a resolved promise with either the fulfilled
action object or rejected
action object inside, as appropriate. To consume the resulting value of your api call using the created thunks, there are two approaches, and depends if you are using the connect()
function or React hooks in your component.
react-redux
provides the connect()
function that allows connecting a React
component to the Redux store. This function accepts a mapState
and a
mapDispatch
function.
The first, mapState
wraps the connected component with a component that
receives the function return as merged props.
The mapDispatch
map enables dispatching actions from the component by
passing action creators into the component props.
import React from "react";
import { View, Button, Text } from "react-native";
import { get_pets_list } from "@/store/petStoreService/pets.slice.js";
const PetsCounter = ({ pets, get_pets_list }) => (
<View>
<Text>There's {pets.length} pets in the database.</Text>
<Button onClick={get_pets_list()}>Refresh</button>
</View>
);
const mapState = state => {
return { pets: state.pets.entities };
};
const mapDispatch = {
get_pets_list,
};
export default connect(mapState, mapDispatch)(PetsCounter);
The Redux Toolkit library exports a unwrapResult
function that can be used to extract the payload of a fulfilled action or to throw the error. This can be useful if you wish to access the updated values immediately after the dispatched action is fulfilled, as the current state
might not be updated yet, as you can check in the code below:
import React from "react";
import { View, Button, Text } from "react-native";
import { useSelector, useDispatch } from 'react-redux';
import { get_pets_list } from "@/store/petStoreService/pets.slice.js";
import { unwrapResult } from "@reduxjs/toolkit";
const PetsCounter = () => {
const { entities, api } = useSelector((state) => state.pets)
const dispatch = useDispatch()
const onClick = () => {
try {
const resultAction = await dispatch(get_pets_list())
// unwrapping values returned by the api call. You might notice that the value of pets (unwrapped)
// is different than entities, since the state store might not have been updated yet
const pets = unwrapResult(resultAction)
console.log('success', `Refreshed pets list: ${pets.length} before was ${entities.length}`)
// finish doing any logic with pets
} catch (err) {
console.log('error', `Fetch failed: ${err.message}`)
}
}
return (
<View>
<Text>There are {entities.length} pets in the database.</Text>
<Button onPress={onClick}>Refresh</Button>
</View>
)
};
export default PetsCounter