Redux action ajax result not dispatched to reducer - ajax

I just get to experiment with Redux and I know that middleware is essential to make ajax calls. I've installed redux-thunk and axios package separately and tried to hook my result as a state and render the ajax result to my component. However my browser console displays an error and my reducer couldn't grab the payload.
The error:
Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
This is part of my code and how the middleware is hooked up:
//after imports
const logger = createLogger({
level: 'info',
collapsed: true,
});
const router = routerMiddleware(hashHistory);
const enhancer = compose(
applyMiddleware(thunk, router, logger),
DevTools.instrument(),
persistState(
window.location.href.match(
/[?&]debug_session=([^&]+)\b/
)
)
// store config here...
my action:
import axios from 'axios';
export const SAVE_SETTINGS = 'SAVE_SETTINGS';
const url = 'https://hidden.map.geturl/?with=params';
const request = axios.get(url);
export function saveSettings(form = {inputFrom: null, inputTo: null}) {
return (dispatch) => {
dispatch(request
.then((response) => {
const alternatives = response.data.alternatives;
var routes = [];
for (const alt of alternatives) {
const routeName = alt.response.routeName;
const r = alt.response.results;
var totalTime = 0;
var totalDistance = 0;
var hasToll = false;
// I have some logic to loop through r and reduce to 3 variables
routes.push({
totalTime: totalTime / 60,
totalDistance: totalDistance / 1000,
hasToll: hasToll
});
}
dispatch({
type: SAVE_SETTINGS,
payload: { form: form, routes: routes }
});
})
);
}
}
reducer:
import { SAVE_SETTINGS } from '../actions/configure';
const initialState = { form: {configured: false, inputFrom: null, inputTo: null}, routes: [] };
export default function configure(state = initialState, action) {
switch (action.type) {
case SAVE_SETTINGS:
return state;
default:
return state;
}
}
you can see the state routes has size of 0 but the action payload has array of 3.
Really appreciate any help, thanks.

It looks like you have an unnecessary dispatch in your action, and your request doesn't look to be instantiated in the correct place. I believe your action should be:
export function saveSettings(form = { inputFrom: null, inputTo: null }) {
return (dispatch) => {
axios.get(url).then((response) => {
...
dispatch({
type: SAVE_SETTINGS,
payload: { form: form, routes: routes }
});
});
};
}

Related

Redux toolkit: updating state with entityAdapter vs custom callback

In have an async request to log a user in:
export const loginUser = createAsyncThunk('users/login', async userInputs => {
try {
const { data } = await axios.post(
'url',
userInputs
);
Cookies.set('user', JSON.stringify(data));
return data;
} catch (error) {
return error.response.data;
}
});
In my store slice I have:
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers(builder) {
// builder.addCase(loginUser.fulfilled, usersAdapter.addOne); // <-- updates state with a delay?
builder.addCase(loginUser.fulfilled, (state, action) => {
state.user = action.payload;
});
},
});
When a user clicks a Log In button the thunk is dispatched. When it returns, the user is redirected to the Home page, which renders conditionally:
useSelector(state => state.user) ? <Home /> : null
With the custom callback this works. However, if I switch to using createEntityAdapter with addOne in the slice:
builder.addCase(loginUser.fulfilled, usersAdapter.addOne);
The useSelector call returns null until I reload the page. Why?
Edit:
The initialState looks like this:
const initialState = {
user: Cookies.get('user') ? JSON.parse(Cookies.get('user')) : null,
};
And the loginHandler:
const loginHandler = async () => {
const data = await dispatch(
loginUser({
email,
password,
})
).unwrap();
navigate('/');
};

Redux connected React component not updating until a GET api request is recalled

