Repository navigation
Integrate back end server to React app
Now that we have a full-fledged API and database persistence ready, we need to wire our app to it.
In order to keep both the back-end server and the create-react-app server and our API running
at the same time, we will install a lib called concurrently in dev via yarn. The reason we're doing
it only in dev is because react-scripts won't be needed in production and the app will be served
by our Node.js server.
yarn add concurrently --devOnce concurrently is installed, we will update the start script in our package.json file
to enable starting both the front and back end servers at the same time.
nano package.json...
"scripts": {
"start": "concurrently \"react-scripts start\" \"node server.js\"",
...This way, when we run yarn start, we will be booting up both servers at the same time.
We've now reached a situation where we will need to make asynchronous calls to the server constantly
and update the Redux store with data coming from the MongoDB database. However, Redux as it comes
out-of-the-box doesn't support asynchronous calls, so we will need to include a Redux middleware
to handle such calls. This middleware is called redux-thunk and can be installed via yarn.
yarn add redux-thunkredux-thunk allows us to return functions as well as objects in our action-creating functions.
The functions returned receive a function as an argument, dispatch, that's much like the
mapDispatchToProps function we always create when creating stateful components. Once redux-thunk
is installed, we need to include it in our index.js file.
nano src/index.js...
import { createStore, applyMiddleware } from 'redux';
import ReduxThunk from 'redux-thunk';
...
const store = createStore(reducer, {}, applyMiddleware(ReduxThunk));
...Now we can call async functions and dispatch actions inside their callbacks by returning a function
instead of an object outright. Let's try creating a CONTACT_FETCH action.
nano src/actions/index.js...
export const contactFetch = () => dispatch => {
fetch('http://localhost:8080/contacts')
.then(response => response.json())
.then(json => {
dispatch({
type: 'CONTACT_FETCH',
contacts: json
});
});
};nano src/reducers/contactsReducer.js...
case 'CONTACT_FETCH':
return {
...state,
contactList: action.contacts
};
...Now that we have an action creator and our reducer is ready to populate contactList, it's time
to prep ContactList to list from the database. We will use a lifecycle method called componentWillMount
to call the contactFetch action and wait for the server to respond. Once it does, ContactList
will re-render with the items currently saved to the database.
nano src/containers/ContactList.js...
import { contactDelete, contactFetch } from '../actions';
...
class ContactList extends Component {
componentWillMount() {
this.props.onLoad();
}
...
const mapDispatchToProps = dispatch => ({
onClickDelete: index => dispatch(contactDelete(index)),
onLoad: () => dispatch(contactFetch())
});
...Run yarn start, then refresh the app. Nothing should show up, because our database is empty...
But we have a problem.
CORS (Cross-Origin Resource Sharing) is a security rule that browsers and servers enforce to avoid
unauthorized external access. Then you might ask yourself, "but I'm in localhost!" Well, you are,
but you have two different servers running in different ports and that violates the same-origin
policy. For this, we are going to proxy all local requests to the back-end server.
When we create a React app via create-react-app, it's actually wrapping a Webpack
module bundler in a black box that handles all the configuration for us so that we don't have
to worry about doing too much setup to start developing our app. And, when we run yarn start
in dev, the app is actually running on a Webpack dev server with all the configuration necessary,
including hot reloading. That's why we have two servers in development, but only one in production.
This is also why we had to implement CORS only in development. But CORS isn't actually needed, since
Webpack has a feature called server proxying that points all local AJAX requests to the
address we provide.
Since Webpack is in a black box, we will provide the server address via package.json, which
Webpack reads and fetches information from.
nano package.json...
"scripts": {
...
},
"proxy": "http://localhost:8080"
...Change the API endpoint to use port 3000:
nano src/actions/index.js...
export const contactFetch = () => dispatch => {
fetch('/contacts')
.then(response => response.json())
.then(json => {
dispatch({
type: 'CONTACT_FETCH',
contacts: json
});
});
};Run yarn start again and the changes should reflect now.
Now we shouldn't have any issue regarding CORS. Run yarn start again and refresh the app. Still
not listing anything, but this time it's legit about the MongoDB collection being empty. Let's add
something to it via curl just so that we can see something listing on our screen.
curl -X POST -H "Content-Type: application/json" --data '{"name": "Jules", "email": "jules@example.com", "phone": "11111"}' http://localhost:8080/contactsRefresh the app and you'll see a Jules on the list with number 1111 and e-mail jules@example.com. Now that we have real persistence, we won't lose data between refreshes like previously through this guide.
The other actions are pretty much straightforward and little has to be done when it comes to modifying the reducers, since they're working as expected. We just have to adjust the actions to perform the requests then dispatch the actions when they're done.
nano src/actions/index.js...
export const contactFetch = () => dispatch => {
fetch('/contacts')
.then(response => response.json())
.then(json => {
dispatch({
type: 'CONTACT_FETCH',
contacts: json
});
});
};
export const contactAdd = contact => dispatch => {
fetch('/contacts', {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(contact)
})
.then(response => response.json())
.then(() => dispatch({
type: 'CONTACT_ADD',
contact
}));
};
export const contactEdit = contact => dispatch => {
fetch(`/contacts/${contact.id}`, {
method: "PUT",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(contact)
})
.then(() => dispatch({
type: 'CONTACT_EDIT',
contact
}));
};
export const contactDelete = id => dispatch => {
fetch(`/contacts/${id}`, {
method: "DELETE"
})
.then(() => dispatch({
type: 'CONTACT_DELETE',
id
}));
};Let's also refactor some changes to contactsReducer to reduce code repetition (following
the DRY principle).
nano src/reducers/contactsReducer.jsconst initialState = {
contactList: []
};
const findContactIndex = (state, id) => state.contactList.findIndex(c => c.id === id);
export const contactsReducer = (state = initialState, action) => {
switch (action.type) {
case 'CONTACT_FETCH':
return {
...state,
contactList: action.contacts
};
case 'CONTACT_ADD':
return {
...state,
contactList: state.contactList.concat(action.contact)
}
case 'CONTACT_EDIT': {
const index = findContactIndex(state, action.contact.id);
return {
...state,
contactList: [
...state.contactList.slice(0, index),
action.contact,
...state.contactList.slice(index + 1)
]
};
}
case 'CONTACT_DELETE': {
const index = findContactIndex(state, action.id);
return {
...state,
contactList: [
...state.contactList.slice(0, index),
...state.contactList.slice(index + 1)
]
};
}
default:
return state;
}
};Refresh the app and test it to see if the changes reflect to the database.
Also, we don't need uuid anymore, so you can uninstall it at will.
Now that we're exclusively using MongoDB IDs instead of indexes, we need to refactor ContactList
to pass these keys to the deletion button. The reducer will then search for the array index itself.
nano src/containers/ContactList.js...
<tbody>
{
this.props.contactList.map(({ id, name, phone, email }, index) => (
<tr key={index}>
<td>{name}</td>
<td>{phone}</td>
<td>{email}</td>
<td>
<Link to={`/edit/${id}`} className="btn btn-primary">Edit</Link>
</td>
<td>
<Button
buttonType="btn-danger"
onClick={() => this.props.onClickDelete(id)}
>
Delete
</Button>
</td>
</tr>
))
}
</tbody>
...
const mapDispatchToProps = dispatch => ({
onClickDelete: id => dispatch(contactDelete(id)),
onLoad: () => dispatch(contactFetch())
});
...Now that we have an actual database to fetch data from, instead of loading up the entire list
of contacts to ContactEdit's state and looking for the desired contact on that list, we will
fetch the contact directly from the database. For that, we'll fetch the contact directly on ContactEdit
and add it to its component state (without Redux), while removing mapStateToProps altogether.
nano src/containers/ContactEdit.js...
componentWillMount() {
const { contactId } = this.props.match.params;
fetch(`/contacts/${contactId}`)
.then(response => response.json())
.then(contact => this.setState({ contact }));
}
...
export default connect(null, mapDispatchToProps)(withRouter(ContactEdit));In ContactForm, we need to enable state reloading, otherwise it won't load the initial values provided to
it when ContactEdit re-renders with the fetched contact.
nano src/containers/ContactForm...
export default reduxForm({
form: 'contact',
enableReinitialize: true // This makes the form initialize again when ContactEdit re-renders
})(ContactForm);Refresh the app and test it. You'll see that the changes are kept parallel between the Redux state and the database.
git add .
git commit -m "Integrate app's front-end to back-end"Next step: Configure Form Validation