How to subscribe to state outside React component in Redux Toolkit? - reactjs

I have the following slice:
export const authenticationSlice = createSlice({
name: 'authentication',
initialState: {
isFirstTimeLoading: true,
signedInUser: null
},
reducers: {
signOut: (state) => {
state.signedInUser = null
},
setUserAfterSignIn: (state, action: PayloadAction<SignInResult>) => {
// some logic...
state.signedInUser = {...}
}
},
extraReducers: builder => {
// Can I subscribe to signedInUser changes here?
}
})
Is there a way I can subscribe to when signedInUser changes (setUserAfterSignIn and signOut), inside extraReducers?
For example everytime the setUserAfterSignIn action is dispatched I want to add an interceptor in axios that uses the user's accessToken as a Auth header.
Can I also subscribe to this state from antoher slice? if some state in a different slice depends on signedInUser?
EDIT: Here is the thunk that signs in a user, and one that signs out
export const { signOut: signOutAction, setUserAfterSignIn: setUserAction } = authenticationSlice.actions
export const signInWithGoogleAccountThunk = createAsyncThunk('sign-in-with-google-account', async (staySignedIn: boolean, thunkAPI) => {
const state = thunkAPI.getState() as RootState
state.auth.signedInUser && await thunkAPI.dispatch(signOutThunk())
const googleAuthUser = await googleClient.signIn()
const signedInUser = await signInWithGoogleAccountServer({ idToken: googleAuthUser.getAuthResponse().id_token, staySignedIn })
thunkAPI.dispatch(setUserAction({ data: signedInUser.data, additionalData: { imageUrl: googleAuthUser.getBasicProfile().getImageUrl() } } as SignInResult))
})
export const signInWithLocalAccountThunk = createAsyncThunk('sign-in-with-local-account', async (dto: LocalSignInDto, thunkAPI) => {
const state = thunkAPI.getState() as RootState
state.auth.signedInUser && await thunkAPI.dispatch(signOutThunk())
const user = await signInWithLocalAccountServer(dto)
thunkAPI.dispatch(setUserAction({ data: user.data } as SignInResult))
})
export const signOutThunk = createAsyncThunk<void, void, { dispatch: AppDispatch }>('sign-out', async (_, thunkAPI) => {
localStorage.removeItem(POST_SESSION_DATA_KEY)
sessionStorage.removeItem(POST_SESSION_DATA_KEY)
const state = thunkAPI.getState() as RootState
const signedInUser = state.auth.signedInUser
if (signedInUser?.method === AccountSignInMethod.Google)
await googleClient.signOut()
if (signedInUser)
await Promise.race([signOutServer(), rejectAfter(10_000)])
.catch(error => console.error('Signing out of server was not successful', error))
.finally(() => thunkAPI.dispatch(signOutAction()))
})

