Customizing a Simple Social Media Application Using MERN
Learn how to use the MERN stack to build simple social media application features in this tutorial by Shama Hoque.
MERN Social
MERN Social is a sample social media application, used for the purpose of this article, with rudimentary features inspired by existing social media platforms such as Facebook and Twitter. In this article, you’ll learn how to update a user profile in MERN Social that can display a user uploaded profile photo and an about description.
The main purpose of this application is to demonstrate how to use the MERN stack technologies to implement features that allow users to connect and interact over content. You can extend these implementations further, as desired, for more complex features.
The code for the complete MERN Social application is available on GitHub in the repository at github.com/shamahoque/mern-social. You can clone this code and run the application as you go through the code explanations. Note that you will need to create a MERN skeleton application by adding a working React frontend, including frontend routing and basic server-side rendering of the React views for following these steps.
The views needed for the MERN Social application can be developed by extending and modifying the existing React components in the MERN skeleton application. The following component tree shows all the custom React components that make up the MERN Social frontend and also exposes the composition structure that you can use to build out extended views:
Updating the user profile
The skeleton application only has support for a user’s name, email, and password. However, in MERN Social, you can allow users to add their description and also upload a profile photo while editing the profile after signing up:
Adding an about description
In order to store the description entered in the about field by a user, you need to add an about field to the user model in server/models/user.model.js:
about: { type: String, trim: true }
Then, to get the description as input from the user, you can add a multiline TextField to the EditProfile form in mern-social/client/user/EditProfile.js:
<TextField id="multiline-flexible" label="About" multiline rows="2" value={this.state.about} onChange={this.handleChange('about')} />
Finally, to show the description text added to the about field on the user profile page, you can add it to the existing profile view in mern-social/client/user/Profile.js:
<ListItem> <ListItemText primary={this.state.user.about}/> </ListItem>
With this modification to the user feature in the MERN skeleton code, users can now add and update a description about them to be displayed on their profiles.
Uploading a profile photo
Allowing a user to upload a profile photo will require that you store the uploaded image file and retrieve it on request to load in the view. For MERN Social, you need to assume that the photo files uploaded by the user will be of small sizes and demonstrate how to store these files in MongoDB for the profile photo upload feature.
There are multiple ways of implementing this upload feature considering the different file storage options:
- Server filesystem: Upload and save files to a server filesystem and store the URL to MongoDB
- External file storage: Save files to an external storage such as Amazon S3 and store the URL in MongoDB
- Store as data in MongoDB: Save files of a small size (less than 16 MB) to MongoDB as data of type Buffer
Updating the user model to store a photo in MongoDB
In order to store the uploaded profile photo directly in the database, you can update the user model to add a photo field that stores the file as data of type Buffer, along with its contentType in mern-social/server/models/user.model.js:
photo: { data: Buffer, contentType: String }
Uploading a photo from the edit form
Users should be able to upload an image file from their local files when editing the profile. You can update the EditProfile component in client/user/EditProfile.js with an upload photo option and then attach the user selected file in the form data submitted to the server.
File input with Material-UI
You can use the HTML5 file input type to let the user select an image from their local files. The file input will return the filename in the change event when the user selects a file in mern-social/client/user/EditProfile.js:
<input accept="image/*" type="file" onChange={this.handleChange('photo')} style={{display:'none'}} id="icon-button-file" />
To integrate this file input with Material-UI components, you can apply display:none to hide the input element from view, then add a Material-UI button inside the label for this file input. This way, the view displays the Material-UI button instead of the HTML5 file input element in mern-social/client/user/EditProfile.js:
<label htmlFor="icon-button-file"> <Button variant="raised" color="default" component="span"> Upload <FileUpload/> </Button> </label>
With the Button‘s component prop set to span, the Button component renders as a span element inside the label element. A click on the Upload span or label is registered by the file input with the same ID as the label, and as a result, the file select dialog is opened. Once the user selects a file, you can set it to state in the call to handleChange(…) and display the name in the view in mern-social/client/user/EditProfile.js:
<span className={classes.filename}> {this.state.photo ? this.state.photo.name : ''} </span>
Form submission with the file attached
Uploading files to the server with a form requires a multipart form submission. You can modify the EditProfile component to use the FormData API to store the form data in the format needed for encoding type multipart/form-data.
First, you need to initialize FormData in componentDidMount() in mern-social/client/user/EditProfile.js:
this.userData = new FormData()
Next, you need to update the input handleChange function to store input values for both the text fields and the file input in FormData in mern-social/client/user/EditProfile.js:
handleChange = name => event => { const value = name === 'photo' ? event.target.files[0] : event.target.value this.userData.set(name, value) this.setState({ [name]: value }) }
Then, on clicking submit, this.userData is sent with the fetch API call to update the user. As the content type of the data sent to the server is no longer ‘application/json’, you’ll also need to modify the update fetch method in api-user.js to remove Content-Type from the headers in the fetch call in mern-social/client/user/api-user.js:
const update = (params, credentials, user) => { return fetch('/api/users/' + params.userId, { method: 'PUT', headers: { 'Accept': 'application/json', 'Authorization': 'Bearer ' + credentials.t }, body: user }).then((response) => { return response.json() }).catch((e) => { console.log(e) }) }
Now if the user chooses to upload a profile photo when editing profile, the server will receive a request with the file attached along with the other field values.
Processing a request containing a file upload
On the server, to process the request to the update API that may now contain a file, you need to use the formidable npm module:
npm install --save formidable
Formidable will allow you to read the multipart form data, giving access to the fields and the file, if any. If there is a file, formidable will store it temporarily in the filesystem. You need to read it from the filesystem, using the fs module to retrieve the file type and data and store it in the photo field in the user model. The formidable code will go in the update controller in mern-social/server/controllers/user.controller.js as follows:
import formidable from 'formidable' import fs from 'fs' const update = (req, res, next) => { let form = new formidable.IncomingForm() form.keepExtensions = true form.parse(req, (err, fields, files) => { if (err) { return res.status(400).json({ error: "Photo could not be uploaded" }) } let user = req.profile user = _.extend(user, fields) user.updated = Date.now() if(files.photo){ user.photo.data = fs.readFileSync(files.photo.path) user.photo.contentType = files.photo.type } user.save((err, result) => { if (err) { return res.status(400).json({ error: errorHandler.getErrorMessage(err) }) } user.hashed_password = undefined user.salt = undefined res.json(user) }) }) }
This will store the uploaded file as data in the database. Next, you’ll need to set up file retrieval to be able to access and display the photo uploaded by the user in the frontend views.
Retrieving a profile photo
The simplest option to retrieve the file stored in the database and show it in a view is to set up a route that will fetch the data and return it as an image file to the requesting client.
Profile photo URL
You need to set up a route to the photo stored in the database for each user and also add another route that will fetch a default photo if the given user has not uploaded a profile photo in mern-social/server/routes/user.routes.js:
router.route('/api/users/photo/:userId') .get(userCtrl.photo, userCtrl.defaultPhoto) router.route('/api/users/defaultphoto') .get(userCtrl.defaultPhoto)
You need to look for the photo in the photo controller method and if found, send it in the response to the request at the photo route; otherwise, you’ll need to call next() to return the default photo in mern-social/server/controllers/user.controller.js:
const photo = (req, res, next) => { if(req.profile.photo.data){ res.set("Content-Type", req.profile.photo.contentType) return res.send(req.profile.photo.data) } next() }
The default photo is retrieved and sent from the server’s file system in mern-social/server/controllers/user.controller.js:
import profileImage from './../../client/assets/images/profile-pic.png' const defaultPhoto = (req, res) => { return res.sendFile(process.cwd()+profileImage) }
Showing a photo in a view
With the photo URL routes set up to retrieve the photo, you can simply use these in the img element’s src attribute to load the photo in the view in mern-social/client/user/Profile.js. For example, in the Profile component, you’ll get the user ID from state and use it to construct the photo URL.
const photoUrl = this.state.user._id ? `/api/users/photo/${this.state.user._id}?${new Date().getTime()}` : '/api/users/defaultphoto'
To ensure that the img element reloads in the Profile view after the photo is updated in the edit, you need to also add a time value to the photo URL to bypass the browser’s default image caching behavior.
Then, you can set the photoUrl to the Material-UI Avatar component, which renders the linked image in the view:
<Avatar src={photoUrl}/>
The updated user profile in MERN Social can now display a user uploaded profile photo and an about description:
If you found this article helpful, you can explore Shama Hoque’s Full-Stack React Projects to unleash the power of MERN stack. This book guides you through MERN stack-based web development, for creating a basic skeleton application and extending it to build four different web applications.
Leave a Reply