By the end of this lesson. You should be able to set up two separate servers that will speak with each other -- one with frontend code and the other with react code.
- Communicate with an application server using a front-end client
- Login a user via an external API and store a token in LocalStorage
- Logout a user locally
- Signup a user via an external API and store a token in LocalStorage
- Authorize routes on the frontend
- Populate information from an external API
- Delete records through an external API
- Create new records through an external API
- Update existing records through an external API
-
Fork & clone this repository
-
npm install
-
npm start
- Start both your frontend server and your backend server. Then try copying the code below into the web console.
fetch('http://localhost:5000/api/users') .then(res => res.json()) .then(console.log)
-
Question: What error do you get? Why?
-
Your Answer:
--- Error recieved, "Access to fetch at 'http://localhost:5000/api/users' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled."
CORS enables/disables resource sharing from a web page across domains, enabling "access" for requests from outside domains. In this case, we need to enable resource sharing between our frontend and backend.
- To get around this issue, we need to explicitly allow for requests to come from
localhost:3000
. To do so, we will use the cors package. Installcors
on the backend server and whitelistlocalhost:5000
.
-
Question: Try your request again. What error do you get? Why?
-
Your Answer:
---{status: 401, message: "You are not logged in."}
is returned. This is because our request did not include the expected token.
- In
App.js
, we have created ourloginUser()
method. Try invoking that function through the frontend, inspecting what is outputted.
- We now want to try and login the user when they hit submit. Add the following to your
loginUser()
method:fetch('http://localhost:5000/api/login', { body: JSON.stringify(user), headers: { 'Content-Type': 'application/json' }, method: 'POST', }).then(res => res.json()).then(console.log)
-
Question: Why do we need to include the "Content-Type" in the headers?
-
Your Answer:
--- Content type is included in the headers to ensure that the front end and the backend can communicate using the same "language". Content type informs on the format of the incoming headers data
- Question: How could you convert this method to an
async
method?
---Attach/insert async
before the method name itself, and attach/insert await
on the subsequent fetch request and response
-
Let's move our requests to a better place. Create a new file at
./src/api/auth.js
. Add the following inside of it:const { NODE_ENV } = process.env const BASE_URL = NODE_ENV === 'development' ? 'http://localhost:5000' : 'tbd' // Once we deploy, we need to change this export const login = async (user) => { const response = await fetch(`${BASE_URL}/api/login`, { body: JSON.stringify(user), headers: { 'Content-Type': 'application/json' }, method: 'POST' }) const json = await response.json() return json }
Update
App.js
to use thelogin()
function, logging out the response from it.
-
Question: What is happening on the first couple of lines of the new file you've created?
-
Your Answer:
--- We're establishing the environment variable, assigning BASE URL as our local dev environment path if NODE_ENV is development
, assuming we would otherwise be pointing to a production environment path following deployment of our project.
- Let's store the token in LocalStorage with a key of
journal-app
.
-
Question: Why are we storing the token?
-
Your Answer:
--- We are storing the token in order to maintain authentication,
- We now have the token, but we don't have any of the user information. Add a new function to our
./src/api/auth.js
calledprofile()
that sends over the token in order to retrieve the user information. Then, log that information.
-
Question: Where did you write your code to manipulate LocalStorage? Why?
-
Your Answer:
--- I included the statements in the backend /src/api/auth.js
file as you suggested because i agree with the idea that it makes more sense to include these statements in the backend file as this happens "behind the scenes", and isn't component or UI impacting like the react statements in the front end app.js
file for example.
- Now that we have the user's information, let's store the user's ID in state. Set
currentUserId
to the user ID you've retrieved.
-
Question: What changes on the page after you successfully login? Why?
-
Your Answer:
--- The navigation component displays authenticated links when currentUserID is not null. Because we've set currentUserId
with an _id
, the application sees the user as authenticated.
-
Question: What happens if you enter in the incorrect information? What should happen?
-
Your Answer:
---An error is displayed on the page and in console. A better user experience could be defined using a try/catch statement.
- Try refreshing the page. You'll notice it looks like you've been logged out, although your token is still stored in LocalStorage. To solve this, we will need to plug in to the component life cycle with
componentDidMount()
. Try adding the following code toApp.js
:async componentDidMount () { const token = window.localStorage.getItem('journal-app') if (token) { const profile = await auth.profile() this.setState({ currentUserId: profile.user._id }) } }
-
Question: Describe what is happening in the code above.
-
Your Answer:
---This function is checking for a token in local storage of the browser window, and if present, makes a request to profile route to get the profile information. Then, the function is setting the state of App with currentUserId: profile.user._id, where user._id is the _id of the profile associated to the token.
- Now when you refresh the page, it looks as though you are logged in. Next, try clicking the logout button.
-
Question: When you click "Logout", nothing happens unless you refresh the page. Why not?
-
Your Answer:
---The navigation is still showing as though i am authenticated. My browser's local storage still contains the journal-app
token and the app component's currentUserId
in state has not been reset/set to null.
- Update the
logout()
method to appropriately logout the user.
-
Question: What did you have to do to get the
logout()
function to work? Why? -
Your Answer:
--- I had to create the function in App.js and then pass it through each component so that when the logout component is clicked, the logout function is called and the token data is removed and the app component state's currentuserid is set to null
- Following the patterns we used above, build the Signup feature.
- When a user logs in or signs up, we should bring them to the
/users
route. Update both features so that the user is moved to that route after a successful login/signup.
- Try logging out and then go directly to the
/users
route.
-
Question: What happens? What should happen?
-
Answer:
---I see the /users
page as if i was authenticated. Because i am not authenticated, i should not be authorized to see this page.
- Try replacing the
/users
Route inApp.js
with the following:<Route path='/users' render={() => { return this.state.currentUserId ? <UsersContainer /> : <Redirect to='/login' /> }} />
-
Question: Describe what is happening in the code above.
-
Your Answer:
---Upon accessing /users
route, the App component's state is checked for currentUserId. If currentUserID is present, the app will render the UsersContainer component. If it is not present, the login component is instead rendered as the user is not authenticated, and thus not authorized to view the page.
- Now try logging in. Then, when you're on the
/users
page, refresh the page.
-
Question: What happens and why?
-
Your Answer:
--- I am not sure i fully grasp this quite yet, but i believe it is because the page is refreshed on the /users
route before the app component is mounted and currentUserID is not yet set into state. because of this, the users page cannot be shown to the user as per the route, we are redirecting to login page when no currentUserID is found. it seems that the /users
route execution precedes componentDidMount()
. Is this correct?
- To solve this problem, let's add a
loading
key to our App's state, with the default value set totrue
. WhencomponentDidMount()
finishes, set theloading
key to equalfalse
. Using this key, solve the issue of refreshing on the/users
page. Make sure everyting continues to work whether you are logged in or out.
-
Question: What did you do to solve this problem?
-
Your Answer:
---After setting loading: true
in app component's state, i added a statement in componentDidMount()
to set loading: false
in the component's state once mount has completed.
- We will have the same problem on the
/users/<userId>/posts
page. Use the same strategy to have this page load correctly on refresh.
-
Question: In what component did you add the
loading
property and why? -
Your Answer:
---I am a little confused on this question as we skipped past this step in the in-class exercise, and now after our other changes made i am not able to reproduce a similar experience to that of the above scenario with /users
. Would this need to be added in a similar fashion via componentDidMount()
on the posts component?
- Using the same principals as above, make it so that if the user is logged in, they cannot go to the
/login
or/signup
routes. Instead, forward them to/users
.
-
Right now, the data inside of
users/Container.js
is static. UsingcomponentDidMount()
, update this code so that we pull our data from our API._NOTE: You may want to create a new file in
./src/api/
to organize these requests.
- Let's get our "Delete" link working. On the backend, create a
DELETE Post
route with the path of:This request should only be able to be made if the user is logged in and it's the user's post.DELETE /users/:userId/posts/:postId
- On the frontend, create a new function in your
src/api
folder that will delete a post. Use that function inside of thesrc/components/posts/Container
file. Upon successful deletion, send the user back to the/users/<userId>/posts
route.
- Try deleting a post using the link.
-
Question: Why did the number of posts not change when you were redirected back to the
/users
route? -
Your Answer:
--- We did not refresh our state, and in turn, refresh the posts count for the user(s).
-
Question: Whenever we modify our data with a Create, Update, or Delete, we have a few options on how to make our frontend reflect those changes. What options can you think of?
-
Your Answer:
--- I thought the suggestion to refresh state entirely seemed most appropriate. This would ensure any additional edits, adds, etc. are returned to the user in real-time.
- Using your preferred method, update your code so that the frontend will reflect the changes made to the backend.
- Right now it looks like we can Edit and Delete posts for other users. Hide/display those actions to only be available on a post if it's the user's post.
- Let's get our "Create a New Post" form to work. On the backend, create a
CREATE Post
route with the path of:This request should only be able to be made if the user is logged in and it's from the same user as the one specified in the route.POST /users/:userId/posts
- On the frontend, build a function that will POST to the database. Connect that function to the
onSubmit
functionality for the creation form. Finally, use your preferred method to update the state of our frontend. Upon successful creation, send the user back to the/users/<user-id>/posts
page.
- Our final step is to get our Update form to work. Follow the steps from above to finish this final feature.
We got a lot done but there's still a lot to do to make this app fully functional. Complete the following features on this application.
-
If there are no posts for a user, show a message on their
/users/<userId>/posts
page that encourages them to create a new post. -
If there is no emotion for a post, hide the associated message on each post.
-
Show the user's username on the navigation when they are logged in as a link. When clicked, go to a new page:
/users/<userId>/edit
-
Create a page at
/users/<userId>/edit
that allows a user to update theirname
. On save, redirect them to their/users/<userId>/posts
page. -
If the user has a name, show that on the Navigation,
/users
page, and/users/<userId>/posts
page instead. -
On the login page, appropriately handle errors so that the user has a chance to correct their username/password combination. Display some kind of helpful message.
-
On the signup page, appropriately handle errors so that the user has a chance to correct their username/password combination. Display some kind of helpful message.
-
[] On the create post page, appropriately handle errors so that the user has a chance to correct their post. Display some kind of helpful message.
-
[] On the update post page, appropriately handle errors so that the user has a chance to correct their post. Display some kind of helpful message.
-
Create a new frontend route at
/users/<userId>/posts/<postId>
that shows a single post. Update your Create and Edit forms to redirect here instead of to the general/posts
page.