Redux implements the flux architecture.
This structure allows us to reason easily about our application in a way that is reminiscent of functional reactive programming, or more specifically data-flow programming or flow-based programming, where data flows through the application in a single direction — there are no two-way bindings.
Reducers should not be dependent on each other, because redux does not ensure a specific order in which they are executed. You can work around this by using combineReducer. You can not be sure that the extraReducers are executed after the setUserAfterSignIn reducer.
The options you have are:
Put the code that updates the axios interceptor in the setUserAfterSignIn reducer.
setUserAfterSignIn: (state, action: PayloadAction<SignInResult>) => {
// some logic...
state.signedInUser = {...}
// update axios
}
Create the axios interceptor and pass it a supplier that is connected to the store. This way you can replace the way tokens are supplied easily.
const tokenSupplier = () => store.getState().signedInUser;
// ...
axios.interceptors.request.use(function (config) {
const token = tokenSupplier();
config.headers.Authorization = token;
return config;
});
Extract two reducer functions and ensure their order.
function signInUser(state, action) {
state.signedInUser = {...}
}
function onUserSignedIn(state, action) {
// update axios interceptor
}
// ....
// ensure their order in the redux reducer.
setUserAfterSignIn: (state, action: PayloadAction<SignInResult>) => {
signInUser(state, action);
onUserSignedIn(state, action)
}
EDIT
Given this architecture, What are my options if I have another slice that needs to react when signedInUser has changed?
I guess you will not like the answer. I struggled with the same issue some time ago.
Another slice is an independent part in the store. You can add extra reducers that can listen to actions from other slices, but you can not be sure that the other slice's reducer has already updated the state.
Let's assume you have a slice A and a reducer RA and a slice B with a reducer RB. If the state B depends on A it means that the reducer RB should execute whenever A changes.
You can RA call RB, but this introduces a dependency to RB. It would be nice if RA could dispatch an action like { type: "stateAChanged", payload: stateA} so that other slices can listen to that action, but reducers can not dispatch actions. You can implement a middleware that augments actions with a dispatcher. E.g.
function augmentAction(store, action) {
action.dispatch = (a) => {
store.dispatch(a)
}
store.dispatch(action)
}
so that the reducers can dispatch actions.
setUserAfterSignIn: (state, action: PayloadAction<SignInResult>) => {
// some logic...
state.signedInUser = {...}
action.dispatch({type : "userSignedIn": payload: {...state}})
}
But this approach is not a standard approach and if you excessively use it, you might introduce cycles that lead to endless loops in the dispatch.
Instead of using different slices some use different stores and connect them using the store's subscribe. This is an official AP, but it can also introduce loops if you don't pay enough attention.
So finally the simplest approach is to just call RB from RA. You can manage the dependency between them a bit by reversing it. E.g.
const onUserSignedIn = (token) => someOtherReducer(state, { type: "updateAxios", payload: token});
setUserAfterSignIn: (state, action: PayloadAction<SignInResult>) => {
// some logic...
state.signedInUser = {...}
onUserSignedIn(state.signedInUser.token)
}
Now you can replace the onUserSignedIn callback in tests or with a composite functions that calls other registered callbacks.
EDIT
I'm currently working on a middleware library to solve our issue. I published my actual version of my library on Github and npm. The idea is that you describe the dependencies between states and actions that should be dispatched on change.
stateChangeMiddleware
.whenStateChanges((state) => state.counter)
.thenDispatch({ type: "text", payload: "changed" });

Yes, another slice can listen for various action types and apply the action payload (or meta data) to its own state object.
But be careful that you don't end up keeping mirrored or derived state spread across various slices of your store.
export const signInThunk = createAsyncThunk('signIn', async (_: void, thunkAPI) => {
const authUser = await authClient.signIn()
return authUser
})
export const authenticationSlice = createSlice({
name: 'authentication',
initialState: {
isFirstTimeLoading: true,
signedInUser: null
},
reducers: {},
extraReducers: builder => {
builder.addCase(signInThunk.fulfilled, (state, action) => {
state.signedInUser = action.payload
})
}
})
export const anotherSlice = createSlice({
name: 'another',
initialState: {},
reducers: {},
extraReducers: builder => {
builder.addCase(signInThunk.fulfilled, (state, action) => {
// do something with the action
})
}
})

Related

Redux: Call thunk action from slice reducer action

