Redux-Thunk Action setting action as Promise - reactjs

So I'm using TS React, Redux, and Thunk middleware to handle redux actions that communicate with my api but I cant seem to get the initial configuration for my action function.
My action function is as follows:
export const startSession = ((accessCode: string) => {
return async (dispatch: Dispatch): Promise<Action> => {
try {
const response = await apiCall(accessCode);
return dispatch({ type: SessionActions.START_SESSION, payload: response });
} catch (e) {
console.log('error', e)
}
};
});
I have also tried this:
export const startSession = ((accessCode: string) => {
return async (dispatch: Dispatch) => {
try {
await apiCall(accessCode)
.then(response => dispatch({ type: SessionActions.START_SESSION, payload: response }))
} catch (e) {
console.log('error', e)
}
};
})
but neither seems to work. I thought waiting for the api response would force redux to wait, but it seems to be returning the promise into the state - shown in my redux-logger:
action undefined # 19:10:17.807
redux-logger.js?d665:1 prev state: {some state}
redux-logger.js?d665:1 action: PromiseĀ {<pending>}
And I get the error:
Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
I noticed that this dispatched type is undefined, so there must me a dispatch call being made initially before the data is returned from the api. If anyone could explain to me why it does this, and the standard format for writing actions that use thunk that would be super helpful.
Also please let me know if there is information that I'm missing.
Someone below asked to see how I the initialized store with thunk:
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { createLogger } from 'redux-logger';
import { createBrowserHistory } from 'history';
import rootReducer from '../_reducers/index'
const loggerMiddleware = createLogger();
export const history = createBrowserHistory();
export const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware,
loggerMiddleware
)
);

Related

React and Redux dispatch function problems

I am trying to understand how React, Redux and Axios work together but I just hit a wall and I need some help ...
My problem is that inside the action there is a dispatch but after i return the dispatch it does not continue further.
It's most likely that I do not understand how this works so please try to explain in as much as possible details. Thanks in advance.
my combineReducer
import {combineReducers} from "redux";
import getAvailableDatesReducer from "./getAvailableDatesReducer";
export default combineReducers({
availableDates: getAvailableDatesReducer
});
my reducer
import {FETCH_AVAILABLE_DATES} from "../actions/types";
const initialState = {
availableDates: null
};
const getAvailableDatesReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_AVAILABLE_DATES:
return {...state, availableDates: action.availableDates};
default:
console.log('just default..');
return state;
}
}
export default getAvailableDatesReducer;
my action
export const fetchAvailableDates = (appointmentKey) => {
//return (dispatch) => {
axios.post('/app_dev.php/termin/getavailability/new', {
appointmentKey: appointmentKey
}).then((response) => {
console.log('response received...');
return (dispatch) => {
console.log('not hitting this...');
dispatch({type: FETCH_AVAILABLE_DATES, availableDates: response.data.availability});
};
}).catch(err => {
console.log(err);
});
//}
}
my component
import {fetchAvailableDates} from "../actions";
const Calendar = (props, appointmentKey) => {
useEffect(() => {
fetchAvailableDates(appointmentKey);
}, []);
const mapStateToProps = (state) => {
return {
availableDates: state.availableDates,
}
}
export default connect(mapStateToProps, {fetchAvailableDates})(Calendar);
my index.js file
import {Provider} from 'react-redux';
import thunk from 'redux-thunk';
import {applyMiddleware, compose, createStore} from "redux";
import reducers from './reducers';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducers, composeEnhancers(applyMiddleware(thunk)));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById('root')
);
First, create an action in your action file:
const fetchDatesAction = (response) => ({
type: FETCH_AVAILABLE_DATES,
availableDates: response.data.availability,
});
Then, update connect
export default connect(mapStateToProps, {fetchDatesAction})(Calendar);
Finally, Call api in useEffect, like this:
useEffect(() => {
axios
.post("/app_dev.php/termin/getavailability/new", {
appointmentKey: appointmentKey,
})
.then((response) => {
console.log("response received...");
return (dispatch) => {
console.log("not hitting this...");
props.fetchDatesAction();
};
})
.catch((err) => {
console.log(err);
});
}, []);
Thunk is a library which is responsible for handling side-effects in state management for redux.
Redux is a simple pure function which accepts state and action as an input and based on these two, it returns a new state. So its pretty simple and straight forward.
Now in certain scenarios like the one you have mentioned in your example, we need to perform some asynchronous actions which may not provide immediate result but a promise. In that case, we need to use a third party tool which is also called as enhancer.
That's the reason why you have added
const store = createStore(reducers, composeEnhancers(applyMiddleware(thunk)));
Now when asynchronous action is triggered, it goes to thunk. Thunk processes the request and then triggers one more action which again goes to reducer.
Now reducer being a pure function, does not distinguish between these sources of event and simply update the state based on action and its payload.
Hope this diagram helps you understand the concept.
https://miro.medium.com/max/1400/1*QERgzuzphdQz4e0fNs1CFQ.gif

