Actions for My Bookmark Resource
Today I will be explaining my actions/bookmarks.js file of my React-Redux frontend application.
I utilize actions to portray CRUD functionality, Fetch requests to my Rails API backend, and the resolving or rejecting of Fetch Promises.
🌱☁️This is really for myself, but if you want to read along then come join me!☁️🌱
Actions
Actions are JavaScript objects that tell us how to and/or what we want to change to the state. Since it is a JavaScript object, an action will have key-value pairs as properties. Specifically, for an action to do its job, it must have a "type" property and a "payload" property. (The payload property can go by any name; such as "person" or "data".)
A simple example of an action object is:
const addTodo = {
type: 'ADD_TODO',
payload: 'Buy milk'
}
We have our action object "addTodo" and it has a type property of 'ADD_TODO' and a payload property of 'Buy milk'. It clearly describes how and what it wants to change to the state; the action wants to add a todo with the data 'Buy milk'. Although this is a simple example, actions can get very complex. For example, an action might be a nested object:
const addTodo = {
type: 'ADD_TODO',
payload: {
todo_name:'Buy milk',
category: 'Groceries',
completed: false
}
}
We have a payload object inside an action object. This can continue for many levels as we know that data reflects the complexity of an application.
As complexity ensues, Redux introduced the concept of "action creators". Action creators are JS functions that return an action object. Its primary purpose is to bind our action object to dispatch. Dispatch takes in an action object and passes it to the reducer to invoke the reducer to make the change and return the new state. By binding our action to dispatch we can ultimately connect or bridge our actions to our reducers to make the change.
I used action creators in my actions/bookmarks.js file to connect to the reducers and to ensure the ability to make fetch requests to my Rails API backend.
☁️☁️Let's take a look at some code!☁️☁️
// actions/bookmarks.js
import { CREATE_BOOKMARK, GET_BOOKMARKS, DELETE_BOOKMARK, FAVORITE_BOOKMARK, ERROR, LOADING_BOOKMARKS, UPDATE_QUERY } from './types'
import { getToken } from './users'
// actions/users.js
export function getToken() {
return localStorage.getItem("token")
}
Above, I import my action types from a types folder I built to store them all. As we now know, an action type is the first key-value pair of an action object and essential to an action's purpose.
I also import a function "getToken" from my actions/users.js file because I have Devise-JWT Authentication. I listed the function above as well just for reference.
export function createBookmark(data){
return (dispatch) => {
dispatch({ type: LOADING_BOOKMARKS})
fetch('http://localhost:3000/bookmarks', {
method: "post",
headers: {
"Content-Type": "application/json",
Authorization: getToken()
},
body: JSON.stringify(data)
})
.then(response => {
if (response.ok) {
response.json().then(json => {
dispatch({type: CREATE_BOOKMARK, payload: json})
})
} else {
return response.json().then((json) => {
return Promise.reject(json)
})
}
})
.catch(error => {
dispatch({type: ERROR, payload: error})
})
}
}
My first action creator is a function called "createBookmark". It takes in an argument of data (which reflects the input from the bookmark form on the frontend). I initiate a return statement that passes in dispatch as to be used with action objects within my return statement. I first dispatch an action of "LOADING_BOOKMARKS". This tells me that my Redux store is loading the current bookmarks of my current user and then I can see the action occur in my Redux devTools. This is a point of organization for me; it lets me know what is occurring in the store.
I then dispatch a fetch request to 'localhost:3000/bookmarks'. Since I am creating a bookmark, I need to post my fetch data to '/bookmarks' as if my Promise is resolved the new bookmark will be added to the bookmarks object in my backend. Within fetch, I use a "post" method to tell my request I am adding something. I use headers to accept a content-type of JSON since my backend bookmarks object is serialized and written in JSON. My second header "Authorization: getToken()" touches upon my user authentication. I use the imported function "getToken()" to say I want to associate this created bookmark with my current user reflected by the token.
To finish my fetch request, I use JSON.stringify() method to convert a JavaScript object or value to a JSON string and I pass it the data. I utilize a series of Promise instance methods ".then()" to check if the response is "ok" and if the Promise is resolved. If so, I take the JSON from the response and use it as my payload key's value to be dispatched to my action object "CREATE_BOOKMARK". If the response is not ok, the Promise is rejected.
** A Note on Promises: A Promise is begins as pending and its result value is undefined. A fulfilled Promise is 'resolved' and results in a value (the payload) and a successful callback. Meanwhile, a rejected (or failed) Promise results in an error which is usually a failure callback. These three Promise statuses: fulfilled, pending and rejected determine the behavior and the result of the fetch request.**
Only if the Promise is reject is the .catch() instance method invoked and dispatches an action object of ERROR.
Let's take a look at another action creator:
export function favoriteBookmark(id, favorite){
return(dispatch, getState) => {
const bookmark = getState().bookmarks.bookmarks.find(bookmark => bookmark.id === id)
const data = {
headline: bookmark.headline,
web_url: bookmark.web_url,
description: bookmark.description,
id: id,
favorite: favorite
}
const configObject = {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Accepts: 'application/json',
Authorization: getToken()
},
body: JSON.stringify(data)
}
fetch(`http://localhost:3000/bookmarks/${id}`, configObject)
.then(response => {
if (response.ok) {
response.json().then(json => {
dispatch({type: FAVORITE_BOOKMARK, payload: json})
})
} else {
return response.json().then((json) => {
return Promise.reject(json)
})
}
})
.catch(error => {
dispatch({type: ERROR, payload: error})
})
}
}
This action creator, "favoriteBookmark" takes in two (2) arguments: 'id' and 'favorite'. I initiate a return statement with two (2) arguments as well: 'dispatch' and 'getState'. We learned about dispatch already, so what is getState? getState is a function given to us from the Redux store. It returns the current state of my application. Using getState, I search through current state of the my bookmarks data and use a find method to check if the id I passed into our action creator matches the id of the bookmark I am trying to favorite. I set this value to a constant "bookmark".
I declare and assign another constant "data" to the attributes of the bookmark I am trying to favorite. As the "headline", "web_url" and "description" attributes of my bookmark will not be changed, I set them to their current value. I assign the attributes 'id' and 'favorite' to the params I passed into our action creator.
I optionally created another constant "configObject" to declare my method "PATCH" (as I am updating an already existing bookmark), my headers (including Authorization) and my body which I pass my data constant to be stringified into a JSON string from a JSON object.
I pass my configObject into my fetch request and specify the "id" attribute of the bookmark I am trying to favorite in my localhost endpoint. Similarly to my createBookmark action creator, I check if the response is ok and dispatch my action object with a payload of json. If my response is not ok, my Promise is rejected and I dispatch an ERROR action object in my .catch() instance method.
🌱🌱 🌱 🌱 🌱
Again, this is just for my own amusement + studying. Yet, if you do find yourself reading over this and you would like to suggest something, ask a question or continue the discussion, please feel free!
Top comments (0)