I have a tree structure which is loading children on demand, this is my reducer. The problem I have is that when I want to call my thunk action from toggleExpandedProp I get exception (see bellow). What should I do?
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import { useDispatch } from 'react-redux';
import axios from 'axios';
const dispatch = useDispatch()
export const getRoot = createAsyncThunk('data/nodes/getRoot', async () => {
const response = await axios.get('http://localhost:5000/api/nodes/root');
const data = await response.data;
return data;
});
export const getChildren = createAsyncThunk('data/nodes/getRoot', async params => {
const response = await axios.get('http://localhost:5000/api/nodes/' + params.id + '/children');
const data = await response.data;
return data;
});
const initialState = {
data: [],
loading: 'idle'
};
// Then, handle actions in your reducers:
const nodesSlice = createSlice({
name: 'nodes',
initialState,
reducers: {
toggleExpandedProp: (state, action) => {
state.data.forEach(element => {
if(element.id === action.payload.id) {
element.expanded = !element.expanded;
dispatch(getChildren(element));
}
});
}
},
extraReducers: {
// Add reducers for additional action types here, and handle loading state as needed
[getRoot.fulfilled]: (state, action) => {
state.data = action.payload;
},
[getChildren.fulfilled]: (state, action) => {
state.data.push(action.payload);
}
}
})
export const { toggleExpandedProp } = nodesSlice.actions;
export default nodesSlice.reducer;
Exception has occurred.
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
const dispatch = useDispatch()
You can only use useDispatch inside of a function component or inside another hook. You cannot use it at the top-level of a file like this.
You should not call dispatch from inside a reducer. But it's ok to dispatch multiple actions from a thunk. So you can turn toggleExpandedProp into a thunk action.
You probably need to rethink some of this logic. Does it really make sense to fetch children from an API when expanding a node and then fetch them again when collapsing it?
export const toggleExpandedProp = createAsyncThunk(
"data/nodes/toggleExpandedProp",
async (params, { dispatch }) => {
dispatch(getChildren(params));
}
);
This is kind of a useless thunk since we don't actually return anything. Can you combine it with the getChildren action, or do you need to call that action on its own too?
const nodesSlice = createSlice({
name: "nodes",
initialState,
reducers: {
},
extraReducers: {
[toggleExpandedProp.pending]: (state, action) => {
state.data.forEach((element) => {
if (element.id === action.payload.id) {
element.expanded = !element.expanded;
}
});
},
[getRoot.fulfilled]: (state, action) => {
state.data = action.payload;
},
[getChildren.fulfilled]: (state, action) => {
state.data.push(action.payload);
}
}
});

Redux returning an empty arrays in store - Reducer not working correctly?

I have recently put Redux into my app for the first time and thought I had it working but it seems to be returning empty arrays. I have checked my Postman get posts and it is working fine on the backend. Should my store be returning values if the arrays are empty like below?
What is likely the issue? I have a async Thunk action creator for it and a create slice Reducer that I thought were working ok.
If my index combineReducers that are createSlice are all appearing in white does this mean they aren't working correctly? The auth and message ones are in yellow and my login works correctly however I didn't use createSlice for them.
Update: I think this is to do with the syntax of my extraReducers "state: actionpayload.field". There is no error message flagging but i'm not sure it's doing what it is meant to do.
Or could this be to do with the fact that I have a combineReducer for my store and passing through reducers that are createSlice? (Should be configureStore for Redux toolkit) my Auth and messages work ok but they aren't Redux. Does configureStore allow both createSlice and normal switch statements at the same time?
index.js
export default combineReducers({
// combine the reducers
user,
fields,
article,
diveLog,
marineList,
diveSchool,
diveSpot,
admin,
auth,
message
});
reducer
const fieldsSlice = createSlice({
name: 'diveLogFields',
initialState: {
current: [],
region: [],
diveType: [],
visibility: [],
diveSpot: [],
},
reducers: {},
extraReducers: {
// picks up the success action from the thunk
[requireFieldData.fulfilled.type]: (state, action) => {
// set the property based on the field property in the action
(state: action.payload.field); (state: action.payload.items)
}
}
})
export default fieldsSlice.reducer;
action
export const requireFieldData = createAsyncThunk(
'fields/requireData', // action name
// action expects to be called with the name of the field
async (fields) => {
// you need to define a function to fetch the data by field name
const response = await diveLogFields(fields);
const { data } = response;
// what we return will be the action payload
return {
fields,
items: data.data
};
},
// only fetch when needed
{
condition: (fields, {getState}) => {
const {field} = getState();
// check if there is already data by looking at the array length
if ( field[fields].length > 0 ) {
// return false to cancel execution
return false;
}
}
}
)
Update
I am still getting the below error message when I try to render my page. I had to go into my store and add the compose Redux import as well.
Where does this message suggest the problem is?
I see few minor issues in your code, So, below are fixes and the explanations:
Slice:
const fieldsSlice = createSlice({
name: 'diveLogFields',
initialState: {
current: [],
region: [],
diveType: [],
visibility: [],
diveSpot: [],
fields: [], // Add initial fields array (if you want to store it)
items: [], // Add initial items array (if you want to store it)
},
reducers: {},
extraReducers: {
[requireFieldData.fulfilled.type]: (state, action) => {
state.fields = action.payload.fields
state.items = action.payload.items
},
},
})
export default fieldsSlice.reducer
In the above code, you can use state.fields = ... and state.items = ... to set data in state. It looks like we are directly mutating the state, but we are not because ...
Redux Toolkit allows us to write "mutating" logic in reducers. It doesn't actually mutate the state because it uses the Immer library, which detects changes to a "draft state" and produces a brand new immutable state based off those changes
AsyncThunkAction:
// Assumption, fake async call
function diveLogFields(fields) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ data: { data: [1, 2, 3] }, fields })
}, 2000)
})
}
export const requireFieldData = createAsyncThunk(
'diveLogFields/requireFieldData',
async (fields) => {
const response = await diveLogFields(fields)
const { data } = response
return {
fields,
items: data.data,
}
},
{
condition: (fields, { getState, extra }) => {
const { fields: fieldsState } = getState() // getState() returns your Redux State
if (fieldsState.fields.length > 0) {
return false // cancel the action if fields has some data
}
},
}
)
Here is an example to dispatch the async action:
function MyComponent() {
const dispatch = useDispatch()
// for example, make async call below hook
useEffect(() => {
dispatch(requireFieldData([4, 5, 6]))
}, [dispatch])
return (
<>i am MyComponent</>
)
}
Now, here is how the state looks like after dispatching the action:
Got it working. I had to change my store.js file to the below as it was previously set-up differently. I also used the code changes suggested in my replies.
const store = createStore(
rootReducer,
compose(
applyMiddleware(thunk),
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)
);
export default store;

