Redux middleware with callback - javascript

I really like the concept of having actions written like this:
function signUp(data, callback) {
return {
[CALL_API]: {
type: 'SOME_TYPE',
url: `/api/account/signup`,
method: 'POST',
data: data
}
}
}
But for thing like signUp I don't want to modify/touch the store to get a callback from server
In my React component I have an action that calls the api through an action and changes the state.
this.signUp($(form).serialize(), function(data) { this.setState({response: data}); }.bind(this))
and action signUp looks like this
function signUp(data, callback) {
postRequest(
'/api/account/signup',
data,
'POST',
callback)
}
function postRequest(url, data, method, callback) {
callback(true); //// just testing
}
As you can see the syntax and the length of code isn't pretty compared to the first one
Question: Is there a way to modify the redux middleware OR have an alternative JSON function (similar to CALL_API) to accept callbacks to component without touching the store? I really want to use the CALL_API syntax :)

You can write middleware to intercept actions and do some work without modifying the store's state.
// Action creator
function signUp(data, callback) {
return {
type: CALL_API
url: '/api/account/signup',
method: 'POST',
data: data,
callback: callback
}
}
// Middleware
const actionInterceptor = store => next => action => {
if (action.type === CALL_API) {
postRequest(action.url, action.method, action.data, action.callback);
}
else {
return next(action);
}
}
...
const store = createStore(reducer, initialState, applyMiddleware(actionInterceptor));

I really like the concept of having actions written like this:
...
But for thing like signUp I don't want to modify/touch the store to get a callback from server
Ok, but whats the point of using redux for this?
What I understood:
you would like to call dispatch(signUp(data, callback)) (simplified)
Your signUp actionCreator should look like this:
function signUp(data, callback) {
return function(dispatch) {
//.. do your stuff with data..
//.. and call your callback or pass it to another function
}
}
Sorry if misunderstood your problem, please write a comment if something is still unclear.

Related

Can I use OOP in Redux Store actions

I'm using React JS and Redux JS.
I know that Redux actions functions should be a pure function
But, I was reading about Object Oriented Programming (OOP) and its benefits.
So, Can I use OOP in Redux action (Class) instead of pure functions ??
For example:
If I've a state named "articles", It may have actions like:
function getArtcles() { ... }
function addArtcle({data}) { ... }
function deleteArtcle(id) { ... }
......
So, can I replce that with:
class Artice {
getArtcles() {...}
addArtcle() {...}
deleteArtcle() {...}
}
??
I assume those functions are action creators? Ie, they return an object with a type property, and possibly some additional data?
function deleteArticle(id) {
return { type: 'deleteArticles', id };
}
Or they may create thunks:
function deleteArticle(id) {
return async function (dispatch) {
dispatch({ type: 'deleteStarting' });
// do something asynchronous
dispatch({ type: 'deleteSuccessful' });
}
}
If so, yes, you can put them in a class if you want. I don't see a benefit to putting these in a class, but redux doesn't even have a way to know how you created the actions, so it won't mind.

React - TypeError: this.props.courses.map is not a function