My react app uses a redux connected component to render data from backend for a project page, so I called a GET dispatch inside a React Hook useEffect to make sure data is always rendered when the project page first open, and whenever there is a change in state project, the component will be updated accordingly using connect redux function. However, the component doesn't update after I reduce the new state using a DELETE API request, only if I dispatch another GET request then the state will be updated. So I have to call 2 dispatches, one for DELETE and one for GET to get the page updated synchronously (as you can see in handleDeleteUpdate function), and the same thing happened when I dispatch a POST request to add an update (in handleProjectUpdate). Only when I reload the page, the newly changed data will show up otherwise it doesn't happen synchronously, anyone knows what's wrong with the state update in my code? and how can I fix this so the page can be loaded faster with only one request?
I've changed the reducer to make sure the state is not mutated and is updated correctly.
I have also tried using async function in handleDeleteUpdate to make sure the action dispatch is finished
I have tried
console.log(props.project.data.updates)
to print out the updates list after calling props.deleteUpdate but it seems the updates list in the state have never been changed, but when I reload the page, the new updates list is shown up
Here is the code I have for the main connected redux component, actions, and reducers file for the component
function Project(props) {
let options = {year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit'}
const {projectID} = useParams();
const history = useHistory();
console.log(props.project.data? props.project.data.updates : null);
console.log(props.project.data);
// const [updates, setUpdates] = useState(props.project.data? props.project.data.updates : null)
useEffect(() => {
props.getProject(projectID);
}, []);
// Add an update to project is handled here
const handleProjectUpdate = async (updateInfo) => {
await props.postProjectUpdate(projectID, updateInfo)
await props.getProject(projectID);
}
const handleDeleteUpdate = async (updateID) => {
await props.deleteUpdate(projectID, updateID);
await props.getProject(projectID);
console.log(props.project.data.updates);
};
return (
<div>
<Navbar selected='projects'/>
<div className = "project-info-layout">
<UpdateCard
updates = {props.project.data.updates}
handleProjectUpdate = {handleProjectUpdate}
handleDeleteUpdate = {handleDeleteUpdate}
options = {options}
/>
</div>
</div>
)
}
const mapStateToProps = state => ({
project: state.project.project,
});
export default connect(
mapStateToProps,
{getProject, postProjectUpdate, deleteUpdate}
)(Project);
ACTION
import axios from 'axios';
import { GET_PROJECT_SUCCESS,ADD_PROJECT_UPDATE_SUCCESS, DELETE_PROJECT_UPDATE_SUCCESS} from './types';
let token = localStorage.getItem("token");
const config = {
headers: {
Authorization: `Token ${token}`,
}
};
export const getProject = (slug) => dispatch => {
axios.get(`${backend}/api/projects/` + slug, config)
.then(
res => {
dispatch({
type: GET_PROJECT_SUCCESS,
payload: res.data,
});
},
).catch(err => console.log(err));
}
export const postProjectUpdate = (slug, updateData) => dispatch => {
axios.post(`${backend}/api/projects/`+slug+ `/updates`,updateData, config)
.then(
res => {
dispatch({
type: ADD_PROJECT_UPDATE_SUCCESS,
payload: res.data,
});
},
).catch(err => console.log(err));
}
export const deleteUpdate = (slug, updateID) => dispatch => {
axios.delete(`${backend}/api/projects/`+ slug + `/updates/`+ updateID, config)
.then(
res => {
dispatch({
type: DELETE_PROJECT_UPDATE_SUCCESS,
payload: updateID,
});
},
).catch(err => console.log(err));
}
Reducer
import { GET_PROJECT_SUCCESS,ADD_PROJECT_UPDATE_SUCCESS, DELETE_PROJECT_UPDATE_SUCCESS} from "../actions/types";
const initialState = {
project: {},
};
export default function ProjectReducer(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case GET_PROJECT_SUCCESS:
return {
...state, // return all initial state
project: payload
};
case ADD_PROJECT_UPDATE_SUCCESS:
return {
...state,
project: {
...state.project,
updates: [...state.project.data.updates, payload.data]
}
};
case DELETE_PROJECT_UPDATE_SUCCESS:
let newUpdatesArray = [...state.project.updates]
newUpdatesArray.filter(update => update.uuid !== payload)
return {
...state,
project: {
...state.project,
members: newUpdatesArray
}
};
default:
return state;
}
}
updateCard in the Project component is showing a list of all updates

How to listen emit event in parent component in vue3

