why are my reducers called twice? Please find firste the source code and below a screenshot from developer tools.
Code:
import { createStore } from 'redux';
const initState = {
title: "-",
todos: []
}
function rootReducer(state = initState, action) {
console.log("rootReducer, state=", state, "rootReducer, action=", action);
if (action.type === "ADD_TODO") {
return {
...state,
todos: [...state.todos, action.todo],
}
}
return state;
}
const store = createStore(rootReducer);
store.subscribe(() => {
console.log("State updated!", store.getState());
});
store.dispatch({ type: 'ADD_TODO', todo: 'Learn Babel' });
Developer tools:
Thank you
The first invocation is from createStore(rootReducer) trying to test your reducer for bad patterns.
Related
I am using React with Redux. The Redux devtool console shows that data exists in the state (redux devtools console), but the webpage displays an error saying that the object is undefined (error).
This is my code for my screen:
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { listProductDetails } from "../redux/actions/productActions";
const ProductScreen = ({ match }) => {
const dispatch = useDispatch();
const productDetails = useSelector((state) => state.productDetails);
const { loading, error, product } = productDetails;
useEffect(() => {
dispatch(listProductDetails(match.params.id));
}, [dispatch, match]);
return <div>{product.name}</div>;
};
export default ProductScreen;
This is the code for my redux reducer:
import {
PRODUCT_DETAILS_FAIL,
PRODUCT_DETAILS_REQUEST,
PRODUCT_DETAILS_SUCCESS,
} from "../constants";
export const productDetailsReducer = (state = { product: {} }, action) => {
switch (action.type) {
case PRODUCT_DETAILS_REQUEST:
return { loading: true };
case PRODUCT_DETAILS_SUCCESS:
return { loading: false, product: action.payload };
case PRODUCT_DETAILS_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
This is the code for my action:
import axios from "axios";
import {
PRODUCT_DETAILS_FAIL,
PRODUCT_DETAILS_REQUEST,
PRODUCT_DETAILS_SUCCESS,
} from "../constants";
export const listProductDetails = (id) => async (dispatch) => {
try {
dispatch({
type: PRODUCT_DETAILS_REQUEST,
});
const { data } = await axios.get(`/api/products/${id}`);
dispatch({
type: PRODUCT_DETAILS_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: PRODUCT_DETAILS_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
I really cannot find the issue here, any help would be greatly appreciated!
I think the problem is when you dispatch PRODUCT_DETAILS_REQUEST action, reducer will override the state value with { loading: true }, and so product will be undefined instead of empty object {}.
So you should return merged object with the previous state in the reducer. e.g. return { ...state, loading: true };
Hope it could help you.
export const productDetailsReducer = (state = { product: {} }, action) => {
switch (action.type) {
case PRODUCT_DETAILS_REQUEST:
return { ...state, loading: true };
case PRODUCT_DETAILS_SUCCESS:
return { ...state, loading: false, product: action.payload };
case PRODUCT_DETAILS_FAIL:
return { ...state, loading: false, error: action.payload };
default:
return state;
}
};
I am trying to fetch some data in a react component using the useEffect hook. After the initial render, fetchItems() gets the items and updates the store. However, items is still an empty object even after the store updates.
I might be using useEffects wrong. How do you use Redux with useEffects? I want to set a loading state for the component, but since the component only dispatches an action to fetch items (instead of directly calling the API), it does not know when the data is fetched and the store is updated so it can pull it.
Can someone please help figure out how to make sure that items object is updated after the saga fetch and the subsequent store update?
import React, { useState, useEffect } from "react";
import { connect } from 'react-redux';
import { useParams } from "react-router-dom";
const ItemComponent = ({ item, fetchItem }) => {
const { itemId } = useParams();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true)
fetchItem(itemId)
setIsLoading(false)
}, []);
console.log(item) // gives empty object even after the fetch and store update
}
const mapStateToProps = (state) => {
return {
item: state.item
}
}
const mapDispatchToProps = (dispatch) => {
return {
fetchItem: (itemId) => { dispatch(fetchItemActionCreator(itemId)) }
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ItemComponent);
fetchItemActionCreator is an action creator that creates the action to be dispatched.
My reducer and saga work fine as I can see the store actions and updates in the console.
If I pass the items object into the dependency array for useEffect, then there will be an infinite loop and the page keeps re-rendering.
Reducer:
const itemReducer = (state={}, { type, payload }) => {
switch(type) {
case ITEM_GET_SUCCESS:
return {...state, ...payload}
default: return state
}
}
fetchItemActionCreator:
import { createAction } from '#reduxjs/toolkit';
export const fetchItemActionCreator = createAction(ITEM_GET_PENDING);
Thank you very much in advance!
I want to set a loading state for the component
/** Action */
const getItem = () => dispatch => {
dispatch({ type: 'GET_ITEM_START' });
axios
.get('your api end point')
.then(res => {
const item = res.data;
dispatch({
type: 'GET_ITEM_SUCCESS',
payload: {
item,
},
});
})
.catch(error => {
dispatch({
type: 'GET_ITEM_FAIL',
payload: error,
});
});
};
/** Reducer */
const INITIAL_STATE = {
item: null,
error: '',
loading: false,
};
const itemReducer = (state = INITIAL_STATE, { type, payload }) => {
switch (type) {
case 'GET_ITEM_START':
return { ...state, error: '', loading: true };
case 'GET_ITEM_SUCCESS':
return { ...state, ...payload, loading: false };
case 'GET_ITEM_FAIL':
return { ...state, error: payload, loading: false };
default:
return state;
}
};
Then your could handle Loading state in your component
const ItemComponent = ({ fetchItem, item, loading, error }) => {
/** ... */
/**
Check for loading and show a spinner or anything like that
*/
useEffect(() => {
fetchItem(itemId);
}, []);
if (loading) return <ActivityIndicator />;
if (item) return <View>{/* renderItem */}</View>;
return null;
};
I am trying to access the redux state to display its value on my website. I am using React redux hooks with functional components and Typescript.
Situation:
I have a store with two reducers: UI and user. The initial state is:
{
user: {
authenticated: false,
credentials: {}
},
UI: {
loading: false,
errors: null
}
}
When the user signs in, the signinUser action takes place and correctly changes the redux state. For example, for an invalid signin, the redux state is:
{
user: {
authenticated: false,
credentials: {}
},
UI: {
loading: false,
errors: {
general: 'wrong credentials, please try again'
}
}
}
Problem:
I am trying to acces the UI.errors so I can display them on my website. i have a function in my Signin component thnamed submitForm that calls the signinUser action that correctly dispatches the actions. My problem is that after that I want to retrieve the state.ui.errors and I can't figure out how to.
I have tried all this:
componentWillRecieveProps(nextProps) { ... } this solution is for class components and I am using functional components
useSelector((state: StoreState) => state.UI); If I do it inside submitForm is invalid because React Hooks don't allow to call inside a function. If I do it outside, it fetches the old state.
Here are my files (the parts related to this issue)
store.tsx
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
// Reducers
import userReducer from './reducers/userReducer';
import uiReducer from './reducers/uiReducer';
const initialState = {};
const middleware = [thunk];
const reducers = combineReducers({
user: userReducer,
UI: uiReducer
});
const store = createStore(
reducers,
initialState,
compose(
applyMiddleware(...middleware),
(window as any).__REDUX_DEVTOOLS_EXTENSION__ &&
(window as any).__REDUX_DEVTOOLS_EXTENSION__()
)
);
export default store;
userActions.tsx
import { SET_USER, SET_ERRORS, CLEAR_ERRORS, LOADING_UI } from '../types';
import axios from 'axios';
// Interfaces
import { ISigninForm } from '../../utils/types';
// Redux
import { Dispatch } from 'redux';
import { useDispatch } from 'react-redux';
export const signinUser = (
userData: ISigninForm,
dispatch: Dispatch,
handleDialogClose: () => void
) => {
console.log('signinuser in userActions');
dispatch({ type: LOADING_UI });
axios
.post('/signin', userData)
.then((res) => {
const FBIdToken = `Bearer ${res.data.token}`;
localStorage.setItem('FBIdToken', FBIdToken);
axios.defaults.headers.common['Authorization'] = FBIdToken;
getUserData(dispatch);
dispatch({ type: CLEAR_ERRORS });
handleDialogClose();
// history.push("/profile"); // this will redirect to a page not built yet
})
.catch((err) => {
dispatch({
type: SET_ERRORS,
payload: err.response.data
});
});
};
export const getUserData = (dispatch: Dispatch) => {
console.log('getUserData');
axios
.get('/user')
.then((res) => {
console.log('/user', res);
dispatch({
type: SET_USER,
payload: res.data
});
})
.catch((err) => console.log('err', err));
};
uiReducer.tsx
import { SET_ERRORS, CLEAR_ERRORS, LOADING_UI, IAction } from '../types';
const initialState = {
loading: false,
errors: null
};
export default function (state = initialState, action: IAction) {
switch (action.type) {
case SET_ERRORS:
return {
...state,
loading: false,
errors: action.payload
};
case CLEAR_ERRORS:
return {
...state,
loading: false,
errors: null
};
case LOADING_UI:
return {
...state,
loading: true
};
default:
return state;
}
}
userReducer.tsx
import {
SET_USER,
SET_AUTHENTICATED,
SET_UNAUTHENTICATED,
IAction
} from '../types';
const initialState = {
authenticated: false,
credentials: {}
};
export default function (state = initialState, action: IAction) {
switch (action.type) {
case SET_AUTHENTICATED:
return {
...state,
authenticated: true
};
case SET_UNAUTHENTICATED:
return initialState;
case SET_USER:
console.log('SET_USER', action);
return {
authenticated: true,
...action.payload
};
default:
return state;
}
}
Signin.tsx
function Signin({ history }: RouteComponentProps): JSX.Element {
// States
const [dialogOpen, setDialogOpen] = React.useState(false);
const [errorsAPI, setErrorsAPI] = React.useState<ISigninErrors>({});
const [loading, setLoading] = React.useState(false);
// Dialog
const handleDialogOpen = () => {
setDialogOpen(true);
};
const handleDialogClose = () => {
setDialogOpen(false);
};
// Form
const { register, handleSubmit, errors } = useForm<ISigninForm>();
const submitForm = (data: ISigninForm) => {
signinUser(data, dispatch, handleDialogClose);
};
return (
// HTML content
);
}
export default withRouter(Signin);
Solution:
I had the solution in front of me this whole time, but I was not using the function in the right way.
const state = useSelector((state: StoreState) => state);
This is called inside the Signin function component. Then when I am returning the HTML object, I just call
{state.UI.errors !== null && 'general' in state.UI.errors && (
<p>{state.UI.errors.general}</p>
)}
using Redux and am now straggling with a signin and signout button while using oauth.
When I press on the button to logIn, the popup window appears and I can choose an account. But in the meantime the webpage throws an error.
I got the following error as stated in the title:
Error: Actions must be plain objects. Use custom middleware for async actions.
I am using hooks, in this case useEffect().then() to fetch the data.
1) Why?
2) Also do not know, why I am getting a warning: The 'onAuthChange' function makes the dependencies of useEffect Hook (at line 35) change on every render. Move it inside the useEffect callback. Alternatively, wrap the 'onAuthChange' definition into its own useCallback() Hook react-hooks/exhaustive-deps
Here is my code:
GoogleAuth.js
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { signIn, signOut } from "../actions";
const API_KEY = process.env.REACT_APP_API_KEY;
const GoogleAuth = () => {
const isSignedIn = useSelector((state) => state.auth.isSignedIn);
console.log("IsSignedIn useSelector: " + isSignedIn);
const dispatch = useDispatch();
const onAuthChange = () => {
if (isSignedIn) {
dispatch(signIn());
} else {
dispatch(signOut());
}
};
useEffect(
() => {
window.gapi.load("client:auth2", () => {
window.gapi.client
.init({
clientId: API_KEY,
scope: "email"
})
.then(() => {
onAuthChange(window.gapi.auth2.getAuthInstance().isSignedIn.get());
console.log("isSignedIn.get(): " + window.gapi.auth2.getAuthInstance().isSignedIn.get());
window.gapi.auth2.getAuthInstance().isSignedIn.listen(onAuthChange);
});
});
},
[ onAuthChange ]
);
const onSignInOnClick = () => {
dispatch(window.gapi.auth2.getAuthInstance().signIn());
};
const onSignOutOnClick = () => {
dispatch(window.gapi.auth2.getAuthInstance().signOut());
};
const renderAuthButton = () => {
if (isSignedIn === null) {
return null;
} else if (isSignedIn) {
return (
<button onClick={onSignOutOnClick} className="ui red google button">
<i className="google icon" />
Sign Out
</button>
);
} else {
return (
<button onClick={onSignInOnClick} className="ui red google button">
<i className="google icon" />
Sign In with Google
</button>
);
}
};
return <div>{renderAuthButton()}</div>;
};
export default GoogleAuth;
reducer/index.js
import { combineReducers } from "redux";
import authReducer from "./authReducer";
export default combineReducers({
auth: authReducer
});
reducers/authReducer.js
import { SIGN_IN, SIGN_OUT } from "../actions/types";
const INITIAL_STATE = {
isSignedIn: null
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case SIGN_IN:
return { ...state, isSignedIn: true };
case SIGN_OUT:
return { ...state, isSignedIn: false };
default:
return state;
}
};
actions/index.js
import { SIGN_IN, SIGN_OUT } from "./types";
export const signIn = () => {
return {
type: SIGN_IN
};
};
export const signOut = () => {
return {
type: SIGN_OUT
};
};
types.js
export const SIGN_IN = "SIGN_IN";
export const SIGN_OUT = "SIGN_OUT";
The reason of the first error is that, inside both onSignInOnClick and onSignInOnClick, dispatch() receives a Promise (since window.gapi.auth2.getAuthInstance().signIn() returns a Promise).
There are different solution to handle effects in redux, the simplest are redux promise or redux thunk.
Otherwise you can dispatch the { type: SIGN_IN } action, and write a custom middleware to handle it.
The reason of the second error, is that the onAuthChange is redefined on every render, as you can see here:
const f = () => () => 42
f() === f() // output: false
Here's a possible solution to fix the warning:
useEffect(() => {
const onAuthChange = () => {
if (isSignedIn) {
dispatch(signIn())
} else {
dispatch(signOut())
}
}
window.gapi.load('client:auth2', () => {
window.gapi.client
.init({
clientId: API_KEY,
scope: 'email',
})
.then(() => {
onAuthChange(window.gapi.auth2.getAuthInstance().isSignedIn.get())
console.log(
'isSignedIn.get(): ' +
window.gapi.auth2.getAuthInstance().isSignedIn.get(),
)
window.gapi.auth2.getAuthInstance().isSignedIn.listen(onAuthChange)
})
})
}, [isSignedIn])
I created a rootSaga in sagas.js as
function* fetchStuff(action) {
try {
yield put({type: 'INCREMENT'})
yield call(delay, 1000)
yield put({type: 'DECREMENT'})
const highlights = yield call(API.getStuff, action.data.myObject);
} catch (e) {
yield put({type: 'FETCH_STUFF_FAILED', message: e});
}
}
export default function* rootSaga() {
yield takeEvery('INIT_LOAD', fetchStuff);
}
I am calling the INIT_LOAD after thirdParty.method:
class myClass extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.load();
}
load = () => {
this.init = () => {
this.myObject = thirdParty.method(event => {
const action = {
type: 'INIT_LOAD',
payload: {
myObject: this.myObject
}
};
store.dispatch(action);
});
};
this.init();
};
render() {
return (
<div id="render-here" />
);
}
Passing the this.myObject in the action that is dispatched does not trigger the saga. If I change the action payload to a string, like the following, the saga is triggered.
const action = {
type: 'INIT_LOAD',
payload: {
myObject: 'this.myObject'
}
};
Why am I unable to pass this.myObject but a string is ok?
UPDATE: It is not a saga issue. I replicated the same issue with just plain redux. The rootReducer as
export default function rootReducer(state = initialState, action) {
switch (action.type) {
case 'INIT_LOAD':
return Object.assign({}, state, { myObject: action.payload.myObject });
default:
return state;
}
}
As I mentioned in the comment below, assigning it to an object Obj does not change the issue
let Obj = {};
...
load = () => {
this.init = () => {
Obj.myObject = thirdParty.method(event => {
const action = {
type: 'INIT_LOAD',
payload: {
myObj: Obj
}
};
store.dispatch(action);
});
};
this.init();
};
UPDATE2
I cleaned the code up & simply dispatched an action in the component that triggers the saga. Inside the saga is where I do the init(). I ran into another issue where the object that I was trying to save in the redux store has active socket sessions (which were given me cross-domain issues). Although I didn't solve my original problem, not storing a socket object made my problem go away.