React: integrating a redux reducer to dandelion-pro project

end developer and recently I started to learn front-end. I have troubles with adding some new data to redux store. I am working with dandelion-pro react template and can't figure out how to add my reducers to their stores, it seems much more complex then redux stores I have build for other projects, also I observed they used redux saga. I am trying to introduce a global state for user data on login.
Here is code for my reducer
import { CallToAction } from '#material-ui/icons';
import { SUCCESSFUL_LOGIN, FETCH_LOGIN, ERROR_LOGIN } from '../../actions/actionConstants';
const initialState = {
auth: false,
isLoading: false,
errMess: null,
isAdmin: false,
token: ''
}
export default function userReducer (state = initialState, action) {
console.log("Action: ")
console.log(action)
switch (action.type) {
case SUCCESSFUL_LOGIN: return {
...state,
auth: true,
isLoading: false,
errMess: null,
isAdmin: action.payload.isAdmin,
token: action.payload.token
}
case FETCH_LOGIN: return {
...state,
auth: false,
isLoading: true,
errMess: null
}
case ERROR_LOGIN: return {
...state,
auth: false,
isLoading: false,
errMess: action.payload
}
default: return state
}
}
Code for fetch user data
import { SUCCESSFUL_LOGIN, FETCH_LOGIN, ERROR_LOGIN } from '../../actions/actionConstants';
import axios from 'axios';
import { server } from '../../config'
export const fetchUser = (username, password) => (dispatch) => {
console.log("a ajuns")
dispatch(loginLoading(true));
axios.post(`${server + "/auth/login"}`, { username, password })
.then(res => {
const user = res.data;
console.log(user);
if (user.status) {
window.location.href = '/app';
return dispatch(loginUser(user));
}
else {
var errmess = new Error("False Status of User");
throw errmess;
}
})
.catch(error => dispatch(loginFailed(error.message)))
}
export const loginLoading = () => ({
type: FETCH_LOGIN
});
export const loginFailed = (errmess) => {
return ({
type: ERROR_LOGIN,
payload: errmess
})
};
export const loginUser = (user) => ({
type: SUCCESSFUL_LOGIN,
payload: user
})
Section that combine reducers
/**
* Combine all reducers in this file and export the combined reducers.
*/
import { reducer as form } from 'redux-form/immutable';
import { combineReducers } from 'redux-immutable';
import { connectRouter } from 'connected-react-router/immutable';
import history from 'utils/history';
import languageProviderReducer from 'containers/LanguageProvider/reducer';
import login from './modules/login';
import uiReducer from './modules/ui';
import initval from './modules/initForm';
import user from '../my_redux/modules/initForm';
/**
* Creates the main reducer with the dynamically injected ones
*/
export default function createReducer(injectedReducers = {}) {
const rootReducer = combineReducers({
user,
form,
login,
ui: uiReducer,
initval,
language: languageProviderReducer,
router: connectRouter(history),
...injectedReducers,
});
// Wrap the root reducer and return a new root reducer with router state
const mergeWithRouterState = connectRouter(history);
return mergeWithRouterState(rootReducer);
}
I try to connect my Login component like this
const mapStateToProps = state => ({
user: state.user
});
const mapDispatchToProps = dispatch => ({
fetchUser: (username, password) => dispatch(fetchUser(username, password))
});
// const mapDispatchToProps = dispatch => ({
// actions: bindActionCreators(userActions, dispatch),
// });
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Login));
The store is created here
/**
* Create the store with dynamic reducers
*/
import { createStore, applyMiddleware, compose } from 'redux';
import { routerMiddleware } from 'connected-react-router';
import { fromJS } from 'immutable';
import createSagaMiddleware from 'redux-saga';
import createReducer from './reducers';
export default function configureStore(initialState = {}, history) {
let composeEnhancers = compose;
const reduxSagaMonitorOptions = {};
// If Redux Dev Tools and Saga Dev Tools Extensions are installed, enable them
/* istanbul ignore next */
if (process.env.NODE_ENV !== 'production' && typeof window === 'object') {
/* eslint-disable no-underscore-dangle */
if (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({});
// NOTE: Uncomment the code below to restore support for Redux Saga
// Dev Tools once it supports redux-saga version 1.x.x
// if (window.__SAGA_MONITOR_EXTENSION__)
// reduxSagaMonitorOptions = {
// sagaMonitor: window.__SAGA_MONITOR_EXTENSION__,
// };
/* eslint-enable */
}
const sagaMiddleware = createSagaMiddleware(reduxSagaMonitorOptions);
// Create the store with two middlewares
// 1. sagaMiddleware: Makes redux-sagas work
// 2. routerMiddleware: Syncs the location/URL path to the state
const middlewares = [sagaMiddleware, routerMiddleware(history)];
const enhancers = [applyMiddleware(...middlewares)];
const store = createStore(
createReducer(),
fromJS(initialState),
composeEnhancers(...enhancers),
);
// Extensions
store.runSaga = sagaMiddleware.run;
store.injectedReducers = {}; // Reducer registry
store.injectedSagas = {}; // Saga registry
// Make reducers hot reloadable, see http://mxs.is/googmo
/* istanbul ignore next */
if (module.hot) {
module.hot.accept('./reducers', () => {
store.replaceReducer(createReducer(store.injectedReducers));
});
}
return store;
}
on login form submit I call this.props.fetchUser("admin", "admin"); but I get the following error:
Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
at dispatch (redux.js:198)
at eval (middleware.js:29)
at eval (redux-saga-core.dev.cjs.js:1412)
at Object.fetchUser (Login.js?f3c5:66)
at Login.submitForm (Login.js?f3c5:30)
at onSubmit (Login.js?f3c5:49)
at executeSubmit (handleSubmit.js?e3b3:39)
at handleSubmit (handleSubmit.js?e3b3:131)
at Form._this.submit (createReduxForm.js?d100:362)
at HTMLUnknownElement.callCallback (react-dom.development.js:149)
I reviewed my answer, and update it according to your question update
The syntax you use for defining async function is called a thunk a fancy name for a function that return a promise (or async function), anyway to use that pattern in code you need a library called redux-thunk
To apply the redux-thunk middle ware for your application,
npm install redux-thunk
then apply the middleware in your app store
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
// Note: this API requires redux#>=3.1.0
const store = createStore(rootReducer, applyMiddleware(thunk));
example from official repo of redux-thunk
and for your code just add the thunk imported from redux-thunk in middleware array
import thunk from 'redux-thunk';
const middlewares = [sagaMiddleware, routerMiddleware(history), thunk];
Now for Saga
you need to have a root saga that run others sagas, and run the root saga from the created saga middleware
here're the steps:
1- create saga middleware(just like how you did, but we need to run the root saga from there too)
import createSagaMiddleware from 'redux-saga'
const sagaMiddleware = createSagaMiddleware();
// after you've created the store then run the root saga
sagaMiddleware.run(rootSagas);
2- create your rootSaga
export function* rootSagas() {
try {
yield fork(fetchUsersSaga);
} catch (error) {
console.warn(error);
}
}
3- create your fetch user saga
import { take, put, call } from "redux-saga/effects";
export function* fetchUsersSaga() {
while (true) {
const action: FetchUser = yield take(FETCH_USER);
try {
const response = yield call(usersService.fetchUsersOfProject, { ...paramsPassedToFetchUserFunction })
if (response) {
const { data: { response: { user } } } = response;
yield put(setUser({ user }));
}
} catch (error) {
yield put(fetchUser());
}
}
}
now you need to notice the big difference between saga and thunk, for thunk you write an action that is hard coded to do one thing(or multiple but it still for a more specific case) and in saga you listen for what ever action the store has dispatched and react to that action in generator code style

Integrate socket.io into reactjs + redux that uses REST

My project consists of a backend(nodejs , express, mysql) and a frontend (reactjs, redux).
The flow of a rendered component is in a simple redux pattern-
in ComponentDidMount I call an action creator this.props.getResource()
in action creator I use axios to call the backend and dispatch an action in callback like so :
actions.js
export const getResource = () => dispatch => {
axios.get(API_URL/path/to/resource)
.then(res => {
dispatch({
type: SOME_RESOURCE,
payload: res.data
});
})
.catch(e =>
dispatch({
type: ERROR,
payload: e
})
);
};
in reducer I send back to component the state with the new array :
reducers.js
export default function(state = initialState, action) {
switch (action.type) {
case SOME_RESOURCE:
return {
...state,
resources: [...state.resources, action.payload] // add new resource to existing array
};
}
default: return state;
}
}
It is working as it should using REST APIs but now I wish to replace a certain API call with a socket so that data is shown in real-time without needing to refresh the page.
How can I convert above example to use sockets instead of API calls?
This is what I have tried:
Flow starts the same - I call an action creator in ComponentDidMount
I changed the action creator to the following :
actions.js
import io from 'socket.io-client';
const socket = io(); // localhost backend
export const getResource= () => dispatch => {
socket
.on("getResourceEvent", res => {
dispatch({
type: SOME_RESOURCE,
payload: res.data
});
})
.on("onError", e => {
dispatch({
type: ERROR,
payload: e
});
});
};
no changes in reducers.js
This works but with each rendering of the component, the store.getState() gets called 1 additional time. On first render getState() is called 1 time and if I refresh the page I get 2 calls from getState() and so on.
What's causing this behavior and how can I prevent it?
Edit:
store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const initialState = {};
const middleware = [ thunk ];
var createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
var store = createStoreWithMiddleware(
rootReducer,
initialState,
applyMiddleware(...middleware)
)
store.subscribe(() => console.log("Store.getState()", store.getState()))
export default store;