I have created a react-redux application. Currently what it does is load courses from the server(api), and displays them to the course component. This works perfectly. I'm trying to add a feature where you can create a course by posting it to the server, the server would then true an a success object. However, when i post to the server i get the following error(see below). I think this is due to my connect statement listening for the load courses action. Clearly its thinking it should be getting a list of something, instead of a success object. I have tried a few thing for it to listen for both courses and the success response, but to save you the time of reading all the strange thing i have done, i could not get it to work. Dose anyone know how to fix this issue ?
error
TypeError: this.props.courses.map is not a function
course.component.js
onSave(){
// this.props.createCourse(this.state.course);
this.props.actions.createCourse(this.state.course);
}
render() {
return (
<div>
<div className="row">
<h2>Couses</h2>
{this.props.courses.map(this.courseRow)}
<input
type="text"
onChange={this.onTitleChange}
value={this.state.course.title} />
<input
type="submit"
onClick={this.onSave}
value="Save" />
</div>
</div>
);
}
}
// Error occurs here
function mapStateToProps(state, ownProps) {
return {
courses: state.courses
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(courseActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Course);
course.actions.js
export function loadCourse(response) {
return {
type: REQUEST_POSTS,
response
};
}
export function fetchCourses() {
return dispatch => {
return fetch('http://localhost:3000/api/test')
.then(data => data.json())
.then(data => {
dispatch(loadCourse(data));
}).catch(error => {
throw (error);
});
};
}
export function createCourse(response) {
return dispatch => {
return fetch('http://localhost:3000/api/json', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
response: response
})
})
.then(data => data.json())
.then(data => {
dispatch(loadCourse(data));
}).catch(error => {
throw (error);
});
};
}
course.reducer.js
export default function courseReducer(state = [], action) {
switch (action.type) {
case 'REQUEST_POSTS':
return action.response;
default:
return state;
}
}
server.js
router.get('/test', function(req, res, next) {
res.json(courses);
});
router.post('/json', function(req, res, next) {
console.log(req.body);
res.json({response: 200});
});
i have tried added a response to the state, and listening for it in the map state to props, but still for some reason react is trying to map response to courses. Do i need a second connect method?
function mapStateToProps(state, ownProps) {
return {
courses: state.courses,
resposne: state.resposne
};
}
As you can see from the pictures response is getting mapped as courses and not as response.
Picture
Assumptions:
state.courses is initially an empty array - from course.reducer.js
You don't call fetchCourses() action the first time you are rendering your view
Even if you call fetchCourses() there is no problem as long as courses in server.js is an array (the array in the response replaces the initial state.courses)
Flow:
Now I assume the first render is successful and React displays the <input type="text"> and submit button. Now when you enter the title and click on the submit button, the onSave() method triggers the createCourse() action with parameter that is more or less similar to { title: 'something' }.
Then you serialize the above mentioned param and send to the server (in course.actions.js -> createCourse()) which in turn returns a response that looks like {response: 200} (in server.js). Response field is an integer and not an array! Going further you call loadCourses() with the object {response: 200} which triggers the courseReducer in course.reducer.js
The courseReducer() replaces state.courses (which is [] acc. to assumption) with an integer. And this state update triggers a re-render and you end up calling map() on an integer and not on an array, thus resulting in TypeError: this.props.courses.map is not a function.
Possible Solution:
Return a valid response from serve.js (i.e. return the course object the endpoint is called with), or
Update your reducer to add the new course object into the existing state.courses array, like, return [...state, action.response]
Update:
Based on OP's comment, if what you want to do is send the new course object to the server, validate it and send success (or error) and based on response add the same course object to the previous list of courses, then you can simply call loadData() with the same course object you called createCourse() with and (as mentioned above) inside your reducer, instead of replacing or mutating the old array create a new array and append the course object to it, in es6 you can do something like, return [...state, course].
Update 2:
I suggest you go through Redux's Doc. Quoting from Redux Actions' Doc
Actions are payloads of information that send data from your application to your store. They are the only source of information for the store.
The createCourse() action is called with a payload which is more-or-less like,
{title: 'Thing you entered in Text Field'}, then you call your server with an AJAX-request and pass the payload to the server, which then validates the payload and sends a success (or error) response based on your logic. The server response looks like, {response: 200}. This is end of the createCourse()action. Now you dispatch() loadCourses() action from within createCorse(), with the response you received from the server, which is not what you want (based on your comments). So, instead try dispatch()ing the action like this (try renaming response param, it's a bit confusing)
//.....
.then(data => {
dispatch(loadCourse(response)); // the same payload you called createCourse with
})
//.....
Now, loadCourse() is a very basic action and it simply forwards the arguments, which Redux uses to call your reducer. Now, in case you followed the previous discussion and updates how you call loadCourse(), then the return from loadCourse() looks like
{
type: REQUEST_POSTS,
response: {
title: 'Thing you entered in Text Field',
}
}
which is then passed onto your reducer, specifically your courseReducer().
Again quoting from Redux Reducers' Doc
Actions describe the fact that something happened, but don't specify how the application's state changes in response. This is the job of reducers.
The reducer must define the logic on how the action should impact the data inside the store.
In your courseReducer(), you simply returns the response field inside the action object and [expect] Redux to auto-magically mutate your state! Unfortunately this is not what happens :(
Whatever you return from the reducer, completely replaces whatever thing/object was there before, like, if your state looks like this
{ courses: [{...}, {...}, {...}] }
and you return something like this from your reducer
{ title: 'Thing you entered in Text Field'}
then redux will update the state to look like
{ courses: { title: 'Thing you entered in Text Field'} }
state.courses is no longer an Array!
Solution:
Change your reducer to something like this
export default function courseReducer(state = [], action) {
switch (action.type) {
case 'REQUEST_POSTS':
return [...state, action.response]
default:
return state
}
}
Side Note: This is may be confusing at times, so just for the sake of record, state inside courseReducer() is not the complete state but a property on the state that the reducer manages. You can read more about this here
--Edit after reading a comment of you in a different answer, I've scraped my previous answer--
What you're currently doing with your actions and reducers, is that you're calling loadCourse when you fetched the initial courses. And when you created a new course, you call loadCourse too.
In your reducer you're directly returning the response of your API call. So when you fetch all the courses, you get a whole list of all your courses. But if you create a new one you currently receive an object saying response: 200. Objects don't have the map function, which explains your error.
I would suggest to use res.status(200).json() on your API and switching the response status in your front-end (or using then and catch if you can validate the response status, axios has this functionality (validateStatus)).
Next I would create a separate action-type for creating posts and dispatch that whenever it's successful.
I would change your reducer to something like
let initialState = {
courses: [],
createdCourse: {},
}
export default (state = initialState, action) => {
switch (action.type) {
case 'REQUEST_POSTS':
return {
...state,
courses: action.response
}
case 'CREATE_COURSE_SUCCESS':
return {
...state,
createdCourse: action.response,
}
default: return state;
}
}
I wouldn't mind looking into your project and giving you some feedback on how to improve some things (ES6'ing, best practices, general stuff)
Based on the questions & answers so far, it looks like you need to do something like this:
1) Add a new action and dispatch this from your createCourse function
export function courseAdded(course, response) {
return {
type: 'COURSE_ADDED',
course
response
};
}
export function createCourse(course) {
return dispatch => {
return fetch('http://localhost:3000/api/json', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
course
})
})
.then(response => response.json())
.then(response => {
dispatch(courseAdded(course, response));
}).catch(error => {
throw (error);
});
};
}
2) Change your reducers to handle both fetching courses and adding a new course (we're using combineReducers to handle this here)
import { combineReducers } from "redux";
function response(state = null, action) {
switch (action.type) {
case 'COURSE_ADDED':
return action.response;
default:
return state;
}
}
function courses(state = [], action) {
switch (action.type) {
case 'COURSE_ADDED':
return [...state, action.course];
case 'REQUEST_POSTS':
return action.response;
default:
return state;
}
}
export default combineReducers({
courses,
response
});
3) Hook up to the new response state in your connect component
function mapStateToProps(state, ownProps) {
return {
courses: state.courses,
response: state.response
};
}
4) Do something with this new response prop in your component if you want to show it e.g.
// this is assuming response is a string
<span className="create-course-response">
Create Course Response - {this.props.response}
</span>
UPDATE
I've added support for adding the new course to the end of the existing course list, as well as handling the response. How you shape the state is completely up to you and it can be re-jigged accordingly.
In order for this code to work, you will need to add support for the spread operator. If you are using babel it can be done like this. Creating a new object is important to ensure that you don't mutate the existing state. It will also mean react-redux knows the state has changed. Spread operator isn't essential and this can be done with Object.assign, but that syntax is ugly IMO.