What is difference between reducers and extrareducers in redux toolkit?

I have learned about redux toolkit for 2 months, and in createSlice have reducers and extrareducers, I know they use to change state from dispatch but I don't know the difference, Where should we use them?
The reducers property both creates an action creator function and responds to that action in the slice reducer. The extraReducers allows you to respond to an action in your slice reducer but does not create an action creator function.
You will use reducers most of the time.
You would use extraReducers when you are dealing with an action that you have already defined somewhere else. The most common examples are responding to a createAsyncThunk action and responding to an action from another slice.
extrareducers is actually like reducers with enhancements, but it's been built to handle more options, esecially other actions (like the ones generated in other slices or actions made by createAction or createAsyncThunk). In one word
Everything you can dispatch in redux can be added to it.
The extrareducers property in createSlice can be used as a function or as an object.
The function form has an input argument named builder.
Example 1: The builder.addCase function adds an action from another slice (with Typescript type safety).
const {actions,reducer} = createSlice({
name:'users',
reducers:{
......
},
initialState:.... ,
extraReducers:(builder) => {
builder.addCase(authActions.logout, state => {
state.isLoggedIn = false;
});
}
});
The authActions.logout here is another action from another slice.
If you create an action using createAsyncThunk function (which is imported from "#reduxjs/toolkit") You can handle loading, success & failure states.
Example 2: Handling loading state from an async Thunk:
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
const {actions,reducer} = createSlice({
name:'users',
reducers:{
......
},
initialState:.... ,
extraReducers: (builder) => {
......
......
builder.addCase(fetchUserById.pending, (state, action) => {
state.loading=true;
state.whoseDataIsLoading = action.payload;
})
}
});
fetchUserById.pending (handles loading state of the asyncThunk)
fetchUserById.rejected (handles failed state)
fetchUserById.fulfilled (handle success state)
The builder also accepts addDefaultCase and addMatcher in which the addDefaultCase acts as the default case in switch statement used by conventional reducers (reducers in the redux without toolkit) and,
addMatcher is used with a type predicate function a function used to infer that some input has some particular type (type of action in here) to ensure the action has a particular type, then the state changes.
Example 3: The extrareducers as an object:
const {actions,reducer} = createSlice({
name:'users',
reducers:{
......
},
initialState:.... ,
extraReducers: {
['incrementBy']: (state, action) => {
state.someValue += action.payload
}
}
});
References:
https://redux-toolkit.js.org/api/createSlice#extrareducers
https://redux-toolkit.js.org/usage/usage-with-typescript#type-safety-with-extrareducers
https://redux-toolkit.js.org/api/createAsyncThunk
https://redux-toolkit.js.org/usage/usage-with-typescript#typing-builderaddmatcher
https://redux-toolkit.js.org/api/createReducer#builderadddefaultcase
let's say you have those two reducers:
const booksSlice = createSlice({
name: "books",
initialState: [],
reducers: {
addBook(state, action) {
// this state is not global state. it is only books slice
state.push(action.payload);
},
resetBooks(state, action) {
// immers assume whatever you return you want your state to be
return [];
},
},});
export const { addBook, resetBooks } = booksSlice.actions;
const authorsSlice = createSlice({
name: "authors",
initialState: [],
reducers: {
addAuthor(state, action) {
state.push(action.payload);
},
resetAuthors(state, action) {
return [];
},
},});
export const { addAuthor, resetAuthors } =authorsSlice.actions;
Both reducers have initial state is []. Let's say you display "books" and "authors" list in a page and you want to have a button to reset the both state.
<button onClick={() => handleResetLists()}>
Clear the page
</button>
inside handleResetLists you can dispatch both actions
const handleReset = () => {
dispatch(resetBooks());
dispatch(resetAuthors());
};
But instead, I could tell authorsSlice to watch for the additional action types with extraReducers
const authorsSlice = createSlice({
name: "authors",
initialState: [],
reducers: {
addAuthor(state, action) {
state.push(action.payload);
},
// builder tells this slice to watch for additional action types
extraReducers(builder) {
// second argument will update the state
builder.addCase(booksSlice.actions.reset, () => {
return [];
});
},
},
});
Now inside handleReset function, I will dispatch an action to book reducer but author reducer will be watching for this action and it will also update
const handleReset = () => {
dispatch(resetBooks());
// I do not need to dispatch this
// dispatch(resetAuthors());
};
In the above implementation, authorsSlice is dependent on booksSlice because we use booksSlice.actions.reset inside extraReducer. Instead we create a separate action because what if we get rid of bookSlice in the future
import {createAction} from "#reduxjs/toolkit"
// we are gonna dispatch this
export const resetAction=createAction("lists/reset")
inside both slicer add this extraReducer:
extraReducers(builder) {
builder.addCase(resetAction, (state, action) => {
state = [];
});
},
inside the page
import {resetAction} from "./store/actions"
const handleReset = () => {
dispatch(reset());
// I do not need to dispatch this
// dispatch(resetAction());
};
If action is supposed to be handled by one reducer, use reducers.
If action is supposed to be handled by multiple reducers, use extraReducers.