Fetch data with redux saga

I have created an example for fetching data from API where I used redux-thunk. The following code is working.
In this context, I want to rewrite my code but using redux saga.
import React from 'react';
import {createStore, applyMiddleware} from 'redux';
import ReactDOM from "react-dom";
import thunk from 'redux-thunk';
import axios from 'axios';
function App(props) {
const initialState = {
loading: false,
data: [],
error: ''
};
const reducer = function (state = initialState, action) {
switch (action.type) {
case 'START_FETCH':
return {
...state,
loading: true
};
case 'PROCESS_FETCH':
return {
...state,
loading: false,
data: action.payload,
error: ""
};
case 'END_FETCH':
return {
...state,
loading: false,
data: [],
error: action.payload
}
}
return state;
};
const START_FETCH = 'START_FETCH';
const PROCESS_FETCH = 'PROCESS_FETCH';
const END_FETCH = 'END_FETCH';
let startFetchFun = () => {
return {
type: START_FETCH,
loading: true
}
};
let processFetchFun = (users) => {
return {
type: PROCESS_FETCH,
payload: users
}
};
let endFetchFun = (error) => {
return {
type: PROCESS_FETCH,
payload: error
}
};
let fetchUsersWithThunk = () => {
return function (dispatch) {
dispatch(startFetchFun());
axios.get('https://jsonplaceholder.typicode.com/users')
.then((response) => {
dispatch(processFetchFun(response.data));
})
.catch((error) => {
dispatch(endFetchFun(error.message));
console.log(error.message);
})
}
};
const store = createStore(reducer, applyMiddleware(thunk));
store.subscribe(() => {
console.log(store.getState())
});
store.dispatch(fetchUsersWithThunk());
return (
<div className="main">
<h1>Redux-Thunk</h1>
</div>
);
}
ReactDOM.render(
<App/>, document.getElementById('root'));
I want to write the code above using redux saga, to understand better sagas. So, how to use redux-saga for this example? Who will be able to help me?
Redux Saga uses yield call to call promises like an api service and uses yield put to dispatch actions to the store.
The difference is about blocking and not blocking calls.
Because we want to wait for the server to respond our request we will use yield call that is a blocking function.
Instead of dispatching the action directly inside the generator saga uses yield put({ type: "actionName" }). That's also useful for testing purposese.
So you should wrote your saga as following:
import {all, fork, put, call, takeLatest} from 'redux-saga/effects';
function* handleRequest (action) {
try {
yield put(startFetchFunc()); // dispatch the action to the store.
const result = yiels call(apiService.users, [data to pass]); // wait for the response blocking the code execution.
yield put(processFetchFun(result)); // dispatch the action to the store containing the data
} catch (e) {
yield put(endFetchFun('Error'));
}
}
function* watchRequest() {
yield takeLatest({type: "START_FETCH"}, handleRequest);
}
export function* rootSaga() {
yield all([
fork(wathcRequest),
// ... more watchers will be here...
]);
}
Congfigure you store as explained here https://redux-saga.js.org/docs/introduction/BeginnerTutorial.html
I suggest you to read the documentation more than once. It contains a lot of useful information that at first might be strange but much clearer once you understand how it works.
You will need to configure your store to use saga middleware:
import React from 'react';
import createSagaMiddleware from 'redux-saga';
import { createStore, applyMiddleware } from 'redux';
import reducer from './reducers';
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware, logger),
);
sagaMiddleware.run(rootSaga); // < -- rootSaga exports all sagas in your app
Then you can convert your thunk to a saga:
import {call} from 'redux-saga/effects';
function* fetchUsersSaga(payload){
try {
yield call(startFetchFun());
axios.get('https://jsonplaceholder.typicode.com/users')
.then((response) => {
yield call(processFetchFun(response.data));
})
} catch(err) {
yield call(endFetchFun(error.message));
console.log(error.message);
}
};

