I have the redux-thunk installed and I think I configured it as documentation. But still get the error: Actions must be plain objects. Use custom middleware for async actions.
action:
import fetch from 'isomorphic-fetch'
export { get_all_posts } from '../utils/http_functions'
export function fetchAllPosts(){
return{
type: 'FETCH_ALL_POSTS'
}
}
export function receivedAllPosts(posts){
return{
type: 'RECEIVED_ALL_POSTS',
posts: posts
}
}
export function getAllPosts(){
return (dispatch) => {
dispatch(fetchAllPosts())
return fetch('/api/posts')
.then(response => response.json())
.then(json => {
dispatch(receivedAllPosts(json))
})
.catch(error => {
})
}
}
store:
import { createStore, applyMiddleware } from 'redux'
import rootReducer from '../reducers/index'
import thunk from 'redux-thunk'
const debugware = [];
if (process.env.NODE_ENV !== 'production') {
const createLogger = require('redux-logger');
debugware.push(createLogger({
collapsed: true
}));
}
export default function configureStore(initialState = {}){
const store = createStore(
rootReducer,
initialState,
window.devToolsExtension && window.devToolsExtension(),
applyMiddleware(thunk, ...debugware)
)
if (module.hot){
module.hot.accept('../reducers', () => {
const nextRootReducer = require('../reducers/index').default
store.replaceReducer(nextRootReducer)
})
}
return store
}
reducer:
export function posts(state = {}, action){
switch(action.type){
case 'RECEIVED_ALL_POSTS':
return Object.assign({}, state, {
'posts': action.posts
})
default:
return state
}
}
in server.js, I am using proxy server to route the '/api/' request to my backend service:
app.all(/^\/api\/(.*)/, function api(req, res){
proxy.web(req, res, {
target: 'http://localhost:5000'
})
})
Because in your code:
const store = createStore(
rootReducer,
initialState,
window.devToolsExtension && window.devToolsExtension(),
applyMiddleware(thunk, ...debugware)
)
The function applyMiddleware(thunk, ...debugware) is actually not applied. In the document of createStore: http://redux.js.org/docs/api/createStore.html
createStore(reducer, [preloadedState], [enhancer])
Your applyMiddleware should be input as the third argument.
Note: Your solution in the comment is another approach to apply the middleware.
Related
FYI: Everything is working fine and all the flows are working as expected but logs are confusing.
I am using Redux ToolKit for managing redux state & using Next Redux Wrapper for Server Side rendering and caching queries.
CartSlice.js
import { createSlice } from '#reduxjs/toolkit';
export const cartSlice = createSlice({
name: 'cart',
initialState: { data: [] },
reducers: {
addToCart: (state, action) => {
const itemInCart = state.data.find((item) => item.id === action.payload.id);
if (itemInCart) {
itemInCart.quantity += action.payload.quantity;
} else {
state.data.push({ ...action.payload });
}
}
},
});
export const cartReducer = cartSlice.reducer;
export const { addToCart } = cartSlice.actions;
ServiceApi.js
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query/react'
import { HYDRATE } from 'next-redux-wrapper'
export const serviceApi = createApi({
reducerPath: 'serviceApi',
baseQuery: fetchBaseQuery({ baseUrl: '<base-url>' }),
extractRehydrationInfo(action, { reducerPath }) {
if (action.type === HYDRATE) {
return action.payload[reducerPath]
}
},
endpoints: (builder) => ({
getItemById: builder.query({ query: (id) => `id/${id}` }),
getItems: builder.query({ query: () => `/` }),
}),
})
export const { getRunningQueriesThunk } = serviceApi.util
export const { useGetItemByIdQuery, useGetItemsQuery } = serviceApi
export const { getItemById, getItems } = serviceApi.endpoints;
store.js
import { configureStore } from '#reduxjs/toolkit'
import { serviceApi } from './serviceApi'
import { createWrapper } from "next-redux-wrapper";
import { cartSlice } from "./cartSlice";
export const store = configureStore({
reducer: {
[cartSlice.name]: cartSlice.reducer,
[serviceApi.reducerPath]: serviceApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(serviceApi.middleware),
})
const makeStore = () => store
export const wrapper = createWrapper(makeStore, {debug: true});
Using getServerSideProps
export const getServerSideProps = wrapper.getServerSideProps(
(store) => async (context) => {
const { id } = context.query;
store.dispatch(serviceApi.endpoints.getItemById.initiate(parseInt(id)));
await Promise.all(store.dispatch(serviceApi.util.getRunningQueriesThunk()));
return {
props: {},
};
}
);
And using wrappedStore
const { store, props } = wrapper.useWrappedStore(pageProps);
On the UI flows everything is working as expected and i am able to add items in the cart and i can see the store state is getting updated by looking the UI.
But the logs from next redux wrapper is confusing:
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 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>
)}
I am trying to reset my storage when I log out but it doesn't seem to work at all.
As you can see I am using AsyncStorage for my store and I try to follow the answer from this post.
Here is my index.js from store folder
import thunk from 'redux-thunk';
import { createStore, compose, applyMiddleware } from 'redux';
import { AsyncStorage } from 'react-native';
import { persistStore, autoRehydrate } from 'redux-persist';
import rootReducer from '../reducers';
var defaultState = {};
export function configureStore(initialState = defaultState) {
var store = createStore(rootReducer, initialState, compose(
applyMiddleware(thunk),
autoRehydrate(),
));
persistStore(store, { storage: AsyncStorage });
return store;
}
and here is my index.js from reducers folder
import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';
import { AsyncStorage } from 'react-native';
import authReducer from './authReducer';
import alertsReducer from './alertsReducer';
import jobsReducer from './jobsReducer';
import userDataReducer from './userDataReducer';
const appReducer = combineReducers({
form: formReducer,
auth: authReducer,
alerts: alertsReducer,
jobs: jobsReducer,
userData: userDataReducer
})
const rootReducer = ( state, action ) => {
if(action.type === 'UNAUTH_USER') {
Object.keys(state).forEach(key => {
AsyncStorage.removeItem(`persist:${key}`);
console.log(state)
});
}
return appReducer(state, action)
}
export default rootReducer
On Initial load of our application the reducer state is fresh.
We Can copy this initial default state and use it to assign to our reducer again on logging out, the way we can achieve this could be as follows.
Step 1: call an action on the application load that will copy reducer's initial state as the defaultState
Step 2: While logging out of the application we can simply reAssign the default state and it should work as new.
App root Component
componentDidMount() {
dispatch(ON_APP_LOAD)
}
App Reducer
const appReducer = combineReducers({
user: userStatusReducer,
analysis: analysisReducer,
incentives: incentivesReducer
});
let defaultState = null;
export default (state, action) => {
switch (action.type) {
case **ON_APP_LOAD**:
// will be assigned or called only once
defaultState = defaultState || state;
break;
case **RESET_STATE**:
// detaching the reference on reset
state = _.deepClone(defaultState);
return state;
default:
break;
}
return appReducer(state, action);
};
On Logout calling the action for resetting state
function* logoutUser(action) {
// on logout success
dispatch("RESET_STATE")
}
I assume you have the js file where all reducers are combined in one and thus you have:
const allReducers = combineReducers({
reducer: nameOfReducer
});
const rootReducer = (state, action) => {
switch (action.type) {
case CLEAR_ALL_REDUCERS_DATA:
return state = undefined;
default:
return allReducers(state, action)
}
};
export default rootReducer;
and in index file where you create a store you need to define
const store = createStore(rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
and then simply call it on logout:
dispatch(clearAllStoreData());
where clearAllStoreData is an action defined in your action file:
export const clearAllStoreData = () => {
return {
type: CLEAR_ALL_REDUCERS_DATA
}
};
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.