https://github.com/reduxjs/redux/issues/3017
Problem: Occurs when I wrap my action creator with a dispatch in the container area where I utilize the connect method--I followed the style from redux documentation.
I am utilizing redux, and redux thunk. I am attempting to create a login action, so far it does not work when I dispatch an action, which dispatch's an another one.
LoginContainer.js
import CONFIG from "../../../config";
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import {authenticateUser} from "../../../actions/authenticateUser";
import Login from '../../../components/views/login/Login'
import {store} from '../../../store';
function handleSubmit(e) {
e.preventDefault();
let calpersId = parseInt(e.target[0].value || e.target[1].value, 10) || 0;
store.dispatch(authenticateUser(calpersId))
}
const mapStateToProps = (state) => {
return {
authentication: state.authentication
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleSubmit: (e) => {dispatch(handleSubmit(e))}
}
}
const LoginContainer = connect(mapStateToProps, mapDispatchToProps)(Login);
export default LoginContainer;
authenticateUser.action.js
import CONFIG from '../config'
export const AUTHENTICATE_USER = 'AUTHENTICATE_USER'
export const initiateUserAuthentication = (token) => ({
type: AUTHENTICATE_USER,
token
})
export const AUTHENTICATATION_SUCCEEDED = 'AUTHENTICATATION_SUCCEEDED'
export const authenticatationSucceeded = (payload) => ({
type: AUTHENTICATE_USER,
payload
})
export const USER_ID_DOES_NOT_EXIST = 'USER_ID_DOES_NOT_EXIST'
export const userIdDoesNotExist = (uid) => ({
type: USER_ID_DOES_NOT_EXIST,
uid,
message: "User id does not exist"
})
export function authenticateUser(id) {
return function (dispatch) {
let guidMap = {
7103503579: "dad08fde-0ac1-404a-ba8a-cc7c76d5810f",
6632408185: "6632408185-guid",
6581985123: "6581985123-guid",
1226290314: "a3908aa7-c142-4752-85ea-3741cf28f75e",
4618604679: "4618604679-guid",
6452522440: "6452522440-guid",
3685610572: "3685610572-guid",
5564535492: "5564535492-guid",
5600493427: "5600493427-guid",
3996179678: "3996179678-guid",
7302651964: "7302651964-guid",
3148148090: "3148148090-guid",
5826752269: "5826752269-guid",
6827859055: "6827859055-guid",
1677401305: "1677401305-guid",
2640602392: "dbed1af6-0fc9-45dc-96a3-ab15aa05a7a2",
6474994805: "6474994805-guid"
};
let guid = guidMap[id]
return fetch(CONFIG.API.MY_CALPERS_SERVER.LOCATION + 'ept/development/rest/simulatedAuth.json?guid=' + guid, {
credentials: 'include'
})
.then(
response => response.json(),
error => console.log('An error occured.', error))
.then(json => {
document.cookie = "authentication=" + guid + "; max-age=" + (60 * 30);
dispatch(authenticatationSucceeded(json))
})
}
}
authenticateUser.reducer.js
import {AUTHENTICATE_USER, AUTHENTICATATION_SUCCEEDED} from "../actions/authenticateUser";
const initialState = {
calpersIds: [
5600493427,
6474994805,
6452522440,
5564535492,
6632408185,
4618604679,
5826752269,
3996179678,
7302651964,
1677401305,
6827859055,
3685610572,
6581985123,
3148148090
],
guidMap: {
7103503579: "dad08fde-0ac1-404a-ba8a-cc7c76d5810f",
6632408185: "6632408185-guid",
6581985123: "6581985123-guid",
1226290314: "a3908aa7-c142-4752-85ea-3741cf28f75e",
4618604679: "4618604679-guid",
6452522440: "6452522440-guid",
3685610572: "3685610572-guid",
5564535492: "5564535492-guid",
5600493427: "5600493427-guid",
3996179678: "3996179678-guid",
7302651964: "7302651964-guid",
3148148090: "3148148090-guid",
5826752269: "5826752269-guid",
6827859055: "6827859055-guid",
1677401305: "1677401305-guid",
2640602392: "dbed1af6-0fc9-45dc-96a3-ab15aa05a7a2",
6474994805: "6474994805-guid"
},
authToken: null,
isAuthenticated: false
};
//#TODO: All fetches, create a seperate reducer for store?
export function authenticateUser(state = initialState, action) {
switch(action.type) {
case AUTHENTICATE_USER:
return Object.assign({}, state, {
authToken: action.token,
})
case AUTHENTICATATION_SUCCEEDED:
return Object.assign({}, state, {
authToken: action.payload.guid,
isAuthenticated: true,
payload: action.payload
})
default:
return state;
}
};
You should'nt use connect mapDispatchToProps like you are doing.
This callback is supposed to create or use functions that will dispatch an action.
For your case you can use it like that:
const mapDispatchToProps = (dispatch) => {
return {
authenticate: calpersId => authenticateUser(calpersId)(dispatch)
}
}
And in your component have a function/method that handle the submit:
class Login extends Component {
...
handleSubmit = e => {
e.preventDefault();
const calpersId = parseInt(e.target[0].value || e.target[1].value, 10) || 0;
this.props.authenticate(calpersId)
}
...
By the way a reducer is supposed to represent the state of an entity. An entity named autenticateUser is pretty ambigious. You should propably named it user. You should read more redux examples to really catch the concept that at first a bit complicated to understand. There are good videos on Youtube.
Turns out I was calling an action creator which did not exist, I simply needed to pass my dispatch to the handler, and let it handle the the event.
Login.js
import CONFIG from "../../../config";
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import {authenticateUser} from "../../../actions/authenticateUser";
import Login from '../../../components/views/login/Login'
function handleSubmit(e, dispatch) {
e.preventDefault();
let calpersId = parseInt(e.target[0].value || e.target[1].value, 10) || 0;
dispatch(authenticateUser(calpersId))
}
const mapStateToProps = (state) => {
return {
authentication: state.authentication
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleSubmit: (e) => {handleSubmit(e, dispatch)}
}
}
const LoginContainer = connect(mapStateToProps, mapDispatchToProps)(Login);
export default LoginContainer;
What is the proper way of doing this, I utillized bindActionCreators which yields the same result.
Related
When I call me thunk from app.tsx, it returns this error:
> Build error occurred
TypeError: Cannot read property 'useContext' of null
at exports.useContext (/Users/kukodajanos/Workspace/Tikex/Portal/Team/node_modules/react/cjs/react.production.min.js:24:118)
at useReduxContext (/Users/kukodajanos/Workspace/Tikex/Portal/Team/node_modules/react-redux/lib/hooks/useReduxContext.js:27:46)
Why app.tsx is specific to all other component?
import { useAppDispatch } from '../hooks'
import { me } from '../tikexModule/slices/tikexAPI'
const MyApp: Page = ({ Component, pageProps }: AppPropsWithLayout) => {
const dispatch = useAppDispatch()
useEffect(() => {
dispatch(me())
}, [])
return <></>
}
export default MyApp
And the thunk itself:
export const me = createAsyncThunk(`${namespace}/me`, async () => {
const { data } = await axios({
method: 'get',
url: 'me',
headers: { crossDomain: true },
})
return data
})
and the action:
const tikexAPI = createSlice({
name: 'tikexAPI',
initialState,
reducers: {
},
extraReducers: (builder) => {
builder
.addCase(me.fulfilled, (state, { payload }) => {
state.authnRes = payload
})
and hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'
import { createSelector } from '#reduxjs/toolkit'
import {
authnResS,
userDTOS,
organizationsS,
selectedOrganizationIdS,
selectedOrganizationS,
selectedEventIdS,
selectedEventS,
} from './tikexModule/slices/tikexAPI'
import {
AuthnRes,
OrganizationDTO,
OrganizationList,
SoldTicketRes,
UserDTO,
ProgramDTO,
} from './tikexModule/Types'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>()
I believe the error is in dispatch, where you dispatch a function call of the thunk, instead of passing the thunk function itself.
const thunkFunction = (dispatch, getState) => {
// logic here that can dispatch actions or read state
}
store.dispatch(thunkFunction)
Note that thunkFunction has no parenthesis () to invoke the thunk.
In your case me() should be without parenthesis:
useEffect(() => {
dispatch(me)
}, [])
I don't work with thunks normally but I believe the reason that works is that a thunk has a toString() function that exposes the action type `${namespace}/me`.
I am making a cart functionality using redux toolkit's createSlice. But in the browser's application tab, the value of the localStorage is showing as [object Object]. Can someone help me with this please?
cartSlice.js
import { createSlice } from '#reduxjs/toolkit';
import axios from 'axios'
const cartItemsFromStorage = localStorage.getItem('cartItems') ? localStorage.getItem('carts') : []
export const cartSlice = createSlice({
name: 'cart',
initialState: {
cartItems: cartItemsFromStorage,
},
reducers: {
add: (state, action) => {
const item = action.payload
const existItem = state.cartItems.find(x => x.product === item.product)
if (existItem) {
const currentItems = state.cartItems.map(x => x.product === existItem.product ? item : x)
state.cartItems = [...currentItems]
} else {
state.cartItems = [...state.cartItems, item]
localStorage.setItem('cartItems', state.cartItems)
}
},
// remove: (state, action) => {
// },
},
});
const { add } = cartSlice.actions;
export const selectCartItems = state => state.cart.cartItems;
export const addToCart = (id, qty) => async (dispatch) => {
const { data } = await axios.get(`/api/products/${id}`)
dispatch(add({
product: data._id,
name: data.name,
imgae: data.imgae,
price: data.price,
countInStock: data.countInStock,
qty
}))
}
export default cartSlice.reducer;
CartScreen.js
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { selectCartItems, addToCart } from '../features/cartSlice'
const CartScreen = ({ match, location, history }) => {
const productId = match.params.id
const qty = location.search ? Number(location.search.split('=')[1]) : 1
const dispatch = useDispatch()
const cartItems = useSelector(selectCartItems)
useEffect(() => {
if (productId) {
dispatch(addToCart(productId, qty))
}
console.log(`cartItems: ${cartItems}`)
}, [dispatch, productId, qty])
return (
<div>
Cart
</div>
)
}
export default CartScreen
The next time I refresh the page, the initialState is not there, insted it shows [object Object]. I know the problem is with localStorage. Please correct me.
I think the problem is you are doing localStorage stuff in a reducer action. Reducer only can do simple operations and modify the state, so I encourage you to try to pass localStorage calls into the thunk action.
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>
)}
In my application I want to add a 'ticket' to an array in the 'event' object. In the action I post the new ticket to the database, and after that I dispatch the action to the reducer. By using the Redux logger, I am able to retrieve the error:
The action of 'createTicket' is this:
// actions/tickets.js
export const TICKET_CREATE_SUCCESS = 'TICKET_CREATE_SUCCESS';
const ticketCreateSuccess = tickets => ({
type: TICKET_CREATE_SUCCESS,
tickets
});
export const createTicket = (eventId, data) => (dispatch, getState) => {
const jwt = getState().currentUser.token;
const id = getState().currentUser.userId;
const email = getState().currentUser.email;
const name = getState().currentUser.name;
request
.post(`${baseUrl}/events/${eventId}/tickets`)
.set('Authorization', `Bearer ${jwt}`)
.send(data)
.then(response => {
dispatch(ticketCreateSuccess({ ...response.body, user: { id, email, name } }));
})
.catch(error => error);
};
The reducer
// reducers/events.js
import { EVENT_FETCHED } from '../actions/events';
import { TICKET_EDIT_SUCCESS, TICKET_CREATE_SUCCESS } from '../actions/tickets';
export default (state = null, action = {}) => {
switch (action.type) {
case EVENT_FETCHED:
return action.event;
case TICKET_EDIT_SUCCESS:
return {
...state,
tickets: state.tickets.map(ticket => {
if (ticket.id === action.ticket.id) {
return action.ticket;
}
return ticket;
})
};
case TICKET_CREATE_SUCCESS:
console.log({ ...state, tickets: [...state.tickets, action.tickets] });
return { ...state, tickets: [...state.tickets, action.tickets] };
default:
return state;
}
};
The reducers are combined into :
import { combineReducers } from 'redux';
import currentUser from './currentUser';
import events from './events';
import event from './event';
import ticket from './ticket';
import tickets from './tickets';
import numberOfTickets from './numberOfTickets';
export default combineReducers({ currentUser, events, event, ticket, tickets, numberOfTickets });
Could it be that you're trying to spread your reducer state when its value is null:
export default (state = null, action = {}) => {
return {
...state, // Here
// rest
}
Your default state should probably be an object, e.g.:
const InitialState = {
tickets: []
};
export default (state = InitialState, action) => {
// Some code
case TICKET_CREATE_SUCCESS:
return {
...state,
tickets: [
...state.tickets,
action.tickets
]
}
}
just add this ...state || []
and you are good to go.
the problem is value of ...state equals null with empty array and when you try to iterate over null it creates an error.
so use and "OR" operator and it will work fine.
I'm new to react redux, so I think I'm just missing something basic.
I have three reducers, two to handle orders that update in the store as arrays, and one that shows the status of a web socket connection I'm using to receive orders from the server.
// reducers.js
import { combineReducers } from 'redux'
import { ADD_POS_ORDER, ADD_MOBILE_ORDER, UPDATE_WS_STATUS, wsStatuses } from '../actions/actions'
const { UNINITIALIZED } = wsStatuses
const posOrders = (state = [], action) => {
switch (action.type) {
case ADD_POS_ORDER:
return [
...state,
{
id: action.order.id,
status: action.order.status,
name: action.order.name,
pickupNum: action.order.pickupNum
}
]
default:
return state
}
}
const mobileOrders = (state = [], action) => {
switch (action.type) {
case ADD_MOBILE_ORDER:
return [
...state,
{
id: action.order.id,
status: action.order.status,
name: action.order.name,
pickupNum: action.order.pickupNum
}
]
default:
return state
}
}
const wsStatus = (state = UNINITIALIZED, action) => {
switch (action.type) {
case UPDATE_WS_STATUS:
return action.status
default:
return state
}
}
const displayApp = combineReducers({
posOrders,
mobileOrders,
wsStatus
})
export default displayApp
When I connect to the socket, I dispatch an action to update wsStatus and the action is stored as 'CONNECTED'.
When I follow with an order with the posOrders reducer, the wsStatus is reset to its default, 'UNINITIALIZED'.
What I am struggling to understand is why wsStatus is not using the previous state of 'CONNECTED', but instead returning default.
// actions.js
export const UPDATE_WS_STATUS = 'UPDATE_WS_STATUS'
export const wsStatuses = {
UNINITIALIZED: 'UNINITIALIZED',
CONNECTING: 'CONNECTING',
CONNECTED: 'CONNECTED',
DISCONNECTED: 'DISCONNECTED'
}
export const ADD_POS_ORDER = 'ADD_POS_ORDER'
export const ADD_MOBILE_ORDER = 'ADD_MOBILE_ORDER'
export const UPDATE_POS_ORDER = 'UPDATE_POS_ORDER'
export const setWsStatus = (status) => {
return {
type: 'UPDATE_WS_STATUS',
status: status
}
}
export const updateOrderQueue = (action, order) => {
return {
type: action,
id: order.id,
order: order,
receivedAt: Date.now()
}
}
Here's where I make the calls:
// socketListeners.js
import { setWsStatus } from '../actions/actions'
import SockJS from 'sockjs-client'
export const socket = new SockJS('http://localhost:3000/echo')
export default function (dispatch, setState) {
socket.onopen = function () {
dispatch(setWsStatus('CONNECTED'))
}
socket.onclose = function () {
dispatch(setWsStatus('DISCONNECTED'))
}
}
// orders container
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { socket } from '../helpers/socketListeners'
import { updateOrderQueue, setWsStatus } from '../actions/actions'
import PosOrder from '../components/queue/PosOrder'
class PosOrderList extends Component {
constructor(props) {
super(props)
}
componentDidMount() {
const { dispatch } = this.props
socket.onmessage = function(e) {
// convert order info to object
let parsedOrder = JSON.parse(e.data)
let action = parsedOrder.action
let order = parsedOrder.order
dispatch(updateOrderQueue(action, order))
}
}
render() {
const { updateOrderQueue } = this.props
return (
<ul>
{this.props.posOrders.map(posOrder =>
<PosOrder
key={posOrder.id}
{...posOrder}
/>
)}
</ul>
)
}
}
PosOrderList.propTypes = {
posOrders: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.hash,
status: PropTypes.string,
name: PropTypes.string,
pickupNum: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
}))
}
// send data to component props
const mapStateToProps = (state) => {
return {
posOrders: state.posOrders,
}
}
export default connect(mapStateToProps)(PosOrderList)
// store
const store = configureStore(initialState)
export default function configureStore(initialState) {
return createStore(
displayApp,
initialState,
applyMiddleware(
createLogger({
stateTransformer: state => state.toJS()
}),
thunk,
// socketMiddleware
)
)
}
addSocketListeners(store.dispatch, store.getState)
Lastly, the store logs here: redux store
Any and all help on this would be very appreciated! Thank you!
When you compose your reducer with combineReducers, for each dispatched action, all subreducers get invoked, since every reducer gets a chance to respond to every action.
Therefore, all state gets initialized after the first action is dispatched.
Your reducers are working fine https://jsfiddle.net/on8v2z8j/1/
var store = Redux.createStore(displayApp);
store.subscribe(render);
store.dispatch({type: 'UPDATE_WS_STATUS',status:'CONNECTED'});
store.dispatch({type: 'ADD_POS_ORDER',id:'id'});
store.dispatch({type: 'UPDATE_WS_STATUS',status:'DISCONNECTED'});