I want to pass event from child comment to parent.
I did same thing in vue2 but i don't know how to that in vue3.
This one is child component setup method.
setup(props, { emit }) {
const router = useRouter();
const form = ref(
{
email: "ajay#gmail.com",
password: "123456789",
isLoading: false,
},
);
const user = ref("");
const error = ref("");
function login() {
User.login(this.form).then(() => {
emit('login', true);
// this.$root.$emit("login", true); -- vue2
localStorage.setItem("auth", "true");
router.push('/dashboard');
})
.catch(error => {});
}
return { form, login, user, error};
}
from here emit login method and i want to listen in parent comment.
this is parent component, emit.on method not working here
setup(props, { emit }) {
const router = useRouter();
const state = reactive({
isLoggedIn: false,
});
onMounted(async () => {
emit.on("login", () => { // `vue2` this.$root.$on("login"`
this.isLoggedIn = true;
});
});
In parent component you should add a handler for that emitted event :
<child #login="onLogin"></child>
setup(props, { emit }) {
const router = useRouter();
const state = reactive({
isLoggedIn: false,
});
function onLogin(){
state.isLoggedIn=true,
}
return{state,onLogin}
}
Or make a composable function named useAuth in separate file :
import {reactive} from 'vue'
const state = reactive({
isLoggedIn: false,
});
const useAuth=()=>{
function onLogin(){
state.isLogged=true;
}
return {state,onLogin}
}
export default useAuth();
then import the function inside the two components :
child :
import useAuth from './useAuth'
....
setup(props, { emit }) {
const router = useRouter();
const {useAuth} =useAuth();
....
function login() {
User.login(this.form).then(() => {
onLogin() //will call the nested function that set loggedIn to true
localStorage.setItem("auth", "true");
router.push('/dashboard');
})
.catch(error => {});
}
in parent :
import useAuth from './useAuth'
....
setup(props, { emit }) {
const router = useRouter();
const {state} =useAuth();
//it replaces your local state

Mock api in react redux-thunk project returning undefined

I am quite new in redux world and have not yet had a project structured the ducks way. I am trying to understand it and use it to make a mock api, since I don't have the backend ready yet. I am working with the legacy code, that I am trying to figure out. There is a folder called data, that has a duck and a backendApi file. Duck file looks like this.
data/duck.jsx
import { createSelector } from 'reselect';
import { createReduxApi } from './backendApi';
const getDataContext = state => state.default.dataContext;
const backendReduxApi = createBackendReduxApi(getDataContext);
// Action creators
export const makeRestApiRequest = endpointName => backendReduxApi .makeRequestActionCreator(endpointName);
export const resetRestApi = endpointName => backendReduxApi .makeResetActionCreator(endpointName);
// Reducers
export const dataReducer = backendReduxApi .createReducer();
// Selectors
const getRestApiState = endpointName => backendReduxApi .getEndpointState(endpointName);
export const getRestApiData = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.data);
export const getRestApiMeta = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.meta);
export const getRestApiError = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.error);
export const getRestApiStarted = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.started);
export const getRestApiFinished = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.finished);
The backendApi.jsx file looks like this:
data/backendApi.jsx
import ReduxRestApi from './rest/ReduxRestApi';
export const BackendApi = { // NOSONAR
LANGUAGE_FILE: 'languageFile',
EMPLOYEE: 'employee',
};
const backendReduxApiBuilder = ReduxRestApi.build()
/* /api */
/* /api/employee */
.withGet('/myproject/api/employee', BackendApi.EMPLOYEE)
/* /language*/
.withGet('/myproject/language/nb_NO.json', BackendApi.LANGUAGE_FILE)
export const createBackendReduxApi = restApiSelector => backendReduxApiBuilder
.withRestApiSelector(restApiSelector)
.create();
Then in the data/rest folder I have 4 files: ReduxRestApi, restConfig, RestDuck and restMethods.
data/rest/ReduxRestApi.jsx
import { combineReducers } from 'redux';
import { get, post, postAndOpenBlob } from './restMethods';
import RestDuck from './RestDuck';
class ReduxRestApi {
constructor(endpoints, getRestApiState) {
this.createReducer = this.createReducer.bind(this);
this.getEndpoint = this.getEndpoint.bind(this);
this.makeRequestActionCreator = this.makeRequestActionCreator.bind(this);
this.makeResetActionCreator = this.makeResetActionCreator.bind(this);
this.getEndpointState = this.getEndpointState.bind(this);
this.ducks = endpoints.map(({ name, path, restMethod }) => new RestDuck(name, path, restMethod, getRestApiState));
}
createReducer() {
const reducers = this.ducks
.map(duck => ({ [duck.name]: duck.reducer }))
.reduce((a, b) => ({ ...a, ...b }), {});
return combineReducers(reducers);
}
getEndpoint(endpointName) {
return this.ducks.find(duck => duck.name === endpointName)
|| { actionCreators: {} };
}
makeRequestActionCreator(endpointName) {
return this.getEndpoint(endpointName).actionCreators.execRequest;
}
makeResetActionCreator(endpointName) {
return this.getEndpoint(endpointName).actionCreators.reset;
}
getEndpointState(endpointName) {
return this.getEndpoint(endpointName).stateSelector;
}
static build() {
class RestApiBuilder {
constructor() {
this.withGet = this.withGet.bind(this);
this.withPost = this.withPost.bind(this);
this.withPostAndOpenBlob = this.withPostAndOpenBlob.bind(this);
this.withRestApiSelector = this.withRestApiSelector.bind(this);
this.endpoints = [];
}
withGet(path, name) {
this.endpoints.push({ path, name, restMethod: get });
return this;
}
withPost(path, name) {
this.endpoints.push({ path, name, restMethod: post });
return this;
}
withPostAndOpenBlob(path, name) {
this.endpoints.push({ path, name, restMethod: postAndOpenBlob });
return this;
}
withRestApiSelector(restApiSelector) {
this.restApiSelector = restApiSelector;
return this;
}
create() {
return new ReduxRestApi(
this.endpoints,
this.restApiSelector
);
}
}
return new RestApiBuilder();
}
}
export default ReduxRestApi;
restConfig.jsx
import axios from 'axios';
import { removeErrorMessage, showErrorMessage } from '../../app/duck';
import { is401Error, isHandledError } from '../../app/ErrorTypes';
const isDevelopment = process.env.NODE_ENV === 'development';
const configureRequestInterceptors = (store) => {
const onRequestAccepted = (config) => {
store.dispatch(removeErrorMessage());
return config;
};
const onRequestRejected = error => Promise.reject(error);
axios.interceptors.request.use(onRequestAccepted, onRequestRejected);
};
const configureResponseInterceptors = (store) => {
const onSuccessResponse = response => response;
const onErrorResponse = (error) => {
if (is401Error(error) && !isDevelopment) {
window.location.reload();
}
if (!isHandledError(error)) {
store.dispatch(showErrorMessage(error));
}
return Promise.reject(error);
};
axios.interceptors.response.use(onSuccessResponse, onErrorResponse);
};
const configureRestInterceptors = (store) => {
configureRequestInterceptors(store);
configureResponseInterceptors(store);
};
export default configureRestInterceptors;
data/rest/RestDuck.jsx
import { createSelector } from 'reselect';
import { get, getBlob, post, postAndOpenBlob, postBlob } from './restMethods';
/**
* getMethodName
* Helper function that maps given AJAX-method to a name
*
* Ex. getMethodName(getBlob) -> 'GET'
*/
const getMethodName = (restMethod) => {
switch (restMethod) {
case get:
case getBlob:
return 'GET';
case post:
case postBlob:
case postAndOpenBlob:
return 'POST';
default:
return '';
}
};
/**
* createRequestActionType
* Helper function to generate actionType for actions related to AJAX calls
*
* Ex: createRequestActionType('fetchEmployee', 'ERROR', get, '/myproject/api/employee') -> '##REST/fetchEmployee GET /myproject/api/employeeERROR'
*/
const createRequestActionType = (name, qualifier, restMethod = '', path = '') => [`##REST/${name}`, getMethodName(restMethod), path, qualifier]
.filter(s => s !== '')
.join(' ');
/**
* createRequestActionTypes
* Helper function to generate ActionTypes for a given AJAX method and resource.
*
* Ex. createRequestActionType(fetchEmployee, get, '/myproject/api/employee') -> {
* reset: '##REST GET /myproject/api/employee RESET',
* requestStarted: '##REST GET /myproject/api/employee STARTED',
* requestError: '##REST GET /myproject/api/employee ERROR',
* requestFinished: '##REST GET /myproject/api/employee FINISHED',
* }
*/
const createRequestActionTypes = (name, restMethod, path) => ({
reset: createRequestActionType(name, 'RESET'),
requestStarted: createRequestActionType(name, 'STARTED', restMethod, path),
requestError: createRequestActionType(name, 'ERROR', restMethod, path),
requestFinished: createRequestActionType(name, 'FINISHED', restMethod, path)
});
/**
* createRequestThunk
* Helper function that generates a thunk that performs an AJAX call specified by 'restMethod' and 'restEndpoint'
*
* When the thunk is running, the action 'requestStarted' will be dispatched immediately.
* Then, it performs the AJAX call that returns a promise.
* If the call goes well, the action 'requestFinished' will be dispatched with data from the call.
* If the call fails, the action 'requestError' is dispatched with the contents of the error.
*/
const createRequestThunk = (restMethod, restEndpoint, requestStarted, requestFinished, requestError) => (
(params, options = {}) => (dispatch) => {
dispatch(requestStarted(params, options));
return restMethod(restEndpoint, params)
.catch((error) => {
const data = error.response && error.response.data ? error.response.data : error;
dispatch(requestError(data));
return Promise.reject(error);
})
.then((response) => {
dispatch(requestFinished(response.data));
return response;
});
}
);
/**
* createRequestActionCreators
* Helper function that creates action creators 'requestStarted', 'requestFinished' and 'requestError',
* #see createRequestThunkCreator
*/
const createRequestActionCreators = (restMethod, restEndpoint, actionTypes) => {
const reset = () => ({ type: actionTypes.reset });
const requestStarted = (params, options = {}) => ({ type: actionTypes.requestStarted, payload: { params, timestamp: Date.now() }, meta: { options } });
const requestFinished = data => ({ type: actionTypes.requestFinished, payload: data });
const requestError = error => ({ type: actionTypes.requestError, payload: error });
const execRequest = createRequestThunk(restMethod, restEndpoint, requestStarted, requestFinished, requestError);
return {
reset, requestStarted, requestFinished, requestError, execRequest
};
};
/**
* createRequestReducer
*
* Helper function that creates a reducer for an AJAX call.
* Reducer alters the state of the actions with the name defined by
* actionTypes.requestStarted
* actionTypes.requestFinished
* actionTypes.requestError
*/
const createRequestReducer = (restMethod, resourceName, actionTypes) => {
const initialState = {
data: undefined,
meta: undefined,
error: undefined,
started: false,
finished: false
};
return (state = initialState, action = {}) => {
switch (action.type) {
case actionTypes.requestStarted:
return {
...initialState,
data: action.meta.options.keepData ? state.data : initialState.data,
started: true,
meta: action.payload
};
case actionTypes.requestFinished:
return {
...state,
started: false,
finished: true,
data: action.payload
};
case actionTypes.requestError:
return {
...state,
started: false,
error: action.payload
};
case actionTypes.reset:
return {
...initialState
};
default:
return state;
}
};
};
/**
* RestDuck
* Class that offers action types, action creators, reducers and selectors for an AJAX call.
* #see createRequestActionTypes
* #see createRequestActionCreators
* #see createRequestReducer
*
* Ex.
* const getEmployeeDuck = new RestDuck(execGetRequest, 'employee', GET_EMPLOYEE_SERVER_URL);
* // Action creators
* export const fetchEmployee = getEmployeeDuck.actionCreators.execRequest;
* // Reducer
* export const dataReducer = combineReducers(
* ...,
* getEmployeeDuck.reducer,
* }
* // Selectors
* export const getDataContext = state => state.default.dataContext;
* export const getEmployeeData = getEmployeeDuck.selectors.getRequestData(getDataContext);
* export const getEmployeeStarted = getEmployeeDuck.selectors.getRequestStarted(getDataContext);
* ...
*/
class RestDuck {
constructor(name, path, restMethod, getApiContext) {
this.restMethod = restMethod;
this.name = name;
this.path = path;
this.getApiContext = getApiContext;
this.$$duck = {}; // for class internal use
}
get actionTypes() {
if (!this.$$duck.actionTypes) {
this.$$duck.actionTypes = createRequestActionTypes(this.name, this.restMethod, this.path);
}
return this.$$duck.actionTypes;
}
get actionCreators() {
if (!this.$$duck.actionCreators) {
this.$$duck.actionCreators = createRequestActionCreators(this.restMethod, this.path, this.actionTypes);
}
return this.$$duck.actionCreators;
}
get reducer() {
if (!this.$$duck.reducer) {
this.$$duck.reducer = createRequestReducer(this.restMethod, this.name, this.actionTypes);
}
return this.$$duck.reducer;
}
get stateSelector() {
return createSelector([this.getApiContext], restApiContext => restApiContext[this.name]);
}
}
export default RestDuck;
data/rest/restMethods.jsx
import axios, { CancelToken } from 'axios';
const openPreview = (data) => {
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(data);
} else {
window.open(URL.createObjectURL(data));
}
};
const cancellable = (config) => {
let cancel;
const request = axios({
...config,
cancelToken: new CancelToken((c) => { cancel = c; })
});
request.cancel = cancel;
return request.catch(error => (axios.isCancel(error) ? Promise.reject(new Error(null)) : Promise.reject(error)));
};
const defaultHeaders = {
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
Expires: 0
};
const defaultPostHeaders = {
'Content-Type': 'application/json'
};
export const get = (url, params, responseType = 'json') => cancellable({
url,
params,
responseType,
method: 'get',
headers: {
...defaultHeaders
}
});
export const post = (url, data, responseType = 'json') => cancellable({
url,
responseType,
data: JSON.stringify(data),
method: 'post',
headers: {
...defaultHeaders,
...defaultPostHeaders
},
cache: false
});
export const getBlob = (url, params) => get(url, params, 'blob');
export const postBlob = (url, data) => post(url, data, 'blob');
export const postAndOpenBlob = (url, data) => postBlob(url, data)
.then((response) => {
openPreview(response.data);
return {
...response,
data: 'blob opened as preview' // Don't waste memory by storing blob in state
};
});
I am not sure where to place and how to do mock api calls in this structure. I was thinking of making a mock api similiar to this one, where I would mimick the ajax calls and store them in the redux, but just not sure how to do this in this kind of setup?
I have tried with making the mockApi folder and instead of using the restMethods, to use the file where I would write promises that would resolve the mockData. This is my attempt:
mockRestMethods
const employee = {
name: 'Joe Doe'
}
const data = {
employee
};
export const get = item => new Promise((resolve) => {
setTimeout(() => {
resolve({ data: data[item] });
}, 1000);
});
But, if I inspect what is returned as the response.data inside the createRequestThunk function in the RestDuck file I get data: undefined there. Why is that, what am I doing wrong?
I may well have this wrong, but it seems like you are replacing
export const get = (url, params, responseType = 'json') => cancellable({
with export const get = item => new Promise((resolve) => { which has a different API.
Regardless, have you tried logging the value of item in the mock get function. I'm guessing it isn't "employee" which is the only property in data.
Yes, that was my goal, to replace the call that was pointing to the backend API, with the call where I would return the mock data. I have tried to log the value of the item, but I get undefined
ok, so there's an awful lot of abstraction going on there. Id suggest starting by replacing get in data/rest/restMethods.jsx directly with a version that returns a promise, get it working, and then break it out. That way you're not dealing with too many unknowns at once.
I had done similar using redux-saga. After debugging, I had found that there must be data property as root key. Here's how you should do:
const employee = {
data: { // root key of employee
items: [
{ name: 'Bhojendra' },
{ name: 'Rauniyar' }
]
}
}
// and no need to use setTimeout, we're just resolving some constant data
export const getItems = item => new Promise(resolve => resolve(employee))
Now, I hope you know why data is undefined with your code.
Still not clear?
Response looks for the data property. That's it.

How to make AJAX request in redux

For all I know, I have to write request in action create. How to use a promise in action for submitting a request? I am getting data in action. Then new state is created in reducer. Bind action and reducer in connect. But I don't know how to use promise for request.
Action
import $ from 'jquery';
export const GET_BOOK = 'GET_BOOK';
export default function getBook() {
return {
type: GET_BOOK,
data: $.ajax({
method: "GET",
url: "/api/data",
dataType: "json"
}).success(function(data){
return data;
})
};
}
Reducer
import {GET_BOOK} from '../actions/books';
const booksReducer = (state = initialState, action) => {
switch (action.type) {
case GET_BOOK:
return state;
default:
return state;
}
};
export default booksReducer;
Container
How display data in container?
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import getBook from '../actions/books';
import Radium from 'radium';
import {Link} from 'react-router';
function mapStateToProps(state) {
return {
books: state.data.books,
};
}
function mapDispatchToProps(dispatch) {
return {
getBooks: () => dispatch(getBook()),
};
}
#Radium
#connect(mapStateToProps, mapDispatchToProps)
class booksPage extends Component {
static propTypes = {
getBooks: PropTypes.func.isRequired,
books: PropTypes.array.isRequired,
};
render() {
const {books} = this.props;
return (
<div>
<Link to={`/authors`}><MUIButton style="flat">All Authors</MUIButton></Link>
<ul>
{books.map((book, index) =>
<li key={index}>
<Link to={`/book/${book.name}`}><MUIButton style="flat"><div class="mui--text-black mui--text-display4">
"{book.name}"</div></MUIButton></Link>
<Link to={`/author/${book.author}`}><MUIButton style="flat"><div class="mui--text-black mui--text-display4">
{book.author}</div></MUIButton></Link>
</li>
)}
</ul>
</div>
);
}
}
export default booksPage;
Since you are already using redux you can apply redux-thunk middleware which allows you to define async actions.
Installation & usage: Redux-thunk
export function fetchBook(id) {
return dispatch => {
dispatch(setLoadingBookState()); // Show a loading spinner
fetch(`/book/${id}`, (response) => {
dispatch(doneFetchingBook()); // Hide loading spinner
if(response.status == 200){
dispatch(setBook(response.json)); // Use a normal function to set the received state
}else {
dispatch(someError)
}
})
}
}
function setBook(data) {
return { type: 'SET_BOOK', data: data };
}
You should use Async Actions described in Redux Documentation
Here an example of reducer for async action.
const booksReducer = (state = {}, action) => {
switch (action.type) {
case 'RESOLVED_GET_BOOK':
return action.data;
default:
return state;
}
};
export default booksReducer;
and then you create your Async Action.
export const getBook() {
return fetch('/api/data')
.then(response => response.json())
.then(json => dispatch(resolvedGetBook(json)))
}
export const resolvedGetBook(data) {
return {
type: 'RESOLVED_GET_BOOK',
data: data
}
}
Several Notes:
We could return Promise (instead of Object) in action by using redux-thunk middleware.
Don't use jQuery ajax library. Use other library specifically for doing that (e.g. fetch()). I use axios http client.
Remember, in redux you only use pure function in reducer. Don't make ajax call inside reducer.
Read the complete guide from redux docs.
You should be able to use dispatch inside the callback (if you pass it as an argument):
export default function getBook(dispatch) {
$.ajax({
method: "GET",
url: "/api/data",
dataType: "json"
}).success(function(data){
return dispatch({type:'GET_BOOK', data: data});
});
}
Then, pass dispatch to the action:
function mapDispatchToProps(dispatch) {
return {
getBooks: () => getBook(dispatch),
};
}
Now, you should have access to the action.data property in the reducer:
const booksReducer = (state = initialState, action) => {
switch (action.type) {
case GET_BOOK:
//action.data <--- here
return state;
default:
return state;
}
};
You might want to separate concerns, to keep action creators "pure".
Solution; write some middleware. Take this for example (using superagent).
import Request from 'superagent';
const successHandler = (store,action,data) => {
const options = action.agent;
const dispatchObject = {};
dispatchObject.type = action.type + '_SUCCESS';
dispatchObject[options.resourceName || 'data'] = data;
store.dispatch(dispatchObject);
};
const errorHandler = (store,action,err) => {
store.dispatch({
type: action.type + '_ERROR',
error: err
});
};
const request = (store,action) => {
const options = action.agent;
const { user } = store.getState().auth;
let method = Request[options.method];
method = method.call(undefined, options.url)
if (user && user.get('token')) {
// This example uses jwt token
method = method.set('Authorization', 'Bearer ' + user.get('token'));
}
method.send(options.params)
.end( (err,response) => {
if (err) {
return errorHandler(store,action,err);
}
successHandler(store,action,response.body);
});
};
export const reduxAgentMiddleware = store => next => action => {
const { agent } = action;
if (agent) {
request(store, action);
}
return next(action);
};
Put all this in a module.
Now, you might have an action creator called 'auth':
export const auth = (username,password) => {
return {
type: 'AUTHENTICATE',
agent: {
url: '/auth',
method: 'post',
resourceName: 'user',
params: {
username,
password
}
}
};
};
The property 'agent' will be picked up by the middleware, which sends the constructed request over the network, then dispatches the incoming result to your store.
Your reducer handles all this, after you define the hooks:
import { Record } from 'immutable';
const initialState = Record({
user: null,
error: null
})();
export default function auth(state = initialState, action) {
switch (action.type) {
case 'AUTHENTICATE':
return state;
case 'AUTHENTICATE_SUCCESS':
return state.merge({ user: action.user, error: null });
case 'AUTHENTICATE_ERROR':
return state.merge({ user: null, error: action.error });
default:
return state;
}
};
Now inject all this into your view logic. I'm using react as an example.
import React from 'react';
import ReactDOM from 'react-dom';
/* Redux + React utils */
import { createStore, applyMiddleware, bindActionCreators } from 'redux';
import { Provider, connect } from 'react-redux';
// thunk is needed for returning functions instead
// of plain objects in your actions.
import thunkMiddleware from 'redux-thunk';
// the logger middleware is useful for inspecting data flow
import createLogger from 'redux-logger';
// Here, your new vital middleware is imported
import { myNetMiddleware } from '<your written middleware>';
/* vanilla index component */
import _Index from './components';
/* Redux reducers */
import reducers from './reducers';
/* Redux actions*/
import actionCreators from './actions/auth';
/* create store */
const store = createStore(
reducers,
applyMiddleware(
thunkMiddleware,
myNetMiddleware
)
);
/* Taint that component with store and actions */
/* If all goes well props should have 'auth', after we are done */
const Index = connect( (state) => {
const { auth } = state;
return {
auth
};
}, (dispatch) => {
return bindActionCreators(actionCreators, dispatch);
})(_Index);
const provider = (
<Provider store={store}>
<Index />
</Provider>
);
const entryElement = document.getElementById('app');
ReactDOM.render(provider, entryElement);
All of this implies you already set up a pipeline using webpack,rollup or something, to transpile from es2015 and react, to vanilla js.
Consider using the new thunk API
export const load = createAsyncThunk(
'example/api',
async (arg, thunkApi) => {
const response = await fetch('http://example.api.com/api')
if (response.status === 200) {
const json = await response.json()
return json
},
)
Also, in the new redux template application, actions are part of the reducer/slice, and you can use extraReducers to response to events related to the async action status. It is much simpler using redux this way.
See documentation of async thunk here: https://redux.js.org/usage/writing-logic-thunks

Resources