Adds automatic loading state to async thunks. Used as a drop-in replacement for @reduxjs/toolkit
's createSlice
.
If you are setting up a new project, I'd recommend you to rather go for RTK's createAPI
. This package was made with the intention for use in existing projects that are otherwise happy to keep using the traditional asyncthunk/slice setup for their API calls.
yarn add @ryfylke-react/create-api-slice
# or
npm i @ryfylke-react/create-api-slice
Create the slice as you normally would, but make sure state
is part of your schema. It should take a type of StateStatus
. Use our createAPISlice
function instead of createSlice
from redux toolkit.
If you want to use another key than
state
to store the StateStatus, pass in{ key: "myCustomKey" }
as a second parameter tocreateAPISlice
.
slices/postSlice.ts
import {
createAPISlice,
StateStatus,
} from "@ryfylke-react/create-api-slice";
const initialState = {
...baseInitialState,
state: StateStatus.IDLE,
};
const postSlice = createAPISlice({
name: "post",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(getPost.fulfilled, (state, { payload }) => {
state.post = payload;
})
.addCase(getPost.rejected, (state) => {
state.post = null;
});
},
});
export const postReducer = postSlice.reducer;
Then, when you create thunks that require loading state, make sure to append :load
to the thunk name.
If you want to change the
:load
identifier, you can pass in{ identifier: ":myCustomIdentifier" }
as a second parameter tocreateAPISlice
slices/postThunks.ts
export const getPost = createAsyncThunk(
`post/getPost:load`,
async (slug: string, { rejectWithValue }) => {
return axios
.get<PostData[]>(`${API_URL}/posts?slug=${slug}`)
.then((res) => res.data[0])
.catch((err) => rejectWithValue(err));
}
);
Calling dispatch(getPost("..."))
will now automatically set the loading state to 1
(PENDING), which will again automatically change to 2
(FULFILLED) or 3
(REJECTED).
Here's how you'd implement this logic on the UI:
// ...
import { StateStatus } from "@ryfylke-react/create-api-slice";
const App = () => {
const { id } = useParams();
const dispatch = useDispatch();
const { state, post } = useSelector((state) => state.post);
useEffect(() => {
dispatch(getPost(id));
}, []);
if (state === StateStatus.REJECTED) {
return "Error from server";
}
if (state === StateStatus.FULFILLED) {
return (...);
}
return "Loading...";
}
This unfortunately only supports one concurring loading state per slice. This means that if you call two async thunks that both have :load
appended - they will both mutate the same loading state.
If you want, you can add a second parameter to createAPISlice
, which is an options object of type APISliceOpts:
type APISliceOpts<T> = {
/** The key that stores the StateStatus in the slice. Default `state`. */
key?: string;
/** The identifier used to add loading state. Default `:load` */
identifier?: string;
/** Replaces the createSlice function used internally */
createSliceOverwrite?: (
options: CreateSliceOptions<T, SliceCaseReducers<T>, string>
) => any;
};