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.jsfetchCourses()
action the first time you are rendering your viewfetchCourses()
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:
course
object the endpoint is called with), orstate.courses
array, like, return [...state, action.response]
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]
.
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
. Object
s 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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With