Exporting extra reducers from redux toolkit

I made a todo list a while ago as a way to practice react and redux. Now I'm trying to rewrite it with redux toolkit and having some trouble with the action creators.
Here is the old actions creator:
export const changeDescription = (event) => ({
type: 'DESCRIPTION_CHANGED',
payload: event.target.value })
export const search = () => {
return (dispatch, getState) => {
const description = getState().todo.description
const search = description ? `&description__regex=/${description}/` : ''
axios.get(`${URL}?sort=-createdAt${search}`)
.then(resp => dispatch({ type: 'TODO_SEARCHED', payload: resp.data }))
} }
export const add = (description) => {
return dispatch => {
axios.post(URL, { description })
.then(() => dispatch(clear()))
.then(() => dispatch(search()))
} }
export const markAsDone = (todo) => {
return dispatch => {
axios.put(`${URL}/${todo._id}`, { ...todo, done: true })
.then(() => dispatch(search()))
} }
export const markAsPending = (todo) => {
return dispatch => {
axios.put(`${URL}/${todo._id}`, { ...todo, done: false })
.then(() => dispatch(search()))
} }
export const remove = (todo) => {
return dispatch => {
axios.delete(`${URL}/${todo._id}`)
.then(() => dispatch(search()))
} }
export const clear = () => {
return [{ type: 'TODO_CLEAR' }, search()] }
Now this is the one that I'm working on, I'm trying to replicate the actions of the old one but using redux toolkit:
export const fetchTodos = createAsyncThunk('fetchTodos', async (thunkAPI) => {
const description = thunkAPI.getState().todo.description
const search = description ? `&description__regex=/${description}/` : ''
const response = await axios.get(`${URL}?sort=-createdAt${search}`)
return response.data
})
export const addTodos = createAsyncThunk('fetchTodos', async (thunkAPI) => {
const description = thunkAPI.getState().todo.description
const response = await axios.post(URL, {description})
return response.data
})
export const todoReducer = createSlice({
name: 'counter',
initialState: {
description: '',
list: []
},
reducers: {
descriptionChanged(state, action) {
return {...state, dedescription: action.payload}
},
descriptionCleared(state, action) {
return {...state, dedescription: ''}
},
},
extraReducers: builder => {
builder
.addCase(fetchTodos.fulfilled, (state, action) => {
const todo = action.payload
return {...state, list: action.payload}
})
.addCase(addTodos.fulfilled, (state, action) => {
let newList = state.list
newList.push(action.payload)
return {...state, list: newList}
})
}
})
The thing is, I can't find anywhere how to export my extra reducers so I can use them. Haven't found anything in the docs. Can someone help?
extraReducers
Calling createSlice creates a slice object with properties reducers and actions based on your arguments. The difference between reducers and extraReducers is that only the reducers property generates matching action creators. But both will add the necessary functionality to the reducer.
You have correctly included your thunk reducers in the extraReducers property because you don't need to generate action creators for these, since you'll use your thunk action creator.
You can just export todoReducer.reducer (personaly I would call it todoSlice). The reducer function that is created includes both the reducers and the extra reducers.
Edit: Actions vs. Reducers
It seems that you are confused by some of the terminology here. The slice object created by createSlice (your todoReducer variable) is an object which contains both a reducer and actions.
The reducer is a single function which takes the previous state and an action and returns the next state. The only place in your app when you use the reducer is to create the store (by calling createStore or configureStore).
An action in redux are the things that you dispatch. You will use these in your components. In your code there are four action creator functions: two which you created with createAsyncThunk and two which were created by createSlice. Those two will be in the actions object todoReducer.actions.
Exporting Individually
You can export each of your action creators individually and import them like:
import {fetchTodos, descriptionChanged} from "./path/file";
Your fetchTodos and addTodos are already exported. The other two you can destructure and export like this:
export const {descriptionChanged, descriptionCleared} = todoReducer.actions;
You would call them in your components like:
dispatch(fetchTodos())
Exporting Together
You might instead choose to export a single object with all of your actions. In order to do that you would combine your thunks with the slice action creators.
export const todoActions = {
...todoReducer.actions,
fetchTodos,
addTodos
}
You would import like this:
import {todoActions} from "./path/file";
And call like this:
dispatch(todoActions.fetchTodos())

