This is my first time working with redux hooks and I keep receiving the error: "Error: Actions must be plain objects. Use custom middleware for async actions."
I have added the middleware thunk. Following the other peoples questions, I am not sure where I am making the mistake. I'm looking for an explanation on what I am doing wrong and what I should be reading in order to fix it.
Actions:
export const fetchNewsData = () => {
return (dispatch) => {
axios.get('http://localhost:3001/getnews')
.then(response => {
console.log(response.data);
const data = response.data;
dispatch(loadNews(data));
})
.catch(error => {
console.log(error);
dispatch(errorOnNews(error));
});
}
}
export const loadNews = (fetchedData) => {
return {
type: LOAD_NEWS,
payload: fetchedData
}
}
export const errorOnNews = (errorMessage) => {
return {
type: ERROR_ON_NEWS,
payload: errorMessage
}
}
Reducer:
const initialState = {
fetched: false,
data: [],
input: '',
filtered: [],
error: ''
}
const newsReducer = (state = initialState, action) => {
switch(action.type) {
case LOAD_NEWS:
return {
...state,
fetched: true,
data: action.payload
}
case FILTER_NEWS:
return {
...state
}
case ERROR_ON_NEWS:
return {
...state,
error: action.payload
}
default: return state;
}
}
Store:
import { createStore, applyMiddleware } from 'redux';
import ReduxThunk from 'redux-thunk';
import rootReducer from './rootReducer';
const store = createStore(rootReducer, applyMiddleware(thunk));
Component:
const fetch = useDispatch(fetchNewsData());
useEffect(() => {
if(hasFetched){
// work on true condition
} else {
fetch(); // fails on this line.
}
}, []);
useDispatch does not work like this, as it ignores all arguments and just returns you a dispatch function. So you have called dispatch() there, which essentially equals dispatch(undefined) - and the store doesn't know what to make of that action.
Do this instead:
const dispatch = useDispatch();
useEffect(() => {
if(hasFetched){
// work on true condition
} else {
dispatch(fetchNewsData()); // fails on this line.
}
}, []);
Also, generally you are writing a very outdated style of redux here that we do not really recommend to learn or use in new applications any more.
You might have been following an outdated tutorial - as this style requires you to write multiple times the necessary code and is much more error prone.
For up-to-date tutorials featuring modern redux with the official redux toolkit please see the official redux tutorials
Was following a tutorial and creating additional work that was unnessessary. Answer is to:
Cut: const fetch = useDispatch(fetchNewsData());
Change: fetch(); to fetchNewsData();
In this case, I am calling a handling function that will execute the dispatches when required.
Related
The information about the error in my case sits deeply in the response, and I'm trying to move my project to redux-toolkit. This is how it used to be:
catch(e) {
let warning
switch (e.response.data.error.message) {
...
}
}
The problem is that redux-toolkit doesn't put that data in the rejected action creator and I have no access to the error message, it puts his message instead of the initial one:
While the original response looks like this:
So how can I retrieve that data?
Per the docs, RTK's createAsyncThunk has default handling for errors - it dispatches a serialized version of the Error instance as action.error.
If you need to customize what goes into the rejected action, it's up to you to catch the initial error yourself, and use rejectWithValue() to decide what goes into the action:
const updateUser = createAsyncThunk(
'users/update',
async (userData, { rejectWithValue }) => {
const { id, ...fields } = userData
try {
const response = await userAPI.updateById(id, fields)
return response.data.user
} catch (err) {
if (!err.response) {
throw err
}
return rejectWithValue(err.response.data)
}
}
)
We use thunkAPI, the second argument in the payloadCreator; containing all of the parameters that are normally passed to a Redux thunk function, as well as additional options: For our example async(obj, {dispatch, getState, rejectWithValue, fulfillWithValue}) is our payloadCreator with the required arguments;
This is an example using fetch api
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
export const getExampleThunk = createAsyncThunk(
'auth/getExampleThunk',
async(obj, {dispatch, getState, rejectWithValue, fulfillWithValue}) => {
try{
const response = await fetch('https://reqrefs.in/api/users/yu');
if (!response.ok) {
return rejectWithValue(response.status)
}
const data = await response.json();
return fulfillWithValue(data)
}catch(error){
throw rejectWithValue(error.message)
}
}
)
Simple example in slice:
const exampleSlice = createSlice({
name: 'example',
initialState: {
httpErr: false,
},
reducers: {
//set your reducers
},
extraReducers: {
[getExampleThunk.pending]: (state, action) => {
//some action here
},
[getExampleThunk.fulfilled]: (state, action) => {
state.httpErr = action.payload;
},
[getExampleThunk.rejected]: (state, action) => {
state.httpErr = action.payload;
}
}
})
Handling Error
Take note:
rejectWithValue - utility (additional option from thunkAPI) that you can return/throw in your action creator to return a rejected response with a defined payload and meta. It will pass whatever value you give it and return it in the payload of the rejected action.
For those that use apisauce (wrapper that uses axios with standardized errors + request/response transforms)
Since apisauce always resolves Promises, you can check !response.ok and handle it with rejectWithValue. (Notice the ! since we want to check if the request is not ok)
export const login = createAsyncThunk(
"auth/login",
async (credentials, { rejectWithValue }) => {
const response = await authAPI.signin(credentials);
if (!response.ok) {
return rejectWithValue(response.data.message);
}
return response.data;
}
);
Usually in a thunk you'd wind up calling other actions:
const startRecipe = {type: "startRecipe"}
const reducer = (state, action) => {
if (action.type === "startRecipe") {
state.mode = AppMode.CookRecipe
}
}
const getRecipeFromUrl = () => async dispatch => {
const res = await Parser.getRecipeFromUrl(url)
dispatch(startRecipe)
}
With createAsyncThunk in redux toolkit, this isn't so straightforward. Indeed you can mutate the state from your resulting action in extraReducers:
export const getRecipeFromUrl = createAsyncThunk('getRecipeFromUrl',
async (url: string): Promise<RecipeJSON> => await Parser.getRecipeFromUrl(url)
)
const appStateSlice = createSlice({
name: 'app',
initialState: initialAppState,
reducers: {},
extraReducers: ({ addCase }) => {
addCase(getRecipeFromUrl.fulfilled, (state) => {
state.mode = AppMode.CookRecipe
})
}
})
But I also want to have non-async ways to start the recipe, which would entail a reducer in the slice:
reducers: {
startRecipe(state): state.mode = AppState.CookRecipe
},
To avoid writing the same code in two places I would love to be able to call the simple reducer function from the thunk handler. I tried simply startRecipe(state) and startRecipe (which had been destructured for ducks exporting so I’m fairly sure I was referring to the correct function) from the extraReducers case but it doesn't work.
My current solution is to define _startRecipe outside of the slice and just refer to that function in both cases
reducers: { startRecipe: _startRecipe },
extraReducers: builder => {
builder.addCase(getRecipeFromUrl.fulfilled, _startRecipe)
}
Is there a "better" way where you can define the simple action in your slice.reducers and refer to it from the thunk handler in extraReducers?
The second argument of the payloadCreator is thunkAPI (doc) from where you could dispatch the cookRecipe action.
interface ThunkApiConfig {
dispatch: AppDispatch,
state: IRootState,
}
export const getRecipeFromUrl = createAsyncThunk('getRecipeFromUrl',
async (url: string, thunkAPI: ThunkApiConfig): Promise<RecipeJSON> => {
await Parser.getRecipeFromUrl(url)
return thunkAPI.dispatch(cookRecipeActionCreator())
}
)
The idea of "calling a reducer" is the wrong approach, conceptually. Part of the design of Redux is that the only way to trigger a state update is by dispatching an action.
If you were writing the reducer using a switch statement, you could have multiple action types as cases that all are handled by the same block:
switch(action.type) {
case TypeA:
case TypeB: {
// common logic for A and B
}
case C: // logic for C
}
When using createSlice, you can mimic this pattern by defining a "case reducer" function outside of the call to createSlice, and pass it for each case you want to handle:
const caseReducerAB = (state) => {
// update logic here
}
const slice = createSlice({
name: "mySlice",
initialState,
reducers: {
typeA: caseReducerAB,
typeB: caseReducerAB,
}
extraReducers: builder => {
builder.addCase(someAction, caseReducerAB)
}
})
That sounds like what you described as your "current solution", so yes, that's what I would suggest.
I am trying to implement a check for authentication and to login/logout users using redux and firebase. I have the following code:
Action Types:
export const LOGIN_REQ = 'AUTH_REQ';
export const LOGOUT_REQ = 'LOGOUT_REQ';
export const AUTH_SUCCESS = 'AUTH_SUCCESS';
export const AUTH_FAILED = 'AUTH_FAILED';
export const GET_AUTH = 'GET_AUTH';
Reducers:
import * as ActionTypes from './ActionTypes';
export const auth = (state = {
isAuth: false,
user: null
}, action) => {
switch (action.type) {
case ActionTypes.LOGIN_REQ:
return { ...state, isAuth: false, user: null };
case ActionTypes.LOGOUT_REQ:
return { ...state, isAuth: false, user: null };
case ActionTypes.AUTH_FAILED:
return { ...state, isAuth: false, user: null };
case ActionTypes.AUTH_SUCCESS:
return { ...state, isAuth: true, user: action.payload };
case ActionTypes.GET_AUTH:
return state;
default:
return state;
}
}
Thunks:
export const getAuth = () => (dispatch) => {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
console.log('Get AUTH called');
dispatch(authSuccess());
}
else {
console.log('Get AUTH called');
dispatch(authFailed());
}
});
}
export const loginReq = (email, password, remember) => (dispatch) => {
firebase.auth().signInWithEmailAndPassword(email, password)
.then((cred) => {
if (remember === false) {
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE);
console.log('Logged In with Redux without persist');
}
else {
console.log('Logging in with Persist');
}
console.log('Dispatching Success !');
dispatch(authSuccess(cred.user.uid));
})
.catch((err) => {
console.log(err);
dispatch(authFailed(err));
});
}
export const logoutReq = () => (dispatch) => {
firebase.auth().signOut()
.then(() => dispatch(getAuth()))
.catch((err) => console.log(err));
}
export const authSuccess = (uid = null) => ({
type: ActionTypes.AUTH_SUCCESS,
payload: uid
});
export const authFailed = (resp) => ({
type: ActionTypes.AUTH_FAILED,
payload: resp
});
And I am calling it from a component as shown below:
const mapStateToProps = state => {
return {
isAuth: state.isAuth,
user: state.user
}
}
const mapDispatchToProps = dispatch => ({
getAuth: () => { dispatch(getAuth()) },
loginReq: (email, password, remember) => { dispatch(loginReq(email, password, remember)) },
logoutReq: () => { dispatch(logoutReq()) }
})
handleLogin() {
this.props.loginReq(this.state.email, this.state.password, this.state.remember);
}
handleLogOut() {
this.props.logoutReq();
}
<BUTTON onClick=()=>this.handleLogOut()/handleLogin()>
I am close to tears because I cannot figure out why my loginReq fires one or many gitAuth() methods even when i click on the button once. This happens only for the loginReq() action. I have not specified anywhere that loginReq() should fire it.
Also i have called the getAuth() method in the component did mount method of my main screen which checks authentication status once at the start of the app.
EDIT: I have console logged in the component did mount method in the main component so I know that this getAuth() call is not coming from there.
Imo the answer is badly done, try to reestructure it better, what you call "Thunks" are actually "Actions". But if I were to tell you something that could help is that maybe the problem lies in the thunk middleware config or with the way firebase is beign treated by the dispatcher, so I would say that you better try coding an apporach with the react-redux-firebase library (this one: http://react-redux-firebase.com/docs/getting_started ) it makes easier to connect redux with a firebase back end. Other great reference, the one that I learned with, is The Net Ninja's tutorial playlist about react, redux and firebase.
A friend of mine told me this has to do with something known as an 'Observer' which is in the onAuthStateChange() provided by firebase. Basically there is a conflict between me manually considering the user as authenticated and the observer doing so.
I am using Redux with react and redux-thunk as a middleware.
When I make http requests I have to dispatch three actions in my thunks.
I will use my auth example.
here are my actions:
export const loginSuccess = () => ({
type: AUTH_LOGIN_SUCCESS,
})
export const loginFailure = (errorMessage) => ({
type: AUTH_LOGIN_FAILURE,
errorMessage,
})
export const loginRequest = () => ({
type: AUTH_LOGIN_REQUEST,
})
and here is the thunk which combines above three actions:
export const login = (credentials) => dispatch => {
dispatch(loginRequest())
const options = {
method: 'post',
url: `${ENDPOINT_LOGIN}?username=${credentials.username}&password=${credentials.password}`,
}
axiosInstance(options)
.then(response => {
dispatch(loginSuccess())
dispatch(loadUser(response.data)) // I have separate action for user and separate reducer.
window.localStorage.setItem(ACCESS_TOKEN_KEY, response.data.token)
})
.catch(error => {
return dispatch(loginFailure(error))
})
}
And here is my reducer:
const initialState = {
pending: false,
error: false,
errorMessage: null,
}
export const loginReducer = (state = initialState, action) => {
switch (action.type) {
case AUTH_LOGIN_SUCCESS:
return {
...state,
pending: false,
error: false,
errorMessage: null,
}
case AUTH_LOGIN_FAILURE:
const { errorMessage } = action
return {
...state,
pending: false,
error: true,
errorMessage,
}
case AUTH_LOGIN_REQUEST:
return {
...state,
pending: true,
}
default:
return state
}
}
I have to do almost exact same things when I am sending another request, for example in case of logout. I feel like I am repeating myself a lot and there must be a better way.
I need to know what is the best practice to handle this issue.
Any other corrections and recommendations will be appreciated.
If you are looking for "ready to use" solution take a look at:
https://redux-toolkit.js.org/api/createAsyncThunk
https://redux-resource.js.org/ (but it is written with js (not TS), and no #types definition for this library)
If you are looking for a custom solution you can create a few factories:
factory for reducer
factory for three actions
factory for thunk
const actions = createActions('My request name');
const reducer = createReducer(actions);
...
const thunk = createThunk(config);
or even you can combine them:
const { actions, reducer, thunk } = createRequestState('Name...', config);
... but this is just an idea.
Recently I've been looking into react and redux. I read up the official documentation and tried some ToDo List tutorials. Part 1 is just about react and this is part 2 about redux:
http://www.theodo.fr/blog/2016/03/getting-started-with-react-redux-and-immutable-a-test-driven-tutorial-part-2/
So basically he just sets up a store and initially adds an array of a few todos. Now I don't want my data to be local and I want to fetch it from an API. I'm having a hard time understanding how this actually works. So the code I would use in my action_creators.js is:
export function fetchData() {
return dispatch => {
fetch('http://127.0.0.1:8000/example')
.then(res => res.json())
.then(res => dispatch({
type: FETCH_DATA,
data: res
}))
}
}
Now in the example code for example adding a 'todo':
export function addItem(text) {
return {
type: 'ADD_ITEM',
text
}
}
You aren't dispatching anything, the tutorial does this in the reducer? But when you return dispatch your fetch, does this automatically get dispatched to your store?
If so I have no clue what I should write in my reducer ..
This is the code I have for adding a 'todo':
import {Map} from 'immutable';
function setState(state, newState) {
return state.merge(newState);
}
function fetchData(state) {
return state;
}
function addItem(state, text) {
const itemId = state.get('hostnames').reduce((maxId, item) => Math.max(maxId,item.get('id')), 0) + 1;
const newItem = Map({id: itemId, text: text, status: 'active'});
return state.update('hostnames', (hostnames) => hostnames.push(newItem));
}
export default function(state = Map(), action) {
switch (action.type) {
case 'SET_STATE':
return setState(state, action.state);
case 'ADD_ITEM':
return addItem(state, action.text);
case 'FETCH_DATA':
return fetchData(state);
}
return state;
}
So basically my question is, how do I fetch the data ( if the fetch is wrong now ) and how do I add the fetched data from my api to the store in my reducer.
I just find react and redux pretty complicated so sorry if I'm asking a really noob question or just making big mistakes in the way I want to do something.
Thanks in advance for any help.
imagine your json
{
"data": {
"apple": 1,
"banana": 3,
},
"status": 200,
}
your actions
export function fetchData() {
return dispatch => {
fetch('http://127.0.0.1:8000/example')
.then(res => res.json())
.then((responseData) => {
if(responseData.status === 200){
dispatch(setData(responseData));
}
})
}
}
export function setData(responseData) {
return {type: SET_DATA, data: responseData.data }
}
your reducer
const initialState = { data: null };
export default function(state = initialState, action) {
switch (action.type) {
case 'SET_DATA':
return Object.assign({}, state, {
data: action.data,
})
default:
return state;
}
}
then your state will become
{ data: {
apple: 1,
banana: 3,
}
}
Actually, all your reducers should be pretty dumb and pure (without any side effects). So their only concern is to modify the state and nothing else. Fetching data from the server or any kind of orchestration should be implemented in redux middleware. Look at redux-thunk or redux-saga if you need something more complicated. Hope that helps.