Issues with $.ajax, $.when and apply

I am having an issue with $.ajax, $.when and apply. I have a constructor:
The ajax request is not triggered when it should be :
http://plnkr.co/edit/Ul9d8tB7BHoZyHzzQQyB?p=preview
(see the console)
function updateGeneralTerm() {
return {
id: "GeneralCondition",
ajax: function () {
return $.ajax({
type: 'POST',
url: "#Url.Action("UpdateGeneralTerms", "Agreements")",
data: $("#GeneralConditions").serialize()
})
}
}
}
//I inject it in my custom function
function CustomFunction(arr) {
let arrOfAjax = arr.map(function (obj) {
return obj.ajax
});
$.when.apply(null, arrOfAjax);
}
CustomFunction([new updateGeneralTerm()];
In my CustomFunction, I am checking other stuff, as does the form has changed... etc. However it doesn't seem to be relevant for my issue. Nothing happens.
In the future I might have n specific term that I'd like to update only if forms has changed.
My issue: ajax are not requested by $.when(). If I am changing to return obj.ajax(), the ajax request is triggered there directly not by the $.when().
I 'd like the $.when() to handle all ajax requests.
http://plnkr.co/edit/Ul9d8tB7BHoZyHzzQQyB?p=preview
try to rewrite your CustomFunction function to use spread operator:
function CustomFunction(arr) {
let arrOfAjax = arr.map(function (obj) {
return obj.ajax
});
$.when(...arrOfAjax).then(function(...results) {
console.log(results);
});
}

How to abort pending jquery ajax request when user navigates to other page?

I have defined a method to send jquery ajax request like this
sendAjaxRequest(URL, data) {
return $.ajax({
type : 'POST',
url : URL,
crossDomain : true,
data : JSON.stringify(data),
dataType : 'json'
}).fail((responseData) => {
if (responseData.responseCode) {
console.error(responseData.responseCode);
}
});
}
From the place of calling I keep the reference of ajax call
this.ajaxRequest = sendAjaxRequest(Url, data);
and based on user navigation if user navigates away from page ( goes to another page on same website via router ) I want to cancel any pending ajax request.
From the docs I found that jquery jqXHR object has an abort method to cancel any ajax request. But in my ajaxRequest object I don't have any such method available. I am using jquery 2.2.
For clarification I also want to add that I am using react, and want to achieve this.
componentDidMount: function() {
this.serverRequest = $.get(this.props.source, function (result) {
var lastGist = result[0];
this.setState({
username: lastGist.owner.login,
lastGistUrl: lastGist.html_url
});
}.bind(this));
},
componentWillUnmount: function() {
this.serverRequest.abort();
},
Taken from this link.
They basically say that When fetching data asynchronously, use componentWillUnmount to cancel any outstanding requests before the component is unmounted.
I must be doing something wrong. Any help is appreciated.
According to the ajax documentation, you're doing it properly. The $.ajax() function returns an object that has an abort function. You should check for existence of the function because it might not exist if the request has already finished.
componentWillUnmount: function(){
if (this.ajaxRequest && this.ajaxRequest.abort){
this.ajaxRequest.abort()
}
}
http://api.jquery.com/jquery.ajax/#jqXHR
Please don't try to abort the AJAX request in this case. The better way is not to do anything if the component is unmounted. Take a look at this:
componentDidMount: function() {
// Register to the handler
this._requestSuccess = this.requestSuccess.bind(this);
$.get(this.props.source, this.requestSuccess);
},
componentWillUnmount: function() {
// "Unregister" by assign it to an empty function
this._requestSuccess = () => {};
},
requestSuccess(lastGist) {
// Do your work
this.setState({
username: lastGist.owner.login,
lastGistUrl: lastGist.html_url
});
},
In the big picture, not just Ajax requests but any async operation inside a component that may change the component's states should be register in componentDidMount and unregister in componentWillUnmount, or else you'll find yourself get lost async spaghetti mess.
In the above example, I used this._requestSuccess = ...; to register and this._requestSuccess = () => {}; to unregister the handler, but in real world application, you'll see people use Flux or Redux to do the same thing.
I highly encourage you to use one of those to manage your application data flow rather than just sending and aborting ajax requests everywhere.
Just have a look on the fiddle. Run the fiddle and check the network tab.
$('button').on('click', function() {
var _ajax = $.ajax({
type: 'POST',
url: "yourURL",
crossDomain: true,
data: JSON.stringify('hello'),
dataType: 'json'
}).fail((responseData) => {
if (responseData.responseCode) {
console.error(responseData.responseCode);
}
});
_ajax.abort();
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
<button>
Click me
</button>
The ajax variable will hold all the ajax related functions as shown in the documentation.
If componentWillUnmount is not called have a look at your router configuration.
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="invoices/:invoiceId" component={Invoice}/>
<Route path="accounts/:accountId" component={Account}/>
</Route>
In this example the {App} component is never unmounted since it's at the root. When you navigate from /invoices/ to /invoices/123 the Invoices component is not unmounted either.
Alternatively you can also tie in to the routers setRouteLeaveHook like this
import React from 'react';
import { withRouter } from 'react-router';
const MyComponent = React.creatClass({
componentDidMount() {
this.request = $.get( /* ... */ );
this.props.router.setRouteLeaveHook(this.props.route, (request) => {
request.abort();
return true;
});
}
/* .... */
});
export default withRouter(MyComponent);
withRoute became available in react-router 2.4.0

Cannot get response content in mithril

I've been trying to make a request to a NodeJS API. For the client, I am using the Mithril framework. I used their first example to make the request and obtain data:
var Model = {
getAll: function() {
return m.request({method: "GET", url: "http://localhost:3000/store/all"});
}
};
var Component = {
controller: function() {
var stores = Model.getAll();
alert(stores); // The alert box shows exactly this: function (){return arguments.length&&(a=arguments[0]),a}
alert(stores()); // Alert box: undefined
},
view: function(controller) {
...
}
};
After running this I noticed through Chrome Developer Tools that the API is responding correctly with the following:
[{"name":"Mike"},{"name":"Zeza"}]
I can't find a way to obtain this data into the controller. They mentioned that using this method, the var may hold undefined until the request is completed, so I followed the next example by adding:
var stores = m.prop([]);
Before the model and changing the request to:
return m.request({method: "GET", url: "http://localhost:3000/store/all"}).then(stores);
I might be doing something wrong because I get the same result.
The objective is to get the data from the response and send it to the view to iterate.
Explanation:
m.request is a function, m.request.then() too, that is why "store" value is:
"function (){return arguments.length&&(a=arguments[0]),a}"
"stores()" is undefined, because you do an async ajax request, so you cannot get the result immediately, need to wait a bit. If you try to run "stores()" after some delay, your data will be there. That is why you basically need promises("then" feature). Function that is passed as a parameter of "then(param)" is executed when response is ready.
Working sample:
You can start playing with this sample, and implement what you need:
var Model = {
getAll: function() {
return m.request({method: "GET", url: "http://www.w3schools.com/angular/customers.php"});
}
};
var Component = {
controller: function() {
var records = Model.getAll();
return {
records: records
}
},
view: function(ctrl) {
return m("div", [
ctrl.records().records.map(function(record) {
return m("div", record.Name);
})
]);
}
};
m.mount(document.body, Component);
If you have more questions, feel free to ask here.

Categories