createAsyncThunk and writing reducer login with redux-toolkit

I was reading createAsyncThunk documentation, and felt kind of confused with the flow. This is from the docs:
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit'
import { userAPI } from './userAPI'
// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: {
// Add reducers for additional action types here, and handle loading state as needed
[fetchUserById.fulfilled]: (state, action) => {
// Add user to the state array
state.entities.push(action.payload)
}
}
})
// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))
What do I have to write in the reducers and extraReducers? Standard reducer logic?
I have this CodeSandbox that I implemented the old redux way. Now, need to implement redux-toolkit in it.
The reducers property of createSlice allows you to make an action creator function and respond to those actions in one step. You use extraReducers to respond to an action that has already been created elsewhere, like in an async thunk. The extraReducer just responds to an action but does not create an action creator function.
The example is saying that you can have some regular reducers in addition to the extraReducers. But I looked at your CodeSandbox and in your case you do not need any other reducers because the only actions that you are responding to are the three actions from the async thunk.
Since your createSlice isn't going to make any action creators you don't really need to use createSlice. You can use it, but you can also just use createReducer.
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit'
import { userAPI } from './userAPI'
export const fetchUserFromGithub = createAsyncThunk(
'users/fetch',
async (username) => {
const response = await axios.get(
`https://api.github.com/users/${username}`
);
return response.data
}
)
const usersSlice = createSlice({
name: 'users',
initialState: {
user: null,
fetchingUser: false,
fetchingError: null
},
reducers: {},
extraReducers: {
[fetchUserFromGithub.pending]: (state, action) => {
state.fetchingUser = true;
state.fetchingError = null;
},
[fetchUserFromGithub.rejected]: (state, action) => {
state.fetchingUser = false;
state.fetchingError = action.error;
}
[fetchUserFromGithub.fulfilled]: (state, action) => {
state.fetchingUser = false;
state.user = action.payload;
}
}
})

Resources