Error: Actions must be plain objects. Use custom middleware for async actions. in React Native

I'm getting this error every time I try to dispatch my action:
Error: Actions must be plain objects. Use custom middleware for async
actions.
I've installed redux-thunk and without async actions, it's working.
Store config:
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
import { createLogger } from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';
import reducers from '../reducers/index';
const logger = createLogger();
export default createStore(reducers, composeWithDevTools(applyMiddleware(thunk)));
UI:
...
import { connect } from 'react-redux';
import { getCities } from '../../actions/cities';
...
componentDidMount = async () => {
try {
const cities = await this.props.getCities();
console.log(cities);
} catch (error) {
console.log(`errorhome: ${error}`);
}
SplashScreen.hide();
}
...
const mapDispatchToProps = dispatch => ({
changeNetworkStatus: () => dispatch(changeNetworkStatus),
getCities: () => dispatch(getCities),
});
export default connect(mapStateToProps, mapDispatchToProps)(Home);
Action:
import database from '../config/utils';
export const GET_CITIES_START = 'GET_CITIES_START';
export const GET_CITIES_FINISHED = 'GET_CITIES_FINISHED';
export const GET_CITIES_ERROR = 'GET_CITIES_ERROR';
const getCititesStart = () => ({ type: GET_CITIES_START });
const getCititesFinished = cities => ({ type: GET_CITIES_FINISHED, cities });
const getCititesError = error => ({ type: GET_CITIES_ERROR, error });
export const getCitites = () => async (dispatch) => {
dispatch(getCititesStart());
try {
const cities = [];
const snap = await database.ref('cities').once('value');
console.log('snap: ', snap);
snap.forEach((element) => {
const city = {
city: element.key,
coordinate: element.val().coordinate,
};
cities.push(city);
});
dispatch(getCititesFinished(cities));
} catch (error) {
dispatch(getCititesError(error));
}
};
EDIT: If I add logger to middlewares too, the error message is this:
TypeError: Cannot read property 'type' of undefined
Thanks for your help!
Actions are functions that return a object with action's data, that data is a object with a type property.
You're dispatching action like this:
dispatch(getCities)
You should dispatch action like this:
dispatch(